관리자 (CEO) Advisory
document.getElementById('app').insertAdjacentHTML('afterbegin', renderSidebar('admin')); // ---- 권한 체크 ---- function checkAccess() { const s = Session.get(); if (!s) return false; const role = s.role || ''; return role.includes('원장') || role.includes('관리자') || role.includes('CEO'); } function renderAccessDenied() { document.getElementById('contentArea').innerHTML = `
🔐

CEO 권한이 필요합니다

이 페이지는 원장 또는 관리자 (CEO) 역할만 접근할 수 있습니다.

🏠 홈으로 돌아가기
`; } function renderCEO() { const kpi = SampleData.kpi; document.getElementById('contentArea').innerHTML = `

👔 CEO Advisory

AI가 KPI와 컨텍스트를 분석해 실행 가능한 전략을 제안합니다

분석 대기
전환율
${kpi.conversionRate}%
↑ ${kpi.conversionRateDelta}% vs 지난주
평균 상담시간
${kpi.avgConsultMin}분
↓ ${Math.abs(kpi.avgConsultMinDelta)}분
재방문율
${kpi.revisitRate}%
↑ ${kpi.revisitRateDelta}%
AI 활용률
${kpi.aiUsageRate}%
↑ ${kpi.aiUsageRateDelta}%
월 매출
${formatCurrency(kpi.monthlyRevenue)}
↑ ${kpi.monthlyRevenueDelta}%
관리 환자
${SampleData.patients.length}
활성

🧠 AI 전략 조언

💡
위 버튼을 눌러 AI 전략 조언을 생성하세요.
(Gemini API 키 미설정 시 데모 응답)

🗺 조직 온톨로지

👥 팀 성과

팀원 역할 상담 수 전환율 평균 상담시간 환자 만족도 AI 활용
${isSuperAdmin() ? `

👥 회원 관리 (SaaS 본사 전용)

이메일 이름 병원 역할 요금제 이번달 사용 본사 관리자 가입일
불러오는 중...
` : ''} `; drawOntology(); renderTeam(); if (isSuperAdmin()) renderMembersTable(); } // ---- 본사 관리자(SaaS) 권한 확인 ---- function isSuperAdmin() { const s = Session.get(); return s && s.is_admin === true; } // ---- 회원 관리 테이블 렌더 ---- async function renderMembersTable() { const tbody = document.getElementById('membersTable'); const summary = document.getElementById('membersSummary'); if (!tbody) return; tbody.innerHTML = '불러오는 중...'; try { const [users, usage] = await Promise.all([ SupabaseDB.listUsers(), SupabaseDB.getAllMonthlyUsage() ]); const tierLimits = { free: 3, pro: 20, max: 60 }; let free = 0, pro = 0, max = 0; const rows = users.map(u => { const used = usage[u.id] || 0; const tier = u.tier || 'free'; const limit = tierLimits[tier]; const ratio = limit ? used / limit : 0; const isOver = !u.is_admin && used >= limit; const isNear = !u.is_admin && !isOver && ratio >= 0.8; const usageColor = isOver ? 'var(--danger)' : isNear ? 'var(--warning)' : 'var(--text-primary)'; const usageWeight = (isOver || isNear) ? '700' : '500'; const usageIcon = isOver ? ' 🔴' : isNear ? ' ⚠️' : ''; if (tier === 'free') free++; else if (tier === 'pro') pro++; else if (tier === 'max') max++; const safeEmail = (u.email || '').replace(/ ${safeEmail} ${safeName} ${safeClinic} ${safeRole} ${used} / ${u.is_admin ? '∞' : limit}${usageIcon} ${u.is_admin ? '✅' : '—'} ${(u.created_at || '').slice(0, 10)} `; }).join(''); tbody.innerHTML = rows || '등록된 회원이 없습니다'; if (summary) { summary.innerHTML = `
전체: ${users.length}명 🆓 Free ${free} ⭐ Pro ${pro} 🚀 Max ${max}
`; } } catch (e) { console.error('회원 조회 실패', e); tbody.innerHTML = `회원 조회 실패: ${e.message}`; } } // ---- tier 변경 ---- async function changeTier(userId, tier) { try { await SupabaseDB.updateUserTier(userId, tier); showToast('요금제 변경 완료', 'success'); renderMembersTable(); } catch (e) { showToast('변경 실패: ' + e.message, 'error'); } } // ---- Ontology SVG ---- function drawOntology() { const structure = OntologyEngine.defaultStructure; const svg = document.getElementById('ontoGraph'); if (!svg) return; const roles = structure.roles; const processes = structure.processes; const flows = structure.flows; // 프로세스 가로 배치 (상단) const procPositions = {}; const procY = 60; const procSpacing = 560 / (processes.length - 1); processes.forEach((p, i) => { procPositions[p] = { x: 20 + i * procSpacing, y: procY }; }); // 역할 가로 배치 (하단) const rolePositions = {}; const roleY = 210; const roleSpacing = 560 / (roles.length - 1); roles.forEach((r, i) => { rolePositions[r] = { x: 20 + i * roleSpacing, y: roleY }; }); let html = ''; // 플로우 (프로세스 간 화살표) flows.forEach(f => { const from = procPositions[f.from]; const to = procPositions[f.to]; if (from && to) { html += ``; } }); // 역할 → 프로세스 (담당) 연결 flows.forEach(f => { const role = rolePositions[f.role]; const proc = procPositions[f.to]; if (role && proc) { html += ``; } }); // 프로세스 노드 Object.entries(procPositions).forEach(([name, pos]) => { html += ` ${name} `; }); // 역할 노드 Object.entries(rolePositions).forEach(([name, pos]) => { html += ` ${name} `; }); // 라벨 html += `프로세스 (Process)`; html += `역할 (Role)`; // 마커 html += ``; svg.innerHTML = html; } // ---- Team Performance ---- function renderTeam() { const team = [ { name: '김원장', role: '원장', consults: 42, conversion: 74, avgMin: 16, satisfaction: 4.8, ai: 78 }, { name: '박실장', role: '상담실장', consults: 128, conversion: 68, avgMin: 14, satisfaction: 4.7, ai: 92 }, { name: '이코디', role: '코디네이터', consults: 86, conversion: 62, avgMin: 12, satisfaction: 4.5, ai: 85 }, { name: '정코디', role: '코디네이터', consults: 74, conversion: 58, avgMin: 13, satisfaction: 4.4, ai: 71 }, { name: '최위생', role: '치위생사', consults: 38, conversion: 52, avgMin: 15, satisfaction: 4.6, ai: 64 }, ]; document.getElementById('teamTable').innerHTML = team.map(t => { const convColor = t.conversion >= 65 ? 'var(--success)' : t.conversion >= 55 ? 'var(--warning)' : 'var(--danger)'; return ` ${t.name} ${t.role} ${t.consults}건 ${t.conversion}% ${t.avgMin}분 ${'⭐'.repeat(Math.floor(t.satisfaction))} ${t.satisfaction} ${t.ai}% `; }).join(''); } // ---- CEO Advise ---- async function generateAdvice() { const btn = document.getElementById('adviceBtn'); const ctx = document.getElementById('contextInput').value.trim(); btn.disabled = true; btn.innerHTML = '
분석 중...'; document.getElementById('adviceResult').innerHTML = `

Gemini로 전략 분석 중...

`; try { // 실데이터 우선: Supabase KPI 스냅샷 최신 1건 or 대시보드 집계 → 없으면 SampleData.kpi let kpis = SampleData.kpi; if (typeof SupabaseDB !== 'undefined' && SupabaseDB.isReady()) { try { const agg = await SupabaseDB.getDashboardAggregates(); if (agg && agg.kpi) kpis = agg.kpi; } catch (_) {} } const res = await CEOEngine.advise({ kpis, context: ctx }); const data = res.data || res; renderAdvice(data, res.demo); // Supabase 저장 — consult_logs engine='ceo' if (typeof SupabaseDB !== 'undefined' && SupabaseDB.isReady()) { try { await SupabaseDB.saveConsultLog({ engine: 'ceo', input: ctx, output: (data.top_3_actions || []).map((a,i)=>`${i+1}. ${a.action}`).join(' / '), metadata: { state: data.state, top_3_actions: data.top_3_actions, risks: data.risks, roi_estimate: data.roi_estimate, author: Session.get()?.name, clinic: Session.get()?.clinic, kpis } }); } catch (e) { console.warn('CEO 조언 저장 실패', e); } } } catch (e) { const r = document.getElementById('adviceResult'); r.textContent = ''; const errEl = document.createElement('div'); errEl.style.cssText = 'padding:16px; color:var(--danger);'; errEl.textContent = '오류: ' + e.message; r.appendChild(errEl); } finally { btn.disabled = false; btn.innerHTML = '✨ 다시 생성'; } } function renderAdvice(data, isDemo) { // State badge const stateBadge = document.getElementById('stateBadge'); const stateLabel = { growth: '🚀 성장 (Growth)', steady: '🛬 안정 (Steady)', caution: '⚠ 주의 (Caution)', danger: '🚨 위험 (Danger)' }; stateBadge.className = 'state-badge ' + (data.state || 'steady'); stateBadge.innerHTML = stateLabel[data.state] || '🛬 안정'; const actions = data.top_3_actions || []; const risks = data.risks || []; let html = ''; if (isDemo) { html += `
⚠ Gemini API 미연결 — 데모 응답입니다
`; } html += `

🎯 우선순위 Top 3 액션

`; actions.forEach((a, i) => { html += `
${a.priority || i + 1} ${a.action}
근거: ${a.why || '-'}
예상 효과: ${a.expected_impact || '-'}
기간: ${a.timeframe || '-'}
`; }); if (risks.length) { html += `

⚠ 리스크

`; html += `
${risks.map(r => `${r}`).join('')}
`; } if (data.roi_estimate) { html += `
예상 ROI
${data.roi_estimate}
`; } document.getElementById('adviceResult').innerHTML = html; } // ---- Ontology Automation ---- async function generateAutomation() { const btn = document.getElementById('autoBtn'); btn.disabled = true; btn.innerHTML = '분석 중...'; document.getElementById('autoResult').innerHTML = `

자동화 후보 분석 중...

`; try { const res = await OntologyEngine.suggestAutomation(OntologyEngine.defaultStructure); const data = res.data || res; const candidates = data.automation_candidates || []; // Supabase 저장 — consult_logs engine='ontology' if (typeof SupabaseDB !== 'undefined' && SupabaseDB.isReady()) { try { await SupabaseDB.saveConsultLog({ engine: 'ontology', input: '자동화 후보 분석 요청', output: candidates.map(c => `${c.process}:${c.automation_type}`).join(' / '), metadata: { candidates, author: Session.get()?.name, clinic: Session.get()?.clinic } }); } catch (e) { console.warn('자동화 제안 저장 실패', e); } } const priorityBadge = { high: 'badge-danger', mid: 'badge-warning', low: 'badge-primary' }; let html = `

${res.demo ? '⚠ 데모 응답' : '💡 AI 제안'} — 자동화 후보

`; html += `
`; candidates.forEach(c => { html += ``; }); html += `
프로세스현재 담당자동화 유형절감우선순위
${c.process} ${c.current_role} ${c.automation_type} ${c.expected_saving_hours_per_week}h/주 ${c.priority}
`; document.getElementById('autoResult').innerHTML = html; } catch (e) { const r = document.getElementById('autoResult'); r.textContent = ''; const errEl = document.createElement('div'); errEl.style.cssText = 'color:var(--danger); padding:12px; font-size:0.8125rem;'; errEl.textContent = '오류: ' + e.message; r.appendChild(errEl); } finally { btn.disabled = false; btn.innerHTML = '⚙ 다시 분석'; } } // ---- 초기화 ---- setTimeout(() => { if (checkAccess()) renderCEO(); else renderAccessDenied(); }, 100);