site-agent / admin_dashboard.html
ginigen-ai's picture
Upload 2 files
d86a256 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>SiteAgent Admin — Intelligence Dashboard</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro','Pretendard','Helvetica Neue',sans-serif;background:#f5f5f7;color:#1d1d1f;font-size:12px;}
::-webkit-scrollbar{width:5px;}::-webkit-scrollbar-thumb{background:rgba(88,86,214,.2);border-radius:4px;}
.hdr{background:linear-gradient(135deg,#5856D6,#4f46e5);padding:20px 24px;position:sticky;top:0;z-index:100;box-shadow:0 2px 20px rgba(88,86,214,.3);}
.hdr h1{font-size:18px;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px;}
.hdr h1 span{color:#fff;}
.hdr p{font-size:10px;color:rgba(255,255,255,.7);margin-top:4px;}
.ct{max-width:1200px;margin:0 auto;padding:16px;}
.tabs{display:flex;gap:4px;margin-bottom:16px;flex-wrap:wrap;background:#fff;padding:6px;border-radius:12px;border:1px solid rgba(0,0,0,.06);box-shadow:0 1px 4px rgba(0,0,0,.03);}
.tab{padding:7px 14px;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;color:#8e8e93;transition:all .15s;border:none;background:none;}
.tab:hover{color:#1d1d1f;background:rgba(0,0,0,.03);}
.tab.on{background:#5856D6;color:#fff;box-shadow:0 2px 12px rgba(88,86,214,.25);}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px;}
.sc{background:#fff;border-radius:14px;padding:16px;border:1px solid rgba(0,0,0,.04);box-shadow:0 1px 6px rgba(0,0,0,.03);transition:all .2s;}
.sc:hover{border-color:rgba(88,86,214,.15);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.06);}
.sc .n{font-size:26px;font-weight:800;color:#5856D6;}
.sc .l{font-size:10px;color:#8e8e93;margin-top:2px;}
.sec{background:#fff;border-radius:14px;padding:16px;margin-bottom:12px;border:1px solid rgba(0,0,0,.04);box-shadow:0 1px 6px rgba(0,0,0,.03);}
.sec h2{font-size:13px;font-weight:700;margin-bottom:10px;padding-bottom:6px;border-bottom:1.5px solid rgba(88,86,214,.08);color:#5856D6;}
table{width:100%;border-collapse:collapse;font-size:11px;}
th{text-align:left;padding:6px 8px;color:#8e8e93;font-weight:600;font-size:9px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid rgba(0,0,0,.06);}
td{padding:6px 8px;border-bottom:1px solid rgba(0,0,0,.03);vertical-align:top;}
tr:hover{background:rgba(88,86,214,.02);}
.bg{display:inline-block;padding:2px 7px;border-radius:5px;font-size:9px;font-weight:600;}
.bg-a{background:rgba(88,86,214,.1);color:#5856D6;}.bg-u{background:rgba(0,0,0,.04);color:#666;}
.bg-f{background:rgba(13,148,136,.08);color:#0d9488;}.bg-s{background:rgba(234,179,8,.08);color:#a16207;}
.bg-d{background:rgba(239,68,68,.08);color:#dc2626;}.bg-g{background:rgba(34,197,94,.08);color:#16a34a;}
.db{padding:3px 8px;background:#ff3b30;color:#fff;border:none;border-radius:5px;font-size:9px;cursor:pointer;font-weight:600;transition:all .15s;}
.db:hover{background:#d63030;}
.det-btn{padding:3px 8px;background:#5856D6;color:#fff;border:none;border-radius:5px;font-size:9px;cursor:pointer;font-weight:600;transition:all .15s;}
.det-btn:hover{background:#4f46e5;}
.bar{height:6px;background:rgba(88,86,214,.06);border-radius:4px;margin-top:3px;}
.bf{height:100%;border-radius:4px;transition:width .3s;}
.bf-p{background:linear-gradient(90deg,#5856D6,#007AFF);}
.bf-t{background:linear-gradient(90deg,#0d9488,#059669);}
.bf-a{background:linear-gradient(90deg,#f59e0b,#ef4444);}
.emp{text-align:center;color:#c7c7cc;padding:20px;font-size:11px;}
.modal{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:200;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);}
.modal-body{background:#fff;border:1px solid rgba(0,0,0,.06);border-radius:16px;max-width:800px;width:95%;max-height:85vh;overflow-y:auto;padding:24px;box-shadow:0 20px 60px rgba(0,0,0,.15);}
.modal-close{float:right;background:none;border:none;font-size:20px;cursor:pointer;color:#8e8e93;}
.rfb{position:fixed;bottom:16px;right:16px;padding:10px 18px;background:#5856D6;color:#fff;border:none;border-radius:10px;font-size:11px;font-weight:700;cursor:pointer;box-shadow:0 4px 16px rgba(88,86,214,.3);z-index:50;}
.chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;font-size:10px;font-weight:600;margin:2px;}
.chip-i{background:rgba(88,86,214,.08);color:#5856D6;}
.chip-p{background:rgba(13,148,136,.08);color:#0d9488;}
.chip-k{background:rgba(234,179,8,.06);color:#a16207;}
.chip-c{background:rgba(59,130,246,.06);color:#2563eb;}
.cta-card{background:linear-gradient(135deg,rgba(88,86,214,.04),rgba(0,122,255,.03));border:1px solid rgba(88,86,214,.1);border-radius:10px;padding:10px 14px;margin:4px 0;display:flex;align-items:center;justify-content:space-between;}
.cta-card .cta-label{font-weight:700;font-size:12px;color:#5856D6;}
.cta-card .cta-reason{font-size:9px;color:#8e8e93;}
.cta-card .cta-prio{background:#5856D6;color:#fff;padding:2px 8px;border-radius:10px;font-size:9px;font-weight:800;}
.hour-bar{display:inline-block;width:12px;margin:0 1px;background:rgba(88,86,214,.08);border-radius:2px 2px 0 0;vertical-align:bottom;transition:height .3s;}
.hour-active{background:linear-gradient(180deg,#5856D6,#007AFF);}
.seg-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;}
.seg-card{background:#fff;border:1px solid rgba(0,0,0,.04);border-radius:12px;padding:14px;text-align:center;box-shadow:0 1px 6px rgba(0,0,0,.03);}
.seg-card .seg-n{font-size:32px;font-weight:800;color:#5856D6;}
.seg-card .seg-l{font-size:10px;color:#8e8e93;margin-top:4px;}
.pane{display:none;}.pane.active{display:block;}
</style>
</head>
<body>
<div class="hdr"><h1>🔑 <span>SiteAgent</span> Intelligence Dashboard</h1><p id="updateTime"></p><p style="font-size:9px;color:rgba(255,255,255,.5);margin-top:2px;">admin: <span id="adminDisplay"></span></p></div>
<div class="ct">
<div class="tabs" id="tabBar">
<div class="tab on" data-t="overview">📊 개요</div>
<div class="tab" data-t="insights">🔥 인사이트</div>
<div class="tab" data-t="analytics">📈 분석</div>
<div class="tab" data-t="users">👥 사용자</div>
<div class="tab" data-t="segments">🎯 세그먼트</div>
<div class="tab" data-t="visits">🌐 방문</div>
<div class="tab" data-t="inputs">📝 입력</div>
<div class="tab" data-t="features">⚡ 기능</div>
<div class="tab" data-t="waitlist">📋 웨이팅</div>
</div>
<div id="overview" class="pane active"></div>
<div id="insights" class="pane"></div>
<div id="analytics" class="pane"></div>
<div id="users" class="pane"></div>
<div id="segments" class="pane"></div>
<div id="visits" class="pane"></div>
<div id="inputs" class="pane"></div>
<div id="features" class="pane"></div>
<div id="waitlist" class="pane"></div>
</div>
<div id="modalWrap"></div>
<button class="rfb" onclick="loadAll()">🔄 새로고침</button>
<script>
var API=location.origin;
var ADMIN=new URLSearchParams(location.search).get('key')||'';
var D={};
/* ── 탭 전환 ── */
document.getElementById('tabBar').addEventListener('click',function(e){
var tab=e.target.closest('.tab');if(!tab)return;
document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('on');});
tab.classList.add('on');
document.querySelectorAll('.pane').forEach(function(p){p.classList.remove('active');});
var t=tab.getAttribute('data-t');
document.getElementById(t).classList.add('active');
});
function ts(v){return v?new Date(v*1000).toLocaleString('ko-KR',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'—';}
function esc(s){var d=document.createElement('div');d.textContent=s||'';return d.innerHTML;}
function card(icon,num,label){return '<div class="sc"><div class="n">'+icon+' '+num+'</div><div class="l">'+label+'</div></div>';}
function barHTML(items,maxVal,cls){
var h='';if(!maxVal)maxVal=items[0]?.cnt||1;
items.forEach(function(v){
h+='<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:10px;"><span>'+esc(v.label||v.domain||v.feature||v.event||v.name||'')+'</span><b style="color:#5856D6;">'+v.cnt+'</b></div><div class="bar"><div class="bf '+(cls||'bf-p')+'" style="width:'+Math.round(v.cnt/maxVal*100)+'%"></div></div></div>';
});
return h||'<div class="emp">데이터 없음</div>';
}
async function api(path){
var url=API+path+(path.includes('?')?'&':'?')+'admin='+encodeURIComponent(ADMIN);
console.log('[SA-Admin] fetch:',url);
var r=await fetch(url);
if(!r.ok){
var t=await r.text().catch(function(){return '';});
console.error('[SA-Admin] API error:',r.status,path,t);
throw new Error(r.status+' '+path);
}
var d=await r.json();
console.log('[SA-Admin] data:',path,JSON.stringify(d).substring(0,200));
return d;
}
async function loadAll(){
if(!ADMIN){
document.getElementById('overview').innerHTML='<div class="emp">❌ 관리자 키가 없습니다.<br>URL에 <b>?key=관리자이메일</b>을 추가하세요.</div>';
return;
}
async function safeApi(path){
try{
var url=API+path+(path.includes('?')?'&':'?')+'admin='+encodeURIComponent(ADMIN);
var r=await fetch(url);
if(!r.ok) return null;
return await r.json();
}catch(e){return null;}
}
D.dash=await safeApi('/api/admin/dashboard')||{total_users:0,total_visits:0,total_inputs:0,top_domains:[],top_features:[],recent_users:[]};
D.inputs=await safeApi('/api/admin/inputs')||{inputs:[]};
D.allVisits=await safeApi('/api/admin/all-visits')||{visits:[]};
D.waitlist=await safeApi('/api/admin/waitlist')||{waitlist:[]};
D.analytics=await safeApi('/api/admin/analytics')||{};
D.segments=await safeApi('/api/admin/segments')||{};
D.insights={};
render();
document.getElementById('updateTime').textContent='Last: '+new Date().toLocaleTimeString('ko-KR');
document.getElementById('insights').innerHTML='<div class="emp"><div class="sd" style="display:inline-flex;margin-right:8px;"><i></i><i style="animation-delay:.15s"></i><i style="animation-delay:.3s"></i></div>🔥 인사이트 분석 중... (사용자가 많으면 10~30초 소요)</div>';
safeApi('/api/admin/insights').then(function(d){
D.insights=d||{};
renderInsights();
});
}
function render(){
renderOverview();
renderInsights();
renderAnalytics();
renderUsers();
renderSegments();
renderVisits();
renderInputs();
renderFeatures();
renderWaitlist();
}
function renderOverview(){
var d=D.dash||{};var a=D.analytics||{};
var h='<div class="stats">'+card('👥',d.total_users||0,'총 사용자')+card('🌐',d.total_visits||0,'총 방문')+card('📝',d.total_inputs||0,'총 입력')+card('📈',a.today_events||0,'오늘 이벤트')+card('📊',a.week_events||0,'이번주 이벤트')+'</div>';
/* AI 성능 */
var perf=a.ai_performance||{};
if(perf.cnt){
h+='<div class="stats">'+card('⚡',Math.round(perf.avg_ms||0)+'ms','평균 AI 응답')+card('✅',perf.cnt||0,'AI 호출 수')+card('❌',perf.errors||0,'AI 에러')+'</div>';
}
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>🌐 TOP 도메인</h2>';
var td=d.top_domains||[];if(td.length){var mx=td[0].cnt;h+=barHTML(td.map(function(v){return{label:v.domain,cnt:v.cnt};}),mx,'bf-p');}else h+='<div class="emp">없음</div>';
h+='</div>';
h+='<div class="sec"><h2>⚡ 기능 사용</h2>';
var tf=d.top_features||[];if(tf.length){var mx=tf[0].cnt;h+=barHTML(tf.map(function(v){return{label:v.feature,cnt:v.cnt};}),mx,'bf-t');}else h+='<div class="emp">없음</div>';
h+='</div></div>';
document.getElementById('overview').innerHTML=h;
}
function renderInsights(){
var ins=D.insights||{};
if(!ins.total_analyzed){document.getElementById('insights').innerHTML='<div class="emp">아직 분석할 사용자 데이터가 없습니다. 사용자가 활동하면 자동으로 채워집니다.</div>';return;}
var h='<div class="stats">'+card('👥',ins.total_analyzed,'분석된 사용자')+card('💯',ins.avg_engagement,'평균 참여도')+card('🔥',ins.total_action_signals,'구매/전환 신호')+'</div>';
h+='<div class="sec"><h2>🎯 CTA 추천 랭킹 (전체 사용자 통합)</h2>';
var ctas=ins.cta_ranking||[];
if(ctas.length){
ctas.forEach(function(c){
h+='<div class="cta-card"><div style="flex:1;"><div class="cta-label">'+esc(c.label)+'</div><div class="cta-reason">'+esc(c.reason)+' <span class="bg bg-s">'+esc(c.source||'')+'</span></div></div><div style="display:flex;align-items:center;gap:8px;"><span style="font-size:11px;font-weight:700;color:#1d1d1f;">👥 '+c.user_count+'명</span><div class="cta-prio">P'+c.priority+'</div></div></div>';
});
}else h+='<div class="emp">CTA 추천 없음</div>';
h+='</div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>🎯 입력 의도 분류 (전체)</h2>';
var ir=ins.intent_ranking||[];
if(ir.length){var mx=ir[0].score;ir.forEach(function(i){h+='<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:10px;"><span>'+esc(i.intent)+'</span><b style="color:#5856D6;">'+i.score+'</b></div><div class="bar"><div class="bf bf-p" style="width:'+Math.round(i.score/mx*100)+'%"></div></div></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div>';
h+='<div class="sec"><h2>📂 관심 주제 클러스터 (전체)</h2>';
var tc=ins.topic_ranking||[];
if(tc.length){var mx=tc[0].count;tc.forEach(function(t){h+='<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:10px;"><span>'+esc(t.topic)+'</span><b style="color:#0d9488;">'+t.count+'</b></div><div class="bar"><div class="bf bf-t" style="width:'+Math.round(t.count/mx*100)+'%"></div></div></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div></div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>❤️ 관심 카테고리 (도메인 기반)</h2>';
var ic=ins.interest_ranking||[];
if(ic.length){var mx=ic[0].score;ic.forEach(function(i){h+='<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:10px;"><span>'+esc(i.category)+'</span><b style="color:#5856D6;">'+i.score+'</b></div><div class="bar"><div class="bf bf-p" style="width:'+Math.round(i.score/mx*100)+'%"></div></div></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div>';
h+='<div class="sec"><h2>🧬 사용자 유형 (기능 기반)</h2>';
var pr=ins.persona_ranking||[];
if(pr.length){var mx=pr[0].score;pr.forEach(function(p){h+='<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:10px;"><span>'+esc(p.type)+'</span><b style="color:#0d9488;">'+p.score+'</b></div><div class="bar"><div class="bf bf-t" style="width:'+Math.round(p.score/mx*100)+'%"></div></div></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div></div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>📊 질문 복잡도 (전체)</h2>';
var cx=ins.complexity||{};
var cxT=(cx.simple||0)+(cx.medium||0)+(cx.complex||0)||1;
h+='<div style="display:flex;height:28px;border-radius:8px;overflow:hidden;margin-bottom:8px;">';
if(cx.simple)h+='<div style="width:'+(cx.simple/cxT*100)+'%;background:#22c55e;display:flex;align-items:center;justify-content:center;font-size:10px;color:#fff;font-weight:700;">단순 '+cx.simple+'</div>';
if(cx.medium)h+='<div style="width:'+(cx.medium/cxT*100)+'%;background:#f59e0b;display:flex;align-items:center;justify-content:center;font-size:10px;color:#fff;font-weight:700;">보통 '+cx.medium+'</div>';
if(cx.complex)h+='<div style="width:'+(cx.complex/cxT*100)+'%;background:#8b5cf6;display:flex;align-items:center;justify-content:center;font-size:10px;color:#fff;font-weight:700;">복잡 '+cx.complex+'</div>';
h+='</div></div>';
h+='<div class="sec"><h2>🔑 핵심 키워드 (전체 입력)</h2>';
var kw=ins.keyword_ranking||[];
if(kw.length){kw.forEach(function(k){h+='<span class="chip chip-k">'+esc(k.word)+' ×'+k.count+'</span>';});}
else h+='<div class="emp">없음</div>';
h+='</div></div>';
h+='<div class="sec"><h2>👤 사용자별 요약</h2>';
var up=ins.user_profiles||[];
if(up.length){
h+='<table><tr><th>이메일</th><th>참여도</th><th>TOP CTA</th><th>TOP 의도</th><th>전환신호</th><th>입력수</th><th>프로파일</th></tr>';
up.forEach(function(u){
var engColor=u.engagement>=70?'#22c55e':u.engagement>=40?'#f59e0b':'#8e8e93';
h+='<tr><td style="font-size:10px;">'+esc(u.email)+'</td>';
h+='<td><b style="color:'+engColor+';">'+u.engagement+'</b>/100</td>';
h+='<td style="font-size:9px;">'+esc(u.top_cta)+'</td>';
h+='<td><span class="bg bg-f">'+esc(u.top_intent)+'</span></td>';
h+='<td>'+(u.action_signals?'<span class="bg bg-d">🔥 '+u.action_signals+'</span>':'—')+'</td>';
h+='<td>'+u.input_count+'</td>';
h+='<td><button class="det-btn" onclick="showFullProfile(\''+esc(u.email)+'\')">상세</button></td></tr>';
});
h+='</table>';
}else h+='<div class="emp">없음</div>';
h+='</div>';
document.getElementById('insights').innerHTML=h;
}
function renderAnalytics(){
var a=D.analytics||{};
var h='<div class="stats">'+card('📈',a.today_events||0,'오늘')+card('📊',a.week_events||0,'이번주')+'</div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
/* 이벤트 유형 */
h+='<div class="sec"><h2>📋 이벤트 유형 (7일)</h2>';
var ev=a.events_by_type||[];if(ev.length){var mx=ev[0].cnt;h+=barHTML(ev,mx,'bf-p');}else h+='<div class="emp">없음</div>';
h+='</div>';
/* 기능별 */
h+='<div class="sec"><h2>⚡ 기능별 사용 (7일)</h2>';
var fw=a.features_week||[];if(fw.length){var mx=fw[0].cnt;h+=barHTML(fw,mx,'bf-t');}else h+='<div class="emp">없음</div>';
h+='</div></div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">';
/* 클라이언트 */
h+='<div class="sec"><h2>📱 클라이언트</h2>';
var cl=a.clients||[];if(cl.length){h+=barHTML(cl.map(function(v){return{label:v.client,cnt:v.cnt};}),cl[0]?.cnt,'bf-p');}else h+='<div class="emp">없음</div>';
h+='</div>';
/* OS */
h+='<div class="sec"><h2>💻 OS</h2>';
var os=a.os_stats||[];if(os.length){h+=barHTML(os.map(function(v){return{label:v.os,cnt:v.cnt};}),os[0]?.cnt,'bf-t');}else h+='<div class="emp">없음</div>';
h+='</div>';
/* 브라우저 */
h+='<div class="sec"><h2>🌐 브라우저</h2>';
var br=a.browser_stats||[];if(br.length){h+=barHTML(br.map(function(v){return{label:v.browser,cnt:v.cnt};}),br[0]?.cnt,'bf-a');}else h+='<div class="emp">없음</div>';
h+='</div></div>';
/* 프로바이더 + MARL 엔진 */
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>🔌 프로바이더</h2>';
var pv=a.providers||[];if(pv.length){h+=barHTML(pv.map(function(v){return{label:v.provider,cnt:v.cnt};}),pv[0]?.cnt,'bf-p');}else h+='<div class="emp">없음</div>';
h+='</div>';
h+='<div class="sec"><h2>🧠 MARL 엔진</h2>';
var en=a.engine_stats||[];if(en.length){h+=barHTML(en.map(function(v){return{label:v.engine,cnt:v.cnt};}),en[0]?.cnt,'bf-t');}else h+='<div class="emp">없음</div>';
h+='</div></div>';
document.getElementById('analytics').innerHTML=h;
}
function renderUsers(){
var d=D.dash||{};
var h='<div class="sec"><h2>👥 전체 사용자 ('+((d.recent_users||[]).length)+'명)</h2>';
if(d.recent_users&&d.recent_users.length){
h+='<table><tr><th>이메일</th><th>닉네임</th><th>역할</th><th>방문</th><th>최근접속</th><th>프로파일</th><th>관리</th></tr>';
d.recent_users.forEach(function(u){
h+='<tr><td>'+esc(u.email)+'</td><td>'+esc(u.nickname)+'</td><td><span class="bg '+(u.role==='admin'?'bg-a':'bg-u')+'">'+u.role+'</span></td><td>'+u.visit_count+'</td><td style="font-size:9px;">'+ts(u.last_seen_at)+'</td>';
h+='<td><button class="det-btn" onclick="showFullProfile(\''+esc(u.email)+'\')">🎯 분석</button></td>';
h+='<td>'+(u.role!=='admin'?'<button class="db" onclick="delUser(\''+esc(u.email)+'\')">삭제</button>':'—')+'</td></tr>';
});
h+='</table>';
}else h+='<div class="emp">사용자 없음</div>';
h+='</div>';
document.getElementById('users').innerHTML=h;
}
function renderSegments(){
var s=D.segments||{};
var h='<div class="seg-grid">';
h+='<div class="seg-card"><div class="seg-n">'+(s.power_users_count||0)+'</div><div class="seg-l">파워 유저 (10+방문 or 20+AI)</div></div>';
h+='<div class="seg-card"><div class="seg-n">'+(s.casual_users_count||0)+'</div><div class="seg-l">일반 유저 (3~10방문)</div></div>';
h+='<div class="seg-card"><div class="seg-n">'+(s.new_users_count||0)+'</div><div class="seg-l">신규 유저 (1~3방문)</div></div>';
h+='</div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px;">';
/* 클라이언트별 */
h+='<div class="sec"><h2>📱 클라이언트별 분포</h2>';
var cl=s.by_client||{};var clArr=Object.entries(cl).map(function(e){return{label:e[0],cnt:e[1]};}).sort(function(a,b){return b.cnt-a.cnt;});
h+=barHTML(clArr,clArr[0]?.cnt,'bf-p');
h+='</div>';
/* 프로바이더별 */
h+='<div class="sec"><h2>🔌 프로바이더별 분포</h2>';
var pv=s.by_provider||{};var pvArr=Object.entries(pv).map(function(e){return{label:e[0],cnt:e[1]};}).sort(function(a,b){return b.cnt-a.cnt;});
h+=barHTML(pvArr,pvArr[0]?.cnt,'bf-t');
h+='</div></div>';
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
/* OS별 */
h+='<div class="sec"><h2>💻 OS 분포</h2>';
var os=s.by_os||{};var osArr=Object.entries(os).map(function(e){return{label:e[0],cnt:e[1]};}).sort(function(a,b){return b.cnt-a.cnt;});
h+=barHTML(osArr,osArr[0]?.cnt,'bf-a');
h+='</div>';
/* 브라우저별 */
h+='<div class="sec"><h2>🌐 브라우저 분포</h2>';
var br=s.by_browser||{};var brArr=Object.entries(br).map(function(e){return{label:e[0],cnt:e[1]};}).sort(function(a,b){return b.cnt-a.cnt;});
h+=barHTML(brArr,brArr[0]?.cnt,'bf-p');
h+='</div></div>';
document.getElementById('segments').innerHTML=h;
}
function renderVisits(){
var visits=(D.allVisits||{}).visits||[];
var h='<div class="sec"><h2>🌐 최근 방문 기록</h2>';
if(visits.length){
h+='<table><tr><th>이메일</th><th>도메인</th><th>URL</th><th>제목</th><th>횟수</th><th>최근</th><th></th></tr>';
visits.forEach(function(v){
h+='<tr><td style="font-size:9px;">'+esc(v.email)+'</td><td><b>'+esc(v.domain)+'</b></td><td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;">'+esc(v.url)+'</td><td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;">'+esc(v.title)+'</td><td>'+v.visit_count+'</td><td style="font-size:9px;">'+ts(v.last_visited_at)+'</td>';
h+='<td><button class="db" onclick="delRecord(\'page_visits\','+v.id+')">삭제</button></td></tr>';
});
h+='</table>';
}else h+='<div class="emp">없음</div>';
h+='</div>';
document.getElementById('visits').innerHTML=h;
}
function renderInputs(){
var inp=(D.inputs||{}).inputs||[];
var h='<div class="sec"><h2>📝 최근 입력 로그</h2>';
if(inp.length){
h+='<table><tr><th>이메일</th><th>기능</th><th>URL</th><th>입력</th><th>시간</th><th></th></tr>';
inp.forEach(function(v){
h+='<tr><td style="font-size:9px;">'+esc(v.email)+'</td><td><span class="bg bg-f">'+esc(v.feature)+'</span></td><td style="max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px;">'+esc(v.url||'')+'</td><td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+esc(v.input_text)+'</td><td style="font-size:9px;">'+ts(v.created_at)+'</td>';
h+='<td><button class="db" onclick="delRecord(\'user_inputs\','+v.id+')">삭제</button></td></tr>';
});
h+='</table>';
}else h+='<div class="emp">없음</div>';
h+='</div>';
document.getElementById('inputs').innerHTML=h;
}
function renderFeatures(){
var d=D.dash||{};
var h='<div class="sec"><h2>⚡ 기능 사용 현황</h2>';
var tf=d.top_features||[];
if(tf.length){
h+='<table><tr><th>기능</th><th>횟수</th><th>비율</th></tr>';
var total=tf.reduce(function(s,v){return s+v.cnt;},0);
tf.forEach(function(v){
var pct=total?Math.round(v.cnt/total*100):0;
h+='<tr><td><b>'+esc(v.feature)+'</b></td><td>'+v.cnt+'회</td><td><div class="bar" style="width:100px;display:inline-block;vertical-align:middle;"><div class="bf bf-t" style="width:'+pct+'%"></div></div> '+pct+'%</td></tr>';
});
h+='</table>';
}else h+='<div class="emp">없음</div>';
h+='</div>';
document.getElementById('features').innerHTML=h;
}
function renderWaitlist(){
var wl=(D.waitlist||{}).waitlist||[];
var h='<div class="sec"><h2>📋 얼리 액세스 ('+wl.length+'명)</h2>';
if(wl.length){
h+='<table><tr><th>#</th><th>이메일</th><th>이름</th><th>관심</th><th>출처</th><th>등록일</th></tr>';
wl.forEach(function(w,i){
h+='<tr><td>'+(i+1)+'</td><td><b>'+esc(w.email)+'</b></td><td>'+esc(w.name||'-')+'</td><td>'+esc(w.interest||'-')+'</td><td>'+esc(w.source||'-')+'</td><td style="font-size:9px;">'+ts(w.created_at)+'</td></tr>';
});
h+='</table>';
}else h+='<div class="emp">없음</div>';
h+='</div>';
document.getElementById('waitlist').innerHTML=h;
}
/* ── 사용자 종합 프로파일 모달 ── */
async function showFullProfile(email){
try{
var p=await api('/api/admin/user-profile/'+encodeURIComponent(email));
var u=p.user||{};var pr=p.profile||{};
var h='<button class="modal-close" onclick="closeModal()"></button>';
h+='<h2 style="margin-bottom:4px;color:#1d1d1f;font-size:16px;">🎯 '+esc(u.email)+'</h2>';
h+='<p style="font-size:11px;color:#8e8e93;margin-bottom:16px;">'+esc(u.nickname||'')+' · '+esc(u.role||'user')+' · 방문 '+u.visit_count+'회 · 참여도 <b style="color:#5856D6;">'+pr.engagement_score+'/100</b></p>';
/* ── CTA 추천 ── */
h+='<div class="sec"><h2>🎯 맞춤 CTA 추천 (광고 타겟팅)</h2>';
var ctas=pr.cta_recommendations||[];
if(ctas.length){
ctas.forEach(function(c){
h+='<div class="cta-card"><div><div class="cta-label">'+esc(c.label)+'</div><div class="cta-reason">'+esc(c.reason)+' <span class="bg bg-s">'+esc(c.source||'')+'</span></div></div><div class="cta-prio">P'+c.priority+'</div></div>';
});
}else h+='<div class="emp">데이터 부족 — 더 많은 활동 후 추론 가능</div>';
h+='</div>';
/* ── 입력 행동 분석 (NEW) ── */
var ib=pr.input_behavior||{};
if(ib.total_inputs>0){
/* 행동 요약 */
h+='<div class="sec"><h2>🧠 행동 분석 요약</h2>';
var bs=ib.behavior_summary||[];
if(bs.length){bs.forEach(function(s){h+='<div style="padding:4px 0;font-size:11px;border-bottom:1px solid rgba(0,0,0,.04);">→ '+esc(s)+'</div>';});}
h+='<div style="margin-top:8px;display:flex;gap:12px;font-size:10px;color:#8e8e93;">분석된 입력 <b style="color:#5856D6;">'+ib.analyzed+'</b>건 / 전체 <b>'+ib.total_inputs+'</b></div>';
h+='</div>';
/* 의도 분석 */
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>🎯 입력 의도 분류</h2>';
var ir=ib.intent_ranking||[];
if(ir.length){
var mxI=ir[0].score;
ir.forEach(function(i){
h+='<div style="margin-bottom:5px;"><div style="display:flex;justify-content:space-between;font-size:10px;"><span>'+esc(i.emoji)+' '+esc(i.intent)+'</span><b style="color:#5856D6;">'+i.score+'</b></div><div class="bar"><div class="bf bf-p" style="width:'+Math.round(i.score/mxI*100)+'%"></div></div></div>';
});
}else h+='<div class="emp">미확인</div>';
h+='</div>';
/* 토픽 클러스터 */
h+='<div class="sec"><h2>📂 관심 주제 클러스터</h2>';
var tc=ib.topic_clusters||[];
if(tc.length){
tc.forEach(function(t){
h+='<div style="padding:5px 0;border-bottom:1px solid rgba(0,0,0,.04);"><div style="display:flex;justify-content:space-between;font-size:11px;"><b>'+esc(t.topic)+'</b><span class="bg bg-a">'+t.count+'건</span></div><div style="font-size:9px;color:#8e8e93;margin-top:2px;">최근: '+esc(t.recent)+'</div></div>';
});
}else h+='<div class="emp">미확인</div>';
h+='</div></div>';
/* 질문 복잡도 + 구매 신호 */
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>📊 질문 복잡도</h2>';
var cx=ib.complexity||{};
var cxTotal=(cx.simple||0)+(cx.medium||0)+(cx.complex||0)||1;
h+='<div style="display:flex;height:24px;border-radius:6px;overflow:hidden;margin-bottom:8px;">';
if(cx.simple)h+='<div style="width:'+(cx.simple/cxTotal*100)+'%;background:#4ade80;" title="단순 '+cx.simple+'건"></div>';
if(cx.medium)h+='<div style="width:'+(cx.medium/cxTotal*100)+'%;background:#fbbf24;" title="보통 '+cx.medium+'건"></div>';
if(cx.complex)h+='<div style="width:'+(cx.complex/cxTotal*100)+'%;background:#a78bfa;" title="복잡 '+cx.complex+'건"></div>';
h+='</div>';
h+='<div style="display:flex;gap:12px;font-size:10px;"><span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#4ade80;margin-right:3px;"></span>단순 '+cx.simple+'</span>';
h+='<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#fbbf24;margin-right:3px;"></span>보통 '+cx.medium+'</span>';
h+='<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#a78bfa;margin-right:3px;"></span>복잡 '+cx.complex+'</span></div>';
h+='</div>';
/* 구매/전환 신호 */
h+='<div class="sec"><h2>🔥 구매/전환 신호</h2>';
var as2=ib.action_signals||[];
if(as2.length){
as2.forEach(function(a){
h+='<div style="padding:4px 0;border-bottom:1px solid rgba(0,0,0,.04);font-size:10px;"><span class="bg bg-d" style="margin-right:4px;">'+esc(a.intent)+'</span>'+esc(a.text)+'</div>';
});
}else h+='<div style="font-size:10px;color:#c7c7cc;">전환 신호 미감지 — 아직 탐색 단계</div>';
h+='</div></div>';
/* 최근 입력 상세 분석 */
h+='<div class="sec"><h2>📝 최근 입력 분석 (의도 분류 포함)</h2>';
var ra=ib.recent_analysis||[];
if(ra.length){
h+='<table><tr><th>의도</th><th>복잡도</th><th>기능</th><th>입력</th><th>도메인</th></tr>';
ra.slice(0,15).forEach(function(r){
var cxCls=r.complexity==='complex'?'bg-a':r.complexity==='medium'?'bg-s':'bg-g';
h+='<tr><td><span class="bg bg-f">'+esc(r.intent_emoji)+' '+esc(r.intent)+'</span></td>';
h+='<td><span class="bg '+cxCls+'">'+esc(r.complexity)+'</span></td>';
h+='<td style="font-size:9px;">'+esc(r.feature)+'</td>';
h+='<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+esc(r.text)+'</td>';
h+='<td style="font-size:9px;">'+esc(r.url_domain)+'</td></tr>';
});
h+='</table>';
}else h+='<div class="emp">없음</div>';
h+='</div>';
}
/* ── 관심사 ── */
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>❤️ 관심 카테고리</h2>';
var ints=pr.interests||[];
if(ints.length){ints.forEach(function(i){h+='<span class="chip chip-i">'+esc(i.category)+' ('+i.score+')</span>';});}else h+='<span style="color:#c7c7cc;font-size:10px;">미확인</span>';
h+='</div>';
/* ── 사용자 유형 ── */
h+='<div class="sec"><h2>🧬 사용 성향</h2>';
var pers=pr.personas||[];
if(pers.length){pers.forEach(function(p){h+='<span class="chip chip-p">'+esc(p.type)+' ('+p.score+')</span>';});}else h+='<span style="color:#c7c7cc;font-size:10px;">미확인</span>';
h+='<div style="margin-top:8px;"><span class="chip chip-c">⏰ '+esc(pr.time_persona||'unknown')+'</span></div>';
h+='</div></div>';
/* ── 키워드 ── */
h+='<div class="sec"><h2>🔑 주요 키워드</h2>';
var kws=pr.keywords||[];
if(kws.length){kws.forEach(function(k){h+='<span class="chip chip-k">'+esc(k.word)+' ×'+k.count+'</span>';});}else h+='<span style="color:#c7c7cc;font-size:10px;">미확인</span>';
h+='</div>';
/* ── 시간대 분포 ── */
h+='<div class="sec"><h2>🕐 활동 시간대 (KST)</h2>';
var hd=pr.hour_distribution||[];
var maxH=Math.max.apply(null,hd)||1;
h+='<div style="display:flex;align-items:flex-end;height:60px;gap:1px;">';
for(var i=0;i<24;i++){
var barH=hd[i]?Math.max(2,Math.round(hd[i]/maxH*50)):1;
var isActive=hd[i]>maxH*0.5;
h+='<div class="hour-bar'+(isActive?' hour-active':'')+'" style="height:'+barH+'px;" title="'+i+'시: '+hd[i]+'건"></div>';
}
h+='</div><div style="display:flex;justify-content:space-between;font-size:8px;color:#c7c7cc;margin-top:2px;"><span>0시</span><span>6</span><span>12</span><span>18</span><span>23시</span></div>';
h+='</div>';
/* ── TOP 도메인 ── */
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
h+='<div class="sec"><h2>🌐 자주 방문</h2>';
var doms=p.domains||[];
if(doms.length){doms.slice(0,10).forEach(function(d){h+='<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:10px;border-bottom:1px solid rgba(0,0,0,.04);"><span>'+esc(d.domain)+'</span><b style="color:#5856D6;">'+d.cnt+'</b></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div>';
/* 기능 사용 */
h+='<div class="sec"><h2>⚡ 기능 사용</h2>';
var feats=p.features||[];
if(feats.length){feats.forEach(function(f){h+='<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:10px;border-bottom:1px solid rgba(0,0,0,.04);"><span>'+esc(f.feature)+'</span><b style="color:#0d9488;">'+f.cnt+'</b></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div></div>';
/* ── 최근 입력 ── */
h+='<div class="sec"><h2>📝 최근 입력</h2>';
var ri=p.recent_inputs||[];
if(ri.length){ri.slice(0,8).forEach(function(i){h+='<div style="padding:4px 0;border-bottom:1px solid rgba(0,0,0,.04);font-size:10px;"><span class="bg bg-f" style="margin-right:5px;">'+esc(i.feature)+'</span>'+esc((i.input_text||'').substring(0,80))+'<span style="margin-left:6px;color:#c7c7cc;font-size:8px;">'+ts(i.created_at)+'</span></div>';});}
else h+='<div class="emp">없음</div>';
h+='</div>';
/* ── 디바이스 ── */
h+='<div class="sec"><h2>📱 환경 정보</h2><div style="display:flex;flex-wrap:wrap;gap:6px;">';
if(u.last_browser) h+='<span class="chip chip-c">🌐 '+esc(u.last_browser)+'</span>';
if(u.last_os) h+='<span class="chip chip-c">💻 '+esc(u.last_os)+'</span>';
if(u.last_screen) h+='<span class="chip chip-c">📐 '+esc(u.last_screen)+'</span>';
if(u.last_language) h+='<span class="chip chip-c">🗣 '+esc(u.last_language)+'</span>';
if(u.last_timezone) h+='<span class="chip chip-c">🕐 '+esc(u.last_timezone)+'</span>';
if(u.last_provider) h+='<span class="chip chip-c">🔌 '+esc(u.last_provider)+'</span>';
if(u.last_client) h+='<span class="chip chip-c">📱 '+esc(u.last_client)+'</span>';
h+='</div></div>';
document.getElementById('modalWrap').innerHTML='<div class="modal" onclick="if(event.target===this)closeModal()"><div class="modal-body">'+h+'</div></div>';
}catch(e){alert('로드 실패: '+e.message);}
}
function closeModal(){document.getElementById('modalWrap').innerHTML='';}
async function delUser(email){
if(!confirm(email+' 삭제?'))return;
await fetch(API+'/api/admin/user/'+encodeURIComponent(email),{method:'DELETE',headers:{'X-Admin-Email':ADMIN}});
loadAll();
}
async function delRecord(table,id){
if(!confirm('#'+id+' 삭제?'))return;
await fetch(API+'/api/admin/record/'+table+'/'+id,{method:'DELETE',headers:{'X-Admin-Email':ADMIN}});
loadAll();
}
loadAll();
document.getElementById('adminDisplay').textContent=ADMIN||'(없음)';
setInterval(loadAll,30000);
</script>
</body>
</html>