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 전략 조언을 생성하세요.
(Gemini API 키 미설정 시 데모 응답)
| 팀원 |
역할 |
상담 수 |
전환율 |
평균 상담시간 |
환자 만족도 |
AI 활용 |
${isSuperAdmin() ? `
| 이메일 |
이름 |
병원 |
역할 |
요금제 |
이번달 사용 |
본사 관리자 |
가입일 |
| 불러오는 중... |
` : ''}
`;
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 = ``;
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 += `
| ${c.process} |
${c.current_role} |
${c.automation_type} |
${c.expected_saving_hours_per_week}h/주 |
${c.priority} |
`;
});
html += `
`;
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);