Spaces:
Running
Running
Upload 2 files
Browse files- admin_dashboard.html +582 -0
- secure-pageagent.extend.js +0 -0
admin_dashboard.html
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 5 |
+
<title>SiteAgent Admin — Intelligence Dashboard</title>
|
| 6 |
+
<style>
|
| 7 |
+
*{margin:0;padding:0;box-sizing:border-box;}
|
| 8 |
+
body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro','Pretendard','Helvetica Neue',sans-serif;background:#f5f5f7;color:#1d1d1f;font-size:12px;}
|
| 9 |
+
::-webkit-scrollbar{width:5px;}::-webkit-scrollbar-thumb{background:rgba(88,86,214,.2);border-radius:4px;}
|
| 10 |
+
.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);}
|
| 11 |
+
.hdr h1{font-size:18px;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px;}
|
| 12 |
+
.hdr h1 span{color:#fff;}
|
| 13 |
+
.hdr p{font-size:10px;color:rgba(255,255,255,.7);margin-top:4px;}
|
| 14 |
+
.ct{max-width:1200px;margin:0 auto;padding:16px;}
|
| 15 |
+
.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);}
|
| 16 |
+
.tab{padding:7px 14px;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;color:#8e8e93;transition:all .15s;border:none;background:none;}
|
| 17 |
+
.tab:hover{color:#1d1d1f;background:rgba(0,0,0,.03);}
|
| 18 |
+
.tab.on{background:#5856D6;color:#fff;box-shadow:0 2px 12px rgba(88,86,214,.25);}
|
| 19 |
+
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px;}
|
| 20 |
+
.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;}
|
| 21 |
+
.sc:hover{border-color:rgba(88,86,214,.15);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.06);}
|
| 22 |
+
.sc .n{font-size:26px;font-weight:800;color:#5856D6;}
|
| 23 |
+
.sc .l{font-size:10px;color:#8e8e93;margin-top:2px;}
|
| 24 |
+
.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);}
|
| 25 |
+
.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;}
|
| 26 |
+
table{width:100%;border-collapse:collapse;font-size:11px;}
|
| 27 |
+
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);}
|
| 28 |
+
td{padding:6px 8px;border-bottom:1px solid rgba(0,0,0,.03);vertical-align:top;}
|
| 29 |
+
tr:hover{background:rgba(88,86,214,.02);}
|
| 30 |
+
.bg{display:inline-block;padding:2px 7px;border-radius:5px;font-size:9px;font-weight:600;}
|
| 31 |
+
.bg-a{background:rgba(88,86,214,.1);color:#5856D6;}.bg-u{background:rgba(0,0,0,.04);color:#666;}
|
| 32 |
+
.bg-f{background:rgba(13,148,136,.08);color:#0d9488;}.bg-s{background:rgba(234,179,8,.08);color:#a16207;}
|
| 33 |
+
.bg-d{background:rgba(239,68,68,.08);color:#dc2626;}.bg-g{background:rgba(34,197,94,.08);color:#16a34a;}
|
| 34 |
+
.db{padding:3px 8px;background:#ff3b30;color:#fff;border:none;border-radius:5px;font-size:9px;cursor:pointer;font-weight:600;transition:all .15s;}
|
| 35 |
+
.db:hover{background:#d63030;}
|
| 36 |
+
.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;}
|
| 37 |
+
.det-btn:hover{background:#4f46e5;}
|
| 38 |
+
.bar{height:6px;background:rgba(88,86,214,.06);border-radius:4px;margin-top:3px;}
|
| 39 |
+
.bf{height:100%;border-radius:4px;transition:width .3s;}
|
| 40 |
+
.bf-p{background:linear-gradient(90deg,#5856D6,#007AFF);}
|
| 41 |
+
.bf-t{background:linear-gradient(90deg,#0d9488,#059669);}
|
| 42 |
+
.bf-a{background:linear-gradient(90deg,#f59e0b,#ef4444);}
|
| 43 |
+
.emp{text-align:center;color:#c7c7cc;padding:20px;font-size:11px;}
|
| 44 |
+
.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);}
|
| 45 |
+
.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);}
|
| 46 |
+
.modal-close{float:right;background:none;border:none;font-size:20px;cursor:pointer;color:#8e8e93;}
|
| 47 |
+
.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;}
|
| 48 |
+
.chip{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:20px;font-size:10px;font-weight:600;margin:2px;}
|
| 49 |
+
.chip-i{background:rgba(88,86,214,.08);color:#5856D6;}
|
| 50 |
+
.chip-p{background:rgba(13,148,136,.08);color:#0d9488;}
|
| 51 |
+
.chip-k{background:rgba(234,179,8,.06);color:#a16207;}
|
| 52 |
+
.chip-c{background:rgba(59,130,246,.06);color:#2563eb;}
|
| 53 |
+
.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;}
|
| 54 |
+
.cta-card .cta-label{font-weight:700;font-size:12px;color:#5856D6;}
|
| 55 |
+
.cta-card .cta-reason{font-size:9px;color:#8e8e93;}
|
| 56 |
+
.cta-card .cta-prio{background:#5856D6;color:#fff;padding:2px 8px;border-radius:10px;font-size:9px;font-weight:800;}
|
| 57 |
+
.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;}
|
| 58 |
+
.hour-active{background:linear-gradient(180deg,#5856D6,#007AFF);}
|
| 59 |
+
.seg-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;}
|
| 60 |
+
.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);}
|
| 61 |
+
.seg-card .seg-n{font-size:32px;font-weight:800;color:#5856D6;}
|
| 62 |
+
.seg-card .seg-l{font-size:10px;color:#8e8e93;margin-top:4px;}
|
| 63 |
+
.pane{display:none;}.pane.active{display:block;}
|
| 64 |
+
</style>
|
| 65 |
+
</head>
|
| 66 |
+
<body>
|
| 67 |
+
<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>
|
| 68 |
+
<div class="ct">
|
| 69 |
+
<div class="tabs" id="tabBar">
|
| 70 |
+
<div class="tab on" data-t="overview">📊 개요</div>
|
| 71 |
+
<div class="tab" data-t="insights">🔥 인사이트</div>
|
| 72 |
+
<div class="tab" data-t="analytics">📈 분석</div>
|
| 73 |
+
<div class="tab" data-t="users">👥 사용자</div>
|
| 74 |
+
<div class="tab" data-t="segments">🎯 세그먼트</div>
|
| 75 |
+
<div class="tab" data-t="visits">🌐 방문</div>
|
| 76 |
+
<div class="tab" data-t="inputs">📝 입력</div>
|
| 77 |
+
<div class="tab" data-t="features">⚡ 기능</div>
|
| 78 |
+
<div class="tab" data-t="waitlist">📋 웨이팅</div>
|
| 79 |
+
</div>
|
| 80 |
+
<div id="overview" class="pane active"></div>
|
| 81 |
+
<div id="insights" class="pane"></div>
|
| 82 |
+
<div id="analytics" class="pane"></div>
|
| 83 |
+
<div id="users" class="pane"></div>
|
| 84 |
+
<div id="segments" class="pane"></div>
|
| 85 |
+
<div id="visits" class="pane"></div>
|
| 86 |
+
<div id="inputs" class="pane"></div>
|
| 87 |
+
<div id="features" class="pane"></div>
|
| 88 |
+
<div id="waitlist" class="pane"></div>
|
| 89 |
+
</div>
|
| 90 |
+
<div id="modalWrap"></div>
|
| 91 |
+
<button class="rfb" onclick="loadAll()">🔄 새로고침</button>
|
| 92 |
+
<script>
|
| 93 |
+
var API=location.origin;
|
| 94 |
+
var ADMIN=new URLSearchParams(location.search).get('key')||'';
|
| 95 |
+
var D={};
|
| 96 |
+
|
| 97 |
+
/* ── 탭 전환 ── */
|
| 98 |
+
document.getElementById('tabBar').addEventListener('click',function(e){
|
| 99 |
+
var tab=e.target.closest('.tab');if(!tab)return;
|
| 100 |
+
document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('on');});
|
| 101 |
+
tab.classList.add('on');
|
| 102 |
+
document.querySelectorAll('.pane').forEach(function(p){p.classList.remove('active');});
|
| 103 |
+
var t=tab.getAttribute('data-t');
|
| 104 |
+
document.getElementById(t).classList.add('active');
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
function ts(v){return v?new Date(v*1000).toLocaleString('ko-KR',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'—';}
|
| 108 |
+
function esc(s){var d=document.createElement('div');d.textContent=s||'';return d.innerHTML;}
|
| 109 |
+
function card(icon,num,label){return '<div class="sc"><div class="n">'+icon+' '+num+'</div><div class="l">'+label+'</div></div>';}
|
| 110 |
+
function barHTML(items,maxVal,cls){
|
| 111 |
+
var h='';if(!maxVal)maxVal=items[0]?.cnt||1;
|
| 112 |
+
items.forEach(function(v){
|
| 113 |
+
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>';
|
| 114 |
+
});
|
| 115 |
+
return h||'<div class="emp">데이터 없음</div>';
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
async function api(path){
|
| 119 |
+
var url=API+path+(path.includes('?')?'&':'?')+'admin='+encodeURIComponent(ADMIN);
|
| 120 |
+
console.log('[SA-Admin] fetch:',url);
|
| 121 |
+
var r=await fetch(url);
|
| 122 |
+
if(!r.ok){
|
| 123 |
+
var t=await r.text().catch(function(){return '';});
|
| 124 |
+
console.error('[SA-Admin] API error:',r.status,path,t);
|
| 125 |
+
throw new Error(r.status+' '+path);
|
| 126 |
+
}
|
| 127 |
+
var d=await r.json();
|
| 128 |
+
console.log('[SA-Admin] data:',path,JSON.stringify(d).substring(0,200));
|
| 129 |
+
return d;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async function loadAll(){
|
| 133 |
+
if(!ADMIN){
|
| 134 |
+
document.getElementById('overview').innerHTML='<div class="emp">❌ 관리자 키가 없습니다.<br>URL에 <b>?key=관리자이메일</b>을 추가하세요.</div>';
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
async function safeApi(path){
|
| 138 |
+
try{
|
| 139 |
+
var url=API+path+(path.includes('?')?'&':'?')+'admin='+encodeURIComponent(ADMIN);
|
| 140 |
+
var r=await fetch(url);
|
| 141 |
+
if(!r.ok) return null;
|
| 142 |
+
return await r.json();
|
| 143 |
+
}catch(e){return null;}
|
| 144 |
+
}
|
| 145 |
+
D.dash=await safeApi('/api/admin/dashboard')||{total_users:0,total_visits:0,total_inputs:0,top_domains:[],top_features:[],recent_users:[]};
|
| 146 |
+
D.inputs=await safeApi('/api/admin/inputs')||{inputs:[]};
|
| 147 |
+
D.allVisits=await safeApi('/api/admin/all-visits')||{visits:[]};
|
| 148 |
+
D.waitlist=await safeApi('/api/admin/waitlist')||{waitlist:[]};
|
| 149 |
+
D.analytics=await safeApi('/api/admin/analytics')||{};
|
| 150 |
+
D.segments=await safeApi('/api/admin/segments')||{};
|
| 151 |
+
D.insights={};
|
| 152 |
+
render();
|
| 153 |
+
document.getElementById('updateTime').textContent='Last: '+new Date().toLocaleTimeString('ko-KR');
|
| 154 |
+
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>';
|
| 155 |
+
safeApi('/api/admin/insights').then(function(d){
|
| 156 |
+
D.insights=d||{};
|
| 157 |
+
renderInsights();
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function render(){
|
| 162 |
+
renderOverview();
|
| 163 |
+
renderInsights();
|
| 164 |
+
renderAnalytics();
|
| 165 |
+
renderUsers();
|
| 166 |
+
renderSegments();
|
| 167 |
+
renderVisits();
|
| 168 |
+
renderInputs();
|
| 169 |
+
renderFeatures();
|
| 170 |
+
renderWaitlist();
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function renderOverview(){
|
| 174 |
+
var d=D.dash||{};var a=D.analytics||{};
|
| 175 |
+
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>';
|
| 176 |
+
/* AI 성능 */
|
| 177 |
+
var perf=a.ai_performance||{};
|
| 178 |
+
if(perf.cnt){
|
| 179 |
+
h+='<div class="stats">'+card('⚡',Math.round(perf.avg_ms||0)+'ms','평균 AI 응답')+card('✅',perf.cnt||0,'AI 호출 수')+card('❌',perf.errors||0,'AI 에러')+'</div>';
|
| 180 |
+
}
|
| 181 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 182 |
+
h+='<div class="sec"><h2>🌐 TOP 도메인</h2>';
|
| 183 |
+
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>';
|
| 184 |
+
h+='</div>';
|
| 185 |
+
h+='<div class="sec"><h2>⚡ 기능 사용</h2>';
|
| 186 |
+
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>';
|
| 187 |
+
h+='</div></div>';
|
| 188 |
+
document.getElementById('overview').innerHTML=h;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function renderInsights(){
|
| 192 |
+
var ins=D.insights||{};
|
| 193 |
+
if(!ins.total_analyzed){document.getElementById('insights').innerHTML='<div class="emp">아직 분석할 사용자 데이터가 없습니다. 사용자가 활동하면 자동으로 채워집니다.</div>';return;}
|
| 194 |
+
var h='<div class="stats">'+card('👥',ins.total_analyzed,'분석된 사용자')+card('💯',ins.avg_engagement,'평균 참여도')+card('🔥',ins.total_action_signals,'구매/전환 신호')+'</div>';
|
| 195 |
+
h+='<div class="sec"><h2>🎯 CTA 추천 랭킹 (전체 사용자 통합)</h2>';
|
| 196 |
+
var ctas=ins.cta_ranking||[];
|
| 197 |
+
if(ctas.length){
|
| 198 |
+
ctas.forEach(function(c){
|
| 199 |
+
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>';
|
| 200 |
+
});
|
| 201 |
+
}else h+='<div class="emp">CTA 추천 없음</div>';
|
| 202 |
+
h+='</div>';
|
| 203 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 204 |
+
h+='<div class="sec"><h2>🎯 입력 의도 분류 (전체)</h2>';
|
| 205 |
+
var ir=ins.intent_ranking||[];
|
| 206 |
+
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>';});}
|
| 207 |
+
else h+='<div class="emp">없음</div>';
|
| 208 |
+
h+='</div>';
|
| 209 |
+
h+='<div class="sec"><h2>📂 관심 주제 클러스터 (전체)</h2>';
|
| 210 |
+
var tc=ins.topic_ranking||[];
|
| 211 |
+
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>';});}
|
| 212 |
+
else h+='<div class="emp">없음</div>';
|
| 213 |
+
h+='</div></div>';
|
| 214 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 215 |
+
h+='<div class="sec"><h2>❤️ 관심 카테고리 (도메인 기반)</h2>';
|
| 216 |
+
var ic=ins.interest_ranking||[];
|
| 217 |
+
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>';});}
|
| 218 |
+
else h+='<div class="emp">없음</div>';
|
| 219 |
+
h+='</div>';
|
| 220 |
+
h+='<div class="sec"><h2>🧬 사용자 유형 (기능 기반)</h2>';
|
| 221 |
+
var pr=ins.persona_ranking||[];
|
| 222 |
+
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>';});}
|
| 223 |
+
else h+='<div class="emp">없음</div>';
|
| 224 |
+
h+='</div></div>';
|
| 225 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 226 |
+
h+='<div class="sec"><h2>📊 질문 복잡도 (전체)</h2>';
|
| 227 |
+
var cx=ins.complexity||{};
|
| 228 |
+
var cxT=(cx.simple||0)+(cx.medium||0)+(cx.complex||0)||1;
|
| 229 |
+
h+='<div style="display:flex;height:28px;border-radius:8px;overflow:hidden;margin-bottom:8px;">';
|
| 230 |
+
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>';
|
| 231 |
+
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>';
|
| 232 |
+
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>';
|
| 233 |
+
h+='</div></div>';
|
| 234 |
+
h+='<div class="sec"><h2>🔑 핵심 키워드 (전체 입력)</h2>';
|
| 235 |
+
var kw=ins.keyword_ranking||[];
|
| 236 |
+
if(kw.length){kw.forEach(function(k){h+='<span class="chip chip-k">'+esc(k.word)+' ×'+k.count+'</span>';});}
|
| 237 |
+
else h+='<div class="emp">없음</div>';
|
| 238 |
+
h+='</div></div>';
|
| 239 |
+
h+='<div class="sec"><h2>👤 사용자별 요약</h2>';
|
| 240 |
+
var up=ins.user_profiles||[];
|
| 241 |
+
if(up.length){
|
| 242 |
+
h+='<table><tr><th>이메일</th><th>참여도</th><th>TOP CTA</th><th>TOP 의도</th><th>전환신호</th><th>입력수</th><th>프로파일</th></tr>';
|
| 243 |
+
up.forEach(function(u){
|
| 244 |
+
var engColor=u.engagement>=70?'#22c55e':u.engagement>=40?'#f59e0b':'#8e8e93';
|
| 245 |
+
h+='<tr><td style="font-size:10px;">'+esc(u.email)+'</td>';
|
| 246 |
+
h+='<td><b style="color:'+engColor+';">'+u.engagement+'</b>/100</td>';
|
| 247 |
+
h+='<td style="font-size:9px;">'+esc(u.top_cta)+'</td>';
|
| 248 |
+
h+='<td><span class="bg bg-f">'+esc(u.top_intent)+'</span></td>';
|
| 249 |
+
h+='<td>'+(u.action_signals?'<span class="bg bg-d">🔥 '+u.action_signals+'</span>':'—')+'</td>';
|
| 250 |
+
h+='<td>'+u.input_count+'</td>';
|
| 251 |
+
h+='<td><button class="det-btn" onclick="showFullProfile(\''+esc(u.email)+'\')">상세</button></td></tr>';
|
| 252 |
+
});
|
| 253 |
+
h+='</table>';
|
| 254 |
+
}else h+='<div class="emp">없음</div>';
|
| 255 |
+
h+='</div>';
|
| 256 |
+
document.getElementById('insights').innerHTML=h;
|
| 257 |
+
}
|
| 258 |
+
function renderAnalytics(){
|
| 259 |
+
var a=D.analytics||{};
|
| 260 |
+
var h='<div class="stats">'+card('📈',a.today_events||0,'오늘')+card('📊',a.week_events||0,'이번주')+'</div>';
|
| 261 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 262 |
+
/* 이벤트 유형 */
|
| 263 |
+
h+='<div class="sec"><h2>📋 이벤트 유형 (7일)</h2>';
|
| 264 |
+
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>';
|
| 265 |
+
h+='</div>';
|
| 266 |
+
/* 기능별 */
|
| 267 |
+
h+='<div class="sec"><h2>⚡ 기능별 사용 (7일)</h2>';
|
| 268 |
+
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>';
|
| 269 |
+
h+='</div></div>';
|
| 270 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">';
|
| 271 |
+
/* 클라이언트 */
|
| 272 |
+
h+='<div class="sec"><h2>📱 클라이언트</h2>';
|
| 273 |
+
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>';
|
| 274 |
+
h+='</div>';
|
| 275 |
+
/* OS */
|
| 276 |
+
h+='<div class="sec"><h2>💻 OS</h2>';
|
| 277 |
+
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>';
|
| 278 |
+
h+='</div>';
|
| 279 |
+
/* 브라우저 */
|
| 280 |
+
h+='<div class="sec"><h2>🌐 브라우저</h2>';
|
| 281 |
+
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>';
|
| 282 |
+
h+='</div></div>';
|
| 283 |
+
/* 프로바이더 + MARL 엔진 */
|
| 284 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 285 |
+
h+='<div class="sec"><h2>🔌 프로바이더</h2>';
|
| 286 |
+
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>';
|
| 287 |
+
h+='</div>';
|
| 288 |
+
h+='<div class="sec"><h2>🧠 MARL 엔진</h2>';
|
| 289 |
+
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>';
|
| 290 |
+
h+='</div></div>';
|
| 291 |
+
document.getElementById('analytics').innerHTML=h;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
function renderUsers(){
|
| 295 |
+
var d=D.dash||{};
|
| 296 |
+
var h='<div class="sec"><h2>👥 전체 사용자 ('+((d.recent_users||[]).length)+'명)</h2>';
|
| 297 |
+
if(d.recent_users&&d.recent_users.length){
|
| 298 |
+
h+='<table><tr><th>이메일</th><th>닉네임</th><th>역할</th><th>방문</th><th>최근접속</th><th>프로파일</th><th>관리</th></tr>';
|
| 299 |
+
d.recent_users.forEach(function(u){
|
| 300 |
+
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>';
|
| 301 |
+
h+='<td><button class="det-btn" onclick="showFullProfile(\''+esc(u.email)+'\')">🎯 분석</button></td>';
|
| 302 |
+
h+='<td>'+(u.role!=='admin'?'<button class="db" onclick="delUser(\''+esc(u.email)+'\')">삭제</button>':'—')+'</td></tr>';
|
| 303 |
+
});
|
| 304 |
+
h+='</table>';
|
| 305 |
+
}else h+='<div class="emp">사용자 없음</div>';
|
| 306 |
+
h+='</div>';
|
| 307 |
+
document.getElementById('users').innerHTML=h;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
function renderSegments(){
|
| 311 |
+
var s=D.segments||{};
|
| 312 |
+
var h='<div class="seg-grid">';
|
| 313 |
+
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>';
|
| 314 |
+
h+='<div class="seg-card"><div class="seg-n">'+(s.casual_users_count||0)+'</div><div class="seg-l">일반 유저 (3~10방문)</div></div>';
|
| 315 |
+
h+='<div class="seg-card"><div class="seg-n">'+(s.new_users_count||0)+'</div><div class="seg-l">신규 유저 (1~3방문)</div></div>';
|
| 316 |
+
h+='</div>';
|
| 317 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px;">';
|
| 318 |
+
/* 클라이언트별 */
|
| 319 |
+
h+='<div class="sec"><h2>📱 클라이언트별 분포</h2>';
|
| 320 |
+
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;});
|
| 321 |
+
h+=barHTML(clArr,clArr[0]?.cnt,'bf-p');
|
| 322 |
+
h+='</div>';
|
| 323 |
+
/* 프로바이더별 */
|
| 324 |
+
h+='<div class="sec"><h2>🔌 프로바이더별 분포</h2>';
|
| 325 |
+
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;});
|
| 326 |
+
h+=barHTML(pvArr,pvArr[0]?.cnt,'bf-t');
|
| 327 |
+
h+='</div></div>';
|
| 328 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 329 |
+
/* OS별 */
|
| 330 |
+
h+='<div class="sec"><h2>💻 OS 분포</h2>';
|
| 331 |
+
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;});
|
| 332 |
+
h+=barHTML(osArr,osArr[0]?.cnt,'bf-a');
|
| 333 |
+
h+='</div>';
|
| 334 |
+
/* 브라우저별 */
|
| 335 |
+
h+='<div class="sec"><h2>🌐 브라우저 분포</h2>';
|
| 336 |
+
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;});
|
| 337 |
+
h+=barHTML(brArr,brArr[0]?.cnt,'bf-p');
|
| 338 |
+
h+='</div></div>';
|
| 339 |
+
document.getElementById('segments').innerHTML=h;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function renderVisits(){
|
| 343 |
+
var visits=(D.allVisits||{}).visits||[];
|
| 344 |
+
var h='<div class="sec"><h2>🌐 최근 방문 기록</h2>';
|
| 345 |
+
if(visits.length){
|
| 346 |
+
h+='<table><tr><th>이메일</th><th>도메인</th><th>URL</th><th>제목</th><th>횟수</th><th>최근</th><th></th></tr>';
|
| 347 |
+
visits.forEach(function(v){
|
| 348 |
+
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>';
|
| 349 |
+
h+='<td><button class="db" onclick="delRecord(\'page_visits\','+v.id+')">삭제</button></td></tr>';
|
| 350 |
+
});
|
| 351 |
+
h+='</table>';
|
| 352 |
+
}else h+='<div class="emp">없음</div>';
|
| 353 |
+
h+='</div>';
|
| 354 |
+
document.getElementById('visits').innerHTML=h;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
function renderInputs(){
|
| 358 |
+
var inp=(D.inputs||{}).inputs||[];
|
| 359 |
+
var h='<div class="sec"><h2>📝 최근 입력 로그</h2>';
|
| 360 |
+
if(inp.length){
|
| 361 |
+
h+='<table><tr><th>이메일</th><th>기능</th><th>URL</th><th>입력</th><th>시간</th><th></th></tr>';
|
| 362 |
+
inp.forEach(function(v){
|
| 363 |
+
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>';
|
| 364 |
+
h+='<td><button class="db" onclick="delRecord(\'user_inputs\','+v.id+')">삭제</button></td></tr>';
|
| 365 |
+
});
|
| 366 |
+
h+='</table>';
|
| 367 |
+
}else h+='<div class="emp">없음</div>';
|
| 368 |
+
h+='</div>';
|
| 369 |
+
document.getElementById('inputs').innerHTML=h;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
function renderFeatures(){
|
| 373 |
+
var d=D.dash||{};
|
| 374 |
+
var h='<div class="sec"><h2>⚡ 기능 사용 현황</h2>';
|
| 375 |
+
var tf=d.top_features||[];
|
| 376 |
+
if(tf.length){
|
| 377 |
+
h+='<table><tr><th>기능</th><th>횟수</th><th>비율</th></tr>';
|
| 378 |
+
var total=tf.reduce(function(s,v){return s+v.cnt;},0);
|
| 379 |
+
tf.forEach(function(v){
|
| 380 |
+
var pct=total?Math.round(v.cnt/total*100):0;
|
| 381 |
+
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>';
|
| 382 |
+
});
|
| 383 |
+
h+='</table>';
|
| 384 |
+
}else h+='<div class="emp">없음</div>';
|
| 385 |
+
h+='</div>';
|
| 386 |
+
document.getElementById('features').innerHTML=h;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
function renderWaitlist(){
|
| 390 |
+
var wl=(D.waitlist||{}).waitlist||[];
|
| 391 |
+
var h='<div class="sec"><h2>📋 얼리 액세스 ('+wl.length+'명)</h2>';
|
| 392 |
+
if(wl.length){
|
| 393 |
+
h+='<table><tr><th>#</th><th>이메일</th><th>이름</th><th>관심</th><th>출처</th><th>등록일</th></tr>';
|
| 394 |
+
wl.forEach(function(w,i){
|
| 395 |
+
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>';
|
| 396 |
+
});
|
| 397 |
+
h+='</table>';
|
| 398 |
+
}else h+='<div class="emp">없음</div>';
|
| 399 |
+
h+='</div>';
|
| 400 |
+
document.getElementById('waitlist').innerHTML=h;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
/* ── 사용자 종합 프로파일 모달 ── */
|
| 404 |
+
async function showFullProfile(email){
|
| 405 |
+
try{
|
| 406 |
+
var p=await api('/api/admin/user-profile/'+encodeURIComponent(email));
|
| 407 |
+
var u=p.user||{};var pr=p.profile||{};
|
| 408 |
+
var h='<button class="modal-close" onclick="closeModal()">✕</button>';
|
| 409 |
+
h+='<h2 style="margin-bottom:4px;color:#1d1d1f;font-size:16px;">🎯 '+esc(u.email)+'</h2>';
|
| 410 |
+
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>';
|
| 411 |
+
|
| 412 |
+
/* ── CTA 추천 ── */
|
| 413 |
+
h+='<div class="sec"><h2>🎯 맞춤 CTA 추천 (광고 타겟팅)</h2>';
|
| 414 |
+
var ctas=pr.cta_recommendations||[];
|
| 415 |
+
if(ctas.length){
|
| 416 |
+
ctas.forEach(function(c){
|
| 417 |
+
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>';
|
| 418 |
+
});
|
| 419 |
+
}else h+='<div class="emp">데이터 부족 — 더 많은 활동 후 추론 가능</div>';
|
| 420 |
+
h+='</div>';
|
| 421 |
+
|
| 422 |
+
/* ── 입력 행동 분석 (NEW) ── */
|
| 423 |
+
var ib=pr.input_behavior||{};
|
| 424 |
+
if(ib.total_inputs>0){
|
| 425 |
+
/* 행동 요약 */
|
| 426 |
+
h+='<div class="sec"><h2>🧠 행동 분석 요약</h2>';
|
| 427 |
+
var bs=ib.behavior_summary||[];
|
| 428 |
+
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>';});}
|
| 429 |
+
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>';
|
| 430 |
+
h+='</div>';
|
| 431 |
+
|
| 432 |
+
/* 의도 분석 */
|
| 433 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 434 |
+
h+='<div class="sec"><h2>🎯 입력 의도 분류</h2>';
|
| 435 |
+
var ir=ib.intent_ranking||[];
|
| 436 |
+
if(ir.length){
|
| 437 |
+
var mxI=ir[0].score;
|
| 438 |
+
ir.forEach(function(i){
|
| 439 |
+
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>';
|
| 440 |
+
});
|
| 441 |
+
}else h+='<div class="emp">미확인</div>';
|
| 442 |
+
h+='</div>';
|
| 443 |
+
|
| 444 |
+
/* 토픽 클러스터 */
|
| 445 |
+
h+='<div class="sec"><h2>📂 관심 주제 클러스터</h2>';
|
| 446 |
+
var tc=ib.topic_clusters||[];
|
| 447 |
+
if(tc.length){
|
| 448 |
+
tc.forEach(function(t){
|
| 449 |
+
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>';
|
| 450 |
+
});
|
| 451 |
+
}else h+='<div class="emp">미확인</div>';
|
| 452 |
+
h+='</div></div>';
|
| 453 |
+
|
| 454 |
+
/* 질문 복잡도 + 구매 신호 */
|
| 455 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 456 |
+
h+='<div class="sec"><h2>📊 질문 복잡도</h2>';
|
| 457 |
+
var cx=ib.complexity||{};
|
| 458 |
+
var cxTotal=(cx.simple||0)+(cx.medium||0)+(cx.complex||0)||1;
|
| 459 |
+
h+='<div style="display:flex;height:24px;border-radius:6px;overflow:hidden;margin-bottom:8px;">';
|
| 460 |
+
if(cx.simple)h+='<div style="width:'+(cx.simple/cxTotal*100)+'%;background:#4ade80;" title="단순 '+cx.simple+'건"></div>';
|
| 461 |
+
if(cx.medium)h+='<div style="width:'+(cx.medium/cxTotal*100)+'%;background:#fbbf24;" title="보통 '+cx.medium+'건"></div>';
|
| 462 |
+
if(cx.complex)h+='<div style="width:'+(cx.complex/cxTotal*100)+'%;background:#a78bfa;" title="복잡 '+cx.complex+'건"></div>';
|
| 463 |
+
h+='</div>';
|
| 464 |
+
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>';
|
| 465 |
+
h+='<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#fbbf24;margin-right:3px;"></span>보통 '+cx.medium+'</span>';
|
| 466 |
+
h+='<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#a78bfa;margin-right:3px;"></span>복잡 '+cx.complex+'</span></div>';
|
| 467 |
+
h+='</div>';
|
| 468 |
+
|
| 469 |
+
/* 구매/전환 신호 */
|
| 470 |
+
h+='<div class="sec"><h2>🔥 구매/전환 신호</h2>';
|
| 471 |
+
var as2=ib.action_signals||[];
|
| 472 |
+
if(as2.length){
|
| 473 |
+
as2.forEach(function(a){
|
| 474 |
+
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>';
|
| 475 |
+
});
|
| 476 |
+
}else h+='<div style="font-size:10px;color:#c7c7cc;">전환 신호 미감지 — 아직 탐색 단계</div>';
|
| 477 |
+
h+='</div></div>';
|
| 478 |
+
|
| 479 |
+
/* 최근 입력 상세 분석 */
|
| 480 |
+
h+='<div class="sec"><h2>📝 최근 입력 분석 (의도 분류 포함)</h2>';
|
| 481 |
+
var ra=ib.recent_analysis||[];
|
| 482 |
+
if(ra.length){
|
| 483 |
+
h+='<table><tr><th>의도</th><th>복잡도</th><th>기능</th><th>입력</th><th>도메인</th></tr>';
|
| 484 |
+
ra.slice(0,15).forEach(function(r){
|
| 485 |
+
var cxCls=r.complexity==='complex'?'bg-a':r.complexity==='medium'?'bg-s':'bg-g';
|
| 486 |
+
h+='<tr><td><span class="bg bg-f">'+esc(r.intent_emoji)+' '+esc(r.intent)+'</span></td>';
|
| 487 |
+
h+='<td><span class="bg '+cxCls+'">'+esc(r.complexity)+'</span></td>';
|
| 488 |
+
h+='<td style="font-size:9px;">'+esc(r.feature)+'</td>';
|
| 489 |
+
h+='<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+esc(r.text)+'</td>';
|
| 490 |
+
h+='<td style="font-size:9px;">'+esc(r.url_domain)+'</td></tr>';
|
| 491 |
+
});
|
| 492 |
+
h+='</table>';
|
| 493 |
+
}else h+='<div class="emp">없음</div>';
|
| 494 |
+
h+='</div>';
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
/* ── 관심사 ── */
|
| 498 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 499 |
+
h+='<div class="sec"><h2>❤️ 관심 카테고리</h2>';
|
| 500 |
+
var ints=pr.interests||[];
|
| 501 |
+
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>';
|
| 502 |
+
h+='</div>';
|
| 503 |
+
|
| 504 |
+
/* ── 사용자 유형 ── */
|
| 505 |
+
h+='<div class="sec"><h2>🧬 사용 성향</h2>';
|
| 506 |
+
var pers=pr.personas||[];
|
| 507 |
+
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>';
|
| 508 |
+
h+='<div style="margin-top:8px;"><span class="chip chip-c">⏰ '+esc(pr.time_persona||'unknown')+'</span></div>';
|
| 509 |
+
h+='</div></div>';
|
| 510 |
+
|
| 511 |
+
/* ── 키워드 ── */
|
| 512 |
+
h+='<div class="sec"><h2>🔑 주요 키워드</h2>';
|
| 513 |
+
var kws=pr.keywords||[];
|
| 514 |
+
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>';
|
| 515 |
+
h+='</div>';
|
| 516 |
+
|
| 517 |
+
/* ── 시간대 분포 ── */
|
| 518 |
+
h+='<div class="sec"><h2>🕐 활동 시간대 (KST)</h2>';
|
| 519 |
+
var hd=pr.hour_distribution||[];
|
| 520 |
+
var maxH=Math.max.apply(null,hd)||1;
|
| 521 |
+
h+='<div style="display:flex;align-items:flex-end;height:60px;gap:1px;">';
|
| 522 |
+
for(var i=0;i<24;i++){
|
| 523 |
+
var barH=hd[i]?Math.max(2,Math.round(hd[i]/maxH*50)):1;
|
| 524 |
+
var isActive=hd[i]>maxH*0.5;
|
| 525 |
+
h+='<div class="hour-bar'+(isActive?' hour-active':'')+'" style="height:'+barH+'px;" title="'+i+'시: '+hd[i]+'건"></div>';
|
| 526 |
+
}
|
| 527 |
+
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>';
|
| 528 |
+
h+='</div>';
|
| 529 |
+
|
| 530 |
+
/* ── TOP 도메인 ── */
|
| 531 |
+
h+='<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">';
|
| 532 |
+
h+='<div class="sec"><h2>🌐 자주 방문</h2>';
|
| 533 |
+
var doms=p.domains||[];
|
| 534 |
+
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>';});}
|
| 535 |
+
else h+='<div class="emp">없음</div>';
|
| 536 |
+
h+='</div>';
|
| 537 |
+
/* 기능 사용 */
|
| 538 |
+
h+='<div class="sec"><h2>⚡ 기능 사용</h2>';
|
| 539 |
+
var feats=p.features||[];
|
| 540 |
+
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>';});}
|
| 541 |
+
else h+='<div class="emp">없음</div>';
|
| 542 |
+
h+='</div></div>';
|
| 543 |
+
|
| 544 |
+
/* ── 최근 입력 ── */
|
| 545 |
+
h+='<div class="sec"><h2>📝 최근 입력</h2>';
|
| 546 |
+
var ri=p.recent_inputs||[];
|
| 547 |
+
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>';});}
|
| 548 |
+
else h+='<div class="emp">없음</div>';
|
| 549 |
+
h+='</div>';
|
| 550 |
+
|
| 551 |
+
/* ── 디바이스 ── */
|
| 552 |
+
h+='<div class="sec"><h2>📱 환경 정보</h2><div style="display:flex;flex-wrap:wrap;gap:6px;">';
|
| 553 |
+
if(u.last_browser) h+='<span class="chip chip-c">🌐 '+esc(u.last_browser)+'</span>';
|
| 554 |
+
if(u.last_os) h+='<span class="chip chip-c">💻 '+esc(u.last_os)+'</span>';
|
| 555 |
+
if(u.last_screen) h+='<span class="chip chip-c">📐 '+esc(u.last_screen)+'</span>';
|
| 556 |
+
if(u.last_language) h+='<span class="chip chip-c">🗣 '+esc(u.last_language)+'</span>';
|
| 557 |
+
if(u.last_timezone) h+='<span class="chip chip-c">🕐 '+esc(u.last_timezone)+'</span>';
|
| 558 |
+
if(u.last_provider) h+='<span class="chip chip-c">🔌 '+esc(u.last_provider)+'</span>';
|
| 559 |
+
if(u.last_client) h+='<span class="chip chip-c">📱 '+esc(u.last_client)+'</span>';
|
| 560 |
+
h+='</div></div>';
|
| 561 |
+
|
| 562 |
+
document.getElementById('modalWrap').innerHTML='<div class="modal" onclick="if(event.target===this)closeModal()"><div class="modal-body">'+h+'</div></div>';
|
| 563 |
+
}catch(e){alert('로드 실패: '+e.message);}
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
function closeModal(){document.getElementById('modalWrap').innerHTML='';}
|
| 567 |
+
async function delUser(email){
|
| 568 |
+
if(!confirm(email+' 삭제?'))return;
|
| 569 |
+
await fetch(API+'/api/admin/user/'+encodeURIComponent(email),{method:'DELETE',headers:{'X-Admin-Email':ADMIN}});
|
| 570 |
+
loadAll();
|
| 571 |
+
}
|
| 572 |
+
async function delRecord(table,id){
|
| 573 |
+
if(!confirm('#'+id+' 삭제?'))return;
|
| 574 |
+
await fetch(API+'/api/admin/record/'+table+'/'+id,{method:'DELETE',headers:{'X-Admin-Email':ADMIN}});
|
| 575 |
+
loadAll();
|
| 576 |
+
}
|
| 577 |
+
loadAll();
|
| 578 |
+
document.getElementById('adminDisplay').textContent=ADMIN||'(없음)';
|
| 579 |
+
setInterval(loadAll,30000);
|
| 580 |
+
</script>
|
| 581 |
+
</body>
|
| 582 |
+
</html>
|
secure-pageagent.extend.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|