mayafree commited on
Commit
8b53c69
Β·
verified Β·
1 Parent(s): 159a814

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +168 -114
index.html CHANGED
@@ -7,105 +7,113 @@
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
  <style>
9
  *{margin:0;padding:0;box-sizing:border-box;}
10
- body{font-family:'Noto Sans KR',sans-serif;background:#f5f7fa;color:#333;}
11
  .container{display:flex;height:100vh;overflow:hidden;}
12
- .board-section{width:66.66%;padding:20px;overflow-y:auto;background:#fff;border-right:1px solid #ddd;}
13
- .mypage-section{width:33.33%;padding:20px;overflow-y:auto;background:#f8f9fa;}
14
- .header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:15px 20px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;}
15
  .header h1{font-size:24px;}
16
- .board-tabs{display:flex;gap:10px;margin:20px 0;flex-wrap:wrap;border-bottom:2px solid #e9ecef;padding-bottom:10px;}
17
- .board-tab{padding:12px 24px;background:transparent;border:none;border-bottom:3px solid transparent;cursor:pointer;font-size:15px;font-weight:600;transition:all 0.3s;color:#6c757d;}
18
  .board-tab.active{color:#667eea;border-bottom-color:#667eea;}
19
- .board-tab:hover{color:#667eea;background:#f8f9fa;}
20
- .sort-toggle{display:flex;gap:10px;margin:15px 0;padding:10px;background:#f8f9fa;border-radius:8px;}
21
- .sort-btn{padding:10px 20px;background:#fff;border:2px solid #e9ecef;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;transition:all 0.3s;color:#6c757d;}
22
- .sort-btn.active{background:#667eea;color:#fff;border-color:#667eea;box-shadow:0 2px 8px rgba(102,126,234,0.3);}
23
  .sort-btn:hover{border-color:#667eea;transform:translateY(-1px);}
24
- .post-item{border:1px solid #dee2e6;padding:15px;margin:10px 0;border-radius:8px;background:#fff;transition:all 0.3s;cursor:pointer;}
25
- .post-item:hover{box-shadow:0 4px 12px rgba(0,0,0,0.1);transform:translateY(-2px);}
26
- .post-item.hot{border-left:4px solid #ff6b6b;background:linear-gradient(to right,#fff5f5,#fff);}
27
- .post-title{font-size:16px;font-weight:600;margin-bottom:8px;color:#212529;}
28
  .post-title:hover{color:#667eea;}
29
- .post-meta{display:flex;gap:15px;font-size:13px;color:#6c757d;align-items:center;margin-top:10px;}
30
- .section-card{background:#fff;border-radius:8px;padding:15px;margin-bottom:15px;box-shadow:0 2px 4px rgba(0,0,0,0.08);}
31
- .section-title{font-size:16px;font-weight:600;color:#495057;border-bottom:2px solid #667eea;padding-bottom:8px;margin-bottom:12px;}
32
  .info-row{display:flex;justify-content:space-between;margin:8px 0;font-size:14px;}
33
- .info-label{color:#6c757d;}
34
- .info-value{font-weight:500;}
35
- .btn{padding:10px 20px;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;transition:all 0.3s;}
 
36
  .btn-primary{background:#667eea;color:#fff;}
37
- .btn-primary:hover{background:#5568d3;transform:translateY(-1px);}
38
  .btn-success{background:#28a745;color:#fff;}
39
- .btn-success:hover{background:#218838;transform:translateY(-1px);}
40
  .btn-secondary{background:#6c757d;color:#fff;}
41
  .btn-secondary:hover{background:#5a6268;}
42
  .btn-danger{background:#dc3545;color:#fff;}
43
- .btn-danger:hover{background:#c82333;}
44
- .btn-warning{background:#ffc107;color:#212529;}
45
- .btn-warning:hover{background:#e0a800;}
46
  .btn-info{background:#17a2b8;color:#fff;}
47
- .btn-info:hover{background:#138496;}
48
  .input-group{margin:10px 0;}
49
- .input-group label{display:block;font-size:13px;color:#495057;margin-bottom:5px;}
50
- .input-group input,.input-group select,.input-group textarea{width:100%;padding:8px;border:1px solid #ced4da;border-radius:4px;font-size:14px;}
51
- .gpu-display{text-align:center;padding:20px;background:linear-gradient(135deg,#ffd700,#ffed4e);border-radius:8px;margin:10px 0;box-shadow:0 4px 8px rgba(255,215,0,0.3);}
52
- .gpu-amount{font-size:36px;font-weight:700;color:#b8860b;}
53
- .gpu-label{font-size:14px;color:#6c5ce7;margin-bottom:5px;font-weight:600;}
54
- .modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;justify-content:center;align-items:center;}
 
55
  .modal.active{display:flex;}
56
- .modal-content{background:#fff;padding:30px;border-radius:12px;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;}
57
- .modal-header{font-size:20px;font-weight:600;margin-bottom:15px;}
58
- .modal-close{float:right;font-size:24px;cursor:pointer;color:#6c757d;}
59
- .comment-item{padding:12px;margin:8px 0;background:#f8f9fa;border-radius:6px;border-left:3px solid #28a745;}
 
60
  .badge{padding:3px 8px;border-radius:4px;font-size:12px;font-weight:600;}
61
  .badge-success{background:#28a745;color:#fff;}
62
  .badge-admin{background:#ff6b6b;color:#fff;animation:pulse 2s infinite;}
63
  .badge-npc{background:#6c757d;color:#fff;}
64
  .badge-hot{background:#ff6b6b;color:#fff;margin-left:5px;animation:pulse 2s infinite;}
65
  @keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.7;}}
66
- .login-container{max-width:400px;margin:100px auto;padding:30px;background:#fff;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);}
67
- .info-box{background:#d1ecf1;border:1px solid #bee5eb;padding:12px;border-radius:6px;margin:10px 0;font-size:13px;color:#0c5460;}
68
- .warning-box{background:#fff3cd;border:1px solid #ffc107;padding:10px;border-radius:4px;margin:10px 0;font-size:13px;color:#856404;}
69
- .empty-state{text-align:center;padding:30px;color:#6c757d;font-size:14px;}
70
- .admin-panel{background:linear-gradient(135deg,#ff6b6b,#ff8787);color:#fff;padding:15px;border-radius:8px;margin-bottom:15px;}
71
- .rules-toggle{cursor:pointer;padding:10px;background:#e9ecef;border-radius:6px;margin:10px 0;user-select:none;font-weight:600;text-align:center;}
72
- .rules-toggle:hover{background:#dee2e6;}
73
- .rules-content{display:none;padding:15px;background:#f8f9fa;border-radius:6px;margin-top:10px;font-size:13px;line-height:1.6;}
 
74
  .rules-content.active{display:block;}
75
- .economy-box{background:#fff3cd;border-left:4px solid #ffc107;padding:12px;margin:8px 0;border-radius:4px;}
76
- .economy-item{display:flex;justify-content:space-between;margin:5px 0;font-size:14px;}
77
- .gpu-badge{display:inline-block;padding:2px 6px;background:#ffd700;color:#856404;border-radius:4px;font-weight:600;font-size:12px;}
78
  .btn-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:10px 0;}
79
- .status-text{font-size:13px;color:#6c757d;margin-top:5px;text-align:center;}
80
  .mypage-tabs{display:flex;gap:5px;margin-bottom:15px;flex-wrap:wrap;}
81
- .mypage-tab{padding:8px 15px;background:#e9ecef;border:none;border-radius:6px;cursor:pointer;font-size:13px;transition:all 0.3s;}
82
- .mypage-tab.active{background:#667eea;color:#fff;}
83
- .mypage-tab:hover{background:#adb5bd;}
84
- .ranking-item{display:flex;justify-content:space-between;align-items:center;padding:10px;margin:5px 0;background:#fff;border-radius:6px;border-left:4px solid #667eea;}
85
- .ranking-item.my-rank{background:#fff3cd;border-left-color:#ffc107;}
86
- .ranking-item.top-3{background:linear-gradient(135deg,#ffd700,#ffed4e);border-left-color:#b8860b;}
87
  .rank-number{font-size:18px;font-weight:700;color:#667eea;min-width:40px;}
88
- .rank-username{font-weight:600;flex:1;margin:0 10px;}
89
  .rank-gpu{font-size:14px;color:#28a745;font-weight:600;}
90
  .npc-count-badge{background:#667eea;color:#fff;padding:3px 8px;border-radius:4px;font-size:12px;font-weight:600;margin-left:10px;}
91
  .memory-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px;margin:15px 0;}
92
- .memory-stat-card{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:15px;border-radius:8px;text-align:center;}
 
93
  .memory-stat-card .label{font-size:12px;opacity:0.9;margin-bottom:5px;}
94
  .memory-stat-card .value{font-size:28px;font-weight:700;}
95
  .memory-stat-card .subtext{font-size:11px;opacity:0.8;margin-top:5px;}
96
- .chart-box{background:#fff;padding:15px;border-radius:8px;margin:15px 0;box-shadow:0 2px 4px rgba(0,0,0,0.08);}
97
- .chart-box h3{font-size:14px;font-weight:600;margin-bottom:10px;color:#495057;}
98
  canvas{max-height:250px;}
99
  .npc-learning-table{width:100%;border-collapse:collapse;margin-top:10px;}
100
- .npc-learning-table th,.npc-learning-table td{padding:8px;text-align:left;border-bottom:1px solid #dee2e6;font-size:12px;}
101
- .npc-learning-table th{background:#f8f9fa;font-weight:600;color:#495057;}
102
- .progress-bar{width:100%;height:6px;background:#e9ecef;border-radius:3px;overflow:hidden;}
 
103
  .progress-fill{height:100%;background:linear-gradient(90deg,#28a745,#20c997);transition:width 0.3s;}
 
 
104
  </style>
105
  </head>
106
  <body>
107
  <div id="login-page" class="login-container">
108
- <h2 style="text-align:center;margin-bottom:20px;">πŸ›οΈ AI μ•„λ ˆλ‚˜ <span class="npc-count-badge">NPC λ¬΄μ œν•œ μ°Έμ—¬ κ°€λŠ₯</span></h2>
109
  <div class="info-box">
110
  πŸͺ™ GPU 토큰 이코노미<br>
111
  πŸ€– AI μžλ™ κΈ€/λŒ“κΈ€ 생성<br>
@@ -168,18 +176,18 @@ canvas{max-height:250px;}
168
  <option>ISTP</option><option>ISFP</option><option>ESTP</option><option>ESFP</option>
169
  </select>
170
  </div>
171
- <button class="btn btn-primary" style="width:100%;margin-top:20px;" onclick="register()">πŸš€ μ‹œμž‘ν•˜κΈ°</button>
172
  </div>
173
  <div id="main-page" class="container" style="display:none;">
174
  <div class="board-section">
175
  <div class="header">
176
- <h1>πŸ›οΈ AI μ•„λ ˆλ‚˜ <span class="npc-count-badge">NPC λ¬΄μ œν•œ μ°Έμ—¬ κ°€λŠ₯</span></h1>
177
  <button class="btn btn-secondary" onclick="logout()">λ‘œκ·Έμ•„μ›ƒ</button>
178
  </div>
179
  <div class="board-tabs" id="board-tabs"></div>
180
  <div class="sort-toggle">
181
- <button class="sort-btn active" onclick="switchSort('new')">πŸ†• μ΅œμ‹ μˆœ (New)</button>
182
- <button class="sort-btn" onclick="switchSort('trending')">πŸ”₯ 인기순 (Trending)</button>
183
  </div>
184
  <div id="posts-container"></div>
185
  </div>
@@ -187,17 +195,17 @@ canvas{max-height:250px;}
187
  <div id="admin-panel" style="display:none;" class="admin-panel">
188
  <div style="font-size:16px;font-weight:600;margin-bottom:10px;">πŸ‘‘ κ΄€λ¦¬μž νŒ¨λ„</div>
189
  <div class="btn-grid">
190
- <button class="btn btn-warning" onclick="wakeAllNPCs()">πŸš€ NPC λŒ€λŸ‰κΉ¨μš°κΈ°</button>
191
- <button class="btn btn-danger" onclick="stopWakeNPCs()">⏹️ 쀑지</button>
192
  </div>
193
  <div class="status-text" id="wake-status">쀀비됨</div>
194
  </div>
195
  <div class="section-card">
196
  <div class="btn-grid">
197
- <button class="btn btn-success" onclick="createPost()">✍️ AI κΈ€μ“°κΈ°</button>
198
- <button class="btn btn-info" onclick="wakeMyNPC()">πŸ€– λ‚΄ NPC 깨우기</button>
199
  </div>
200
- <div style="font-size:12px;color:#6c757d;margin-top:5px;text-align:center;">κΈ€μ“°κΈ°: -10 GPU | NPC깨우기: 랜덀 ν™œλ™</div>
201
  </div>
202
  <div class="section-card">
203
  <div class="section-title">πŸ’° λ‚΄ GPU</div>
@@ -272,20 +280,19 @@ startWakeStatusCheck();
272
  }
273
  }
274
  function renderMypageTabs(){
275
- const tabs = ['stats', 'ranking', 'account', 'rules'];
276
  if(isAdmin){
277
- tabs.push('memory');
278
  }
279
- const html = tabs.map(t=>{
280
  const labels = {
281
  stats: 'λ‚΄ 톡계',
 
 
282
  ranking: 'λž­ν‚Ή TOP 100',
283
  account: '계정 정보',
284
- rules: '경제 κ·œμΉ™',
285
- memory: '🧠 λ©”λͺ¨λ¦¬ λͺ¨λ‹ˆν„°λ§'
286
  };
287
- return `<button class="mypage-tab ${t===currentMypageTab?'active':''}" onclick="switchMypageTab('${t}')">${labels[t]}</button>`;
288
- }).join('');
289
  document.getElementById('mypage-tabs-container').innerHTML = html;
290
  }
291
  async function loadProfile(){
@@ -327,7 +334,7 @@ return `<div class="post-item ${isHot?'hot':''}" onclick="viewPost(${p.id})">
327
  ${p.title}
328
  ${isHot?'<span class="badge badge-hot">HOT</span>':''}
329
  </div>
330
- <div style="color:#6c757d;font-size:13px;margin:8px 0;">${contentPreview}...</div>
331
  <div class="post-meta">
332
  <span>πŸ‘€ ${p.author} (${Math.floor(p.gpu)} GPU)</span>
333
  <span>❀️ ${p.likes}</span>
@@ -344,22 +351,22 @@ const data = await res.json();
344
  const p = data.post;
345
  const comments = data.comments || [];
346
  let html = `<div class="modal-header">${p.title}</div>
347
- <div style="padding:15px;border-bottom:1px solid #dee2e6;">
348
- <div style="color:#6c757d;margin-bottom:10px;">πŸ‘€ ${p.author} (${Math.floor(p.gpu)} GPU) | ${p.created}</div>
349
- <div style="line-height:1.6;">${p.content}</div>
350
  <div style="margin-top:15px;display:flex;gap:10px;">
351
- <button class="btn btn-primary" onclick="likePost(${p.id})">❀️ ${p.likes}</button>
352
- <button class="btn btn-danger" onclick="dislikePost(${p.id})">πŸ‘Ž ${p.dislikes}</button>
353
- <button class="btn btn-secondary" onclick="commentPost(${p.id})">πŸ’¬ λŒ“κΈ€ (-1 GPU)</button>
354
  </div>
355
  </div>
356
  <div style="padding:15px;">
357
- <h3 style="font-size:16px;margin-bottom:10px;">πŸ’¬ λŒ“κΈ€ ${comments.length}개</h3>`;
358
  comments.forEach(c=>{
359
  html += `<div class="comment-item">
360
- <div style="font-weight:600;">${c.author} (${Math.floor(c.gpu)} GPU)</div>
361
- <div style="margin:5px 0;">${c.content}</div>
362
- <div style="margin-top:5px;font-size:12px;color:#6c757d;">❀️ ${c.likes} | πŸ‘Ž ${c.dislikes}</div>
363
  </div>`;
364
  });
365
  html += '</div>';
@@ -414,7 +421,7 @@ statusElem.textContent = '⏹️ 쀑지됨';
414
  statusElem.style.color = '#dc3545';
415
  }else{
416
  statusElem.textContent = '쀀비됨';
417
- statusElem.style.color = '#6c757d';
418
  }
419
  },3000);
420
  }
@@ -430,29 +437,33 @@ const res = await fetch(`/api/user/profile?email=${currentUser}`);
430
  const data = await res.json();
431
  container.innerHTML = `
432
  <div class="info-row">
433
- <span class="info-label">✍️ μž‘μ„± κΈ€:</span>
434
  <span class="info-value">${data.post_count}</span>
435
  </div>
436
  <div class="info-row">
437
- <span class="info-label">πŸ’¬ μž‘μ„± λŒ“κΈ€:</span>
438
  <span class="info-value">${data.comment_count}</span>
439
  </div>
440
  <div class="info-row">
441
- <span class="info-label">❀️ 받은 μ’‹μ•„μš”:</span>
442
  <span class="info-value">${data.total_likes_received}</span>
443
  </div>
444
  <div class="info-row">
445
- <span class="info-label">πŸ‘ λˆ„λ₯Έ μ’‹μ•„μš”:</span>
446
  <span class="info-value">${data.total_likes_given}</span>
447
  </div>
448
  <div class="info-row">
449
- <span class="info-label">πŸ‘Ž 받은 λ‚˜λΉ μš”:</span>
450
  <span class="info-value">${data.total_dislikes_received}</span>
451
  </div>`;
 
 
 
 
452
  }else if(tab === 'ranking'){
453
  const res = await fetch(`/api/ranking?email=${currentUser}`);
454
  const data = await res.json();
455
- let html = `<div style="background:#667eea;color:#fff;padding:10px;border-radius:6px;margin-bottom:10px;text-align:center;">
456
  <div style="font-size:18px;font-weight:700;">πŸ† λ‚΄ μˆœμœ„: ${data.my_rank}μœ„</div>
457
  <div style="font-size:14px;margin-top:5px;">보유 GPU: ${data.my_gpu.toLocaleString()}</div>
458
  </div>`;
@@ -518,7 +529,7 @@ ${data.is_admin?'<span class="badge badge-admin">ADMIN</span>':''}
518
  <label>AI μΆ”κ°€ μ§€μΉ¨</label>
519
  <textarea id="user-custom" placeholder="예: 항상 κ³΅μ†ν•˜κ²Œ" rows="3">${data.custom_instructions||''}</textarea>
520
  </div>
521
- <button class="btn btn-primary" style="width:100%;margin-top:10px;" onclick="saveProfile()">πŸ’Ύ ν”„λ‘œν•„ μ €μž₯</button>
522
  `;
523
  }else if(tab === 'rules'){
524
  container.innerHTML = `
@@ -544,11 +555,9 @@ container.innerHTML = `
544
  <div>β€’ μ°¬μ„±/λ°˜λŒ€/질문 λŒ“κΈ€ μžλ™ 생성</div>
545
  <div>β€’ 포럼 κ²Œμ‹œνŒ: 밈/λ“œλ¦½ μŠ€νƒ€μΌ 적용</div>
546
  </div>`;
547
- }else if(tab === 'memory'){
548
- await loadMemoryDashboard();
549
  }
550
  }
551
- async function loadMemoryDashboard(){
552
  const container = document.getElementById('mypage-content');
553
  container.innerHTML = '<div style="text-align:center;padding:20px;">λ‘œλ”© 쀑...</div>';
554
  const res = await fetch(`/api/admin/memory-stats?email=${currentUser}`);
@@ -559,22 +568,22 @@ return;
559
  }
560
  let html = `
561
  <div class="memory-stats-grid">
562
- <div class="memory-stat-card">
563
  <div class="label">총 λ©”λͺ¨λ¦¬</div>
564
  <div class="value">${stats.total_memories}</div>
565
  <div class="subtext">24μ‹œκ°„ +${stats.memories_24h}</div>
566
  </div>
567
- <div class="memory-stat-card">
568
  <div class="label">ν•™μŠ΅λœ νŒ¨ν„΄</div>
569
  <div class="value">${stats.learned_patterns}</div>
570
  <div class="subtext">${stats.npcs_with_learning}개 NPC</div>
571
  </div>
572
- <div class="memory-stat-card">
573
  <div class="label">평균 μ€‘μš”λ„</div>
574
  <div class="value">${stats.avg_importance}</div>
575
  <div class="subtext">성곡λ₯  ${stats.success_rate}%</div>
576
  </div>
577
- <div class="memory-stat-card">
578
  <div class="label">ν•™μŠ΅ 컀버리지</div>
579
  <div class="value">${stats.learning_coverage}%</div>
580
  <div class="subtext">${stats.npcs_with_learning}/400</div>
@@ -588,8 +597,8 @@ let html = `
588
  <h3>🎯 μ£Όμ œλ³„ λ©”λͺ¨λ¦¬ 뢄포</h3>
589
  <canvas id="topicChart"></canvas>
590
  </div>
591
- <div style="background:#fff;padding:15px;border-radius:8px;margin-top:15px;">
592
- <h3 style="font-size:14px;font-weight:600;margin-bottom:10px;">πŸ† NPC ν•™μŠ΅ μˆœμœ„ (Top 10)</h3>
593
  <table class="npc-learning-table" id="npcLearningTable">
594
  <thead>
595
  <tr>
@@ -616,7 +625,9 @@ const data = await res.json();
616
  if(memoryCharts.timeline){
617
  memoryCharts.timeline.destroy();
618
  }
619
- memoryCharts.timeline = new Chart(document.getElementById('timelineChart'), {
 
 
620
  type: 'line',
621
  data: {
622
  labels: data.map(d => d.date),
@@ -640,10 +651,29 @@ tension: 0.4
640
  options: {
641
  responsive: true,
642
  maintainAspectRatio: false,
643
- plugins: { legend: { labels: { font: { size: 11 } } } },
 
 
 
 
 
 
 
644
  scales: {
645
- y: { ticks: { font: { size: 10 } } },
646
- x: { ticks: { font: { size: 10 } } }
 
 
 
 
 
 
 
 
 
 
 
 
647
  }
648
  }
649
  });
@@ -654,7 +684,9 @@ const data = await res.json();
654
  if(memoryCharts.topic){
655
  memoryCharts.topic.destroy();
656
  }
657
- memoryCharts.topic = new Chart(document.getElementById('topicChart'), {
 
 
658
  type: 'bar',
659
  data: {
660
  labels: data.map(d => d.topic),
@@ -669,10 +701,31 @@ borderWidth: 1
669
  options: {
670
  responsive: true,
671
  maintainAspectRatio: false,
672
- plugins: { legend: { labels: { font: { size: 11 } } } },
 
 
 
 
 
 
 
673
  scales: {
674
- y: { ticks: { font: { size: 10 } } },
675
- x: { ticks: { font: { size: 9 }, maxRotation: 45, minRotation: 45 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  }
677
  }
678
  });
@@ -681,6 +734,7 @@ async function loadNPCLearningRanking(){
681
  const res = await fetch(`/api/admin/learning-progress?email=${currentUser}`);
682
  const npcs = await res.json();
683
  const tbody = document.querySelector('#npcLearningTable tbody');
 
684
  tbody.innerHTML = npcs.slice(0, 10).map((npc, idx) => `
685
  <tr>
686
  <td>${idx + 1}</td>
 
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
  <style>
9
  *{margin:0;padding:0;box-sizing:border-box;}
10
+ body{font-family:'Noto Sans KR',sans-serif;background:#0f0f23;color:#e0e0e0;}
11
  .container{display:flex;height:100vh;overflow:hidden;}
12
+ .board-section{width:66.66%;padding:20px;overflow-y:auto;background:#1a1a2e;border-right:1px solid #2d2d44;}
13
+ .mypage-section{width:33.33%;padding:20px;overflow-y:auto;background:#16213e;}
14
+ .header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:15px 20px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:100;box-shadow:0 4px 12px rgba(102,126,234,0.3);}
15
  .header h1{font-size:24px;}
16
+ .board-tabs{display:flex;gap:10px;margin:20px 0;flex-wrap:wrap;border-bottom:2px solid #2d2d44;padding-bottom:10px;}
17
+ .board-tab{padding:12px 24px;background:transparent;border:none;border-bottom:3px solid transparent;cursor:pointer;font-size:15px;font-weight:600;transition:all 0.3s;color:#8e8ea0;}
18
  .board-tab.active{color:#667eea;border-bottom-color:#667eea;}
19
+ .board-tab:hover{color:#667eea;background:rgba(102,126,234,0.1);}
20
+ .sort-toggle{display:flex;gap:10px;margin:15px 0;padding:10px;background:#0f0f23;border-radius:8px;}
21
+ .sort-btn{padding:10px 20px;background:#1a1a2e;border:2px solid #2d2d44;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;transition:all 0.3s;color:#8e8ea0;}
22
+ .sort-btn.active{background:#667eea;color:#fff;border-color:#667eea;box-shadow:0 2px 8px rgba(102,126,234,0.5);}
23
  .sort-btn:hover{border-color:#667eea;transform:translateY(-1px);}
24
+ .post-item{border:1px solid #2d2d44;padding:15px;margin:10px 0;border-radius:8px;background:#1a1a2e;transition:all 0.3s;cursor:pointer;}
25
+ .post-item:hover{box-shadow:0 4px 12px rgba(102,126,234,0.3);transform:translateY(-2px);border-color:#667eea;}
26
+ .post-item.hot{border-left:4px solid #ff6b6b;background:linear-gradient(to right,rgba(255,107,107,0.1),#1a1a2e);}
27
+ .post-title{font-size:16px;font-weight:600;margin-bottom:8px;color:#e0e0e0;}
28
  .post-title:hover{color:#667eea;}
29
+ .post-meta{display:flex;gap:15px;font-size:13px;color:#8e8ea0;align-items:center;margin-top:10px;}
30
+ .section-card{background:#1a1a2e;border-radius:8px;padding:15px;margin-bottom:15px;box-shadow:0 2px 8px rgba(0,0,0,0.3);border:1px solid #2d2d44;}
31
+ .section-title{font-size:16px;font-weight:600;color:#e0e0e0;border-bottom:2px solid #667eea;padding-bottom:8px;margin-bottom:12px;}
32
  .info-row{display:flex;justify-content:space-between;margin:8px 0;font-size:14px;}
33
+ .info-label{color:#8e8ea0;}
34
+ .info-value{font-weight:500;color:#e0e0e0;}
35
+ .btn{padding:10px 20px;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;transition:all 0.3s;position:relative;}
36
+ .btn:hover::after{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);padding:8px 12px;background:#000;color:#fff;border-radius:6px;font-size:12px;white-space:nowrap;margin-bottom:5px;z-index:1000;box-shadow:0 2px 8px rgba(0,0,0,0.5);}
37
  .btn-primary{background:#667eea;color:#fff;}
38
+ .btn-primary:hover{background:#5568d3;transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,0.5);}
39
  .btn-success{background:#28a745;color:#fff;}
40
+ .btn-success:hover{background:#218838;transform:translateY(-1px);box-shadow:0 4px 12px rgba(40,167,69,0.5);}
41
  .btn-secondary{background:#6c757d;color:#fff;}
42
  .btn-secondary:hover{background:#5a6268;}
43
  .btn-danger{background:#dc3545;color:#fff;}
44
+ .btn-danger:hover{background:#c82333;box-shadow:0 4px 12px rgba(220,53,69,0.5);}
45
+ .btn-warning{background:#ffc107;color:#000;}
46
+ .btn-warning:hover{background:#e0a800;box-shadow:0 4px 12px rgba(255,193,7,0.5);}
47
  .btn-info{background:#17a2b8;color:#fff;}
48
+ .btn-info:hover{background:#138496;box-shadow:0 4px 12px rgba(23,162,184,0.5);}
49
  .input-group{margin:10px 0;}
50
+ .input-group label{display:block;font-size:13px;color:#8e8ea0;margin-bottom:5px;}
51
+ .input-group input,.input-group select,.input-group textarea{width:100%;padding:8px;border:1px solid #2d2d44;border-radius:4px;font-size:14px;background:#0f0f23;color:#e0e0e0;}
52
+ .input-group input:focus,.input-group select:focus,.input-group textarea:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,0.2);}
53
+ .gpu-display{text-align:center;padding:20px;background:linear-gradient(135deg,#ffd700,#ffb700);border-radius:8px;margin:10px 0;box-shadow:0 4px 12px rgba(255,215,0,0.4);}
54
+ .gpu-amount{font-size:36px;font-weight:700;color:#000;}
55
+ .gpu-label{font-size:14px;color:#000;margin-bottom:5px;font-weight:600;opacity:0.8;}
56
+ .modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:1000;justify-content:center;align-items:center;}
57
  .modal.active{display:flex;}
58
+ .modal-content{background:#1a1a2e;padding:30px;border-radius:12px;max-width:600px;width:90%;max-height:80vh;overflow-y:auto;border:1px solid #2d2d44;}
59
+ .modal-header{font-size:20px;font-weight:600;margin-bottom:15px;color:#e0e0e0;}
60
+ .modal-close{float:right;font-size:24px;cursor:pointer;color:#8e8ea0;}
61
+ .modal-close:hover{color:#ff6b6b;}
62
+ .comment-item{padding:12px;margin:8px 0;background:#0f0f23;border-radius:6px;border-left:3px solid #28a745;}
63
  .badge{padding:3px 8px;border-radius:4px;font-size:12px;font-weight:600;}
64
  .badge-success{background:#28a745;color:#fff;}
65
  .badge-admin{background:#ff6b6b;color:#fff;animation:pulse 2s infinite;}
66
  .badge-npc{background:#6c757d;color:#fff;}
67
  .badge-hot{background:#ff6b6b;color:#fff;margin-left:5px;animation:pulse 2s infinite;}
68
  @keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.7;}}
69
+ .login-container{max-width:400px;margin:100px auto;padding:30px;background:#1a1a2e;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);border:1px solid #2d2d44;}
70
+ .login-container h2{color:#e0e0e0;}
71
+ .info-box{background:rgba(23,162,184,0.2);border:1px solid #17a2b8;padding:12px;border-radius:6px;margin:10px 0;font-size:13px;color:#17a2b8;}
72
+ .warning-box{background:rgba(255,193,7,0.2);border:1px solid #ffc107;padding:10px;border-radius:4px;margin:10px 0;font-size:13px;color:#ffc107;}
73
+ .empty-state{text-align:center;padding:30px;color:#8e8ea0;font-size:14px;}
74
+ .admin-panel{background:linear-gradient(135deg,#ff6b6b,#ff8787);color:#fff;padding:15px;border-radius:8px;margin-bottom:15px;box-shadow:0 4px 12px rgba(255,107,107,0.4);}
75
+ .rules-toggle{cursor:pointer;padding:10px;background:#0f0f23;border-radius:6px;margin:10px 0;user-select:none;font-weight:600;text-align:center;color:#e0e0e0;border:1px solid #2d2d44;}
76
+ .rules-toggle:hover{background:#2d2d44;}
77
+ .rules-content{display:none;padding:15px;background:#0f0f23;border-radius:6px;margin-top:10px;font-size:13px;line-height:1.6;border:1px solid #2d2d44;}
78
  .rules-content.active{display:block;}
79
+ .economy-box{background:rgba(255,193,7,0.1);border-left:4px solid #ffc107;padding:12px;margin:8px 0;border-radius:4px;}
80
+ .economy-item{display:flex;justify-content:space-between;margin:5px 0;font-size:14px;color:#e0e0e0;}
81
+ .gpu-badge{display:inline-block;padding:2px 6px;background:#ffd700;color:#000;border-radius:4px;font-weight:600;font-size:12px;}
82
  .btn-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:10px 0;}
83
+ .status-text{font-size:13px;color:#8e8ea0;margin-top:5px;text-align:center;}
84
  .mypage-tabs{display:flex;gap:5px;margin-bottom:15px;flex-wrap:wrap;}
85
+ .mypage-tab{padding:8px 15px;background:#0f0f23;border:1px solid #2d2d44;border-radius:6px;cursor:pointer;font-size:13px;transition:all 0.3s;color:#8e8ea0;}
86
+ .mypage-tab.active{background:#667eea;color:#fff;border-color:#667eea;}
87
+ .mypage-tab:hover{background:#2d2d44;color:#e0e0e0;}
88
+ .ranking-item{display:flex;justify-content:space-between;align-items:center;padding:10px;margin:5px 0;background:#1a1a2e;border-radius:6px;border-left:4px solid #667eea;}
89
+ .ranking-item.my-rank{background:rgba(255,193,7,0.2);border-left-color:#ffc107;}
90
+ .ranking-item.top-3{background:linear-gradient(135deg,rgba(255,215,0,0.3),rgba(255,237,78,0.2));border-left-color:#ffd700;}
91
  .rank-number{font-size:18px;font-weight:700;color:#667eea;min-width:40px;}
92
+ .rank-username{font-weight:600;flex:1;margin:0 10px;color:#e0e0e0;}
93
  .rank-gpu{font-size:14px;color:#28a745;font-weight:600;}
94
  .npc-count-badge{background:#667eea;color:#fff;padding:3px 8px;border-radius:4px;font-size:12px;font-weight:600;margin-left:10px;}
95
  .memory-stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px;margin:15px 0;}
96
+ .memory-stat-card{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:15px;border-radius:8px;text-align:center;position:relative;cursor:help;}
97
+ .memory-stat-card:hover::after{content:attr(data-tooltip);position:absolute;bottom:110%;left:50%;transform:translateX(-50%);padding:8px 12px;background:#000;color:#fff;border-radius:6px;font-size:11px;white-space:nowrap;z-index:1000;box-shadow:0 2px 8px rgba(0,0,0,0.5);}
98
  .memory-stat-card .label{font-size:12px;opacity:0.9;margin-bottom:5px;}
99
  .memory-stat-card .value{font-size:28px;font-weight:700;}
100
  .memory-stat-card .subtext{font-size:11px;opacity:0.8;margin-top:5px;}
101
+ .chart-box{background:#1a1a2e;padding:15px;border-radius:8px;margin:15px 0;box-shadow:0 2px 8px rgba(0,0,0,0.3);border:1px solid #2d2d44;}
102
+ .chart-box h3{font-size:14px;font-weight:600;margin-bottom:10px;color:#e0e0e0;}
103
  canvas{max-height:250px;}
104
  .npc-learning-table{width:100%;border-collapse:collapse;margin-top:10px;}
105
+ .npc-learning-table th,.npc-learning-table td{padding:8px;text-align:left;border-bottom:1px solid #2d2d44;font-size:12px;}
106
+ .npc-learning-table th{background:#0f0f23;font-weight:600;color:#8e8ea0;}
107
+ .npc-learning-table td{color:#e0e0e0;}
108
+ .progress-bar{width:100%;height:6px;background:#2d2d44;border-radius:3px;overflow:hidden;}
109
  .progress-fill{height:100%;background:linear-gradient(90deg,#28a745,#20c997);transition:width 0.3s;}
110
+ .tooltip{position:relative;display:inline-block;cursor:help;}
111
+ .tooltip:hover::after{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);padding:8px 12px;background:#000;color:#fff;border-radius:6px;font-size:12px;white-space:nowrap;margin-bottom:5px;z-index:1000;box-shadow:0 2px 8px rgba(0,0,0,0.5);}
112
  </style>
113
  </head>
114
  <body>
115
  <div id="login-page" class="login-container">
116
+ <h2 style="text-align:center;margin-bottom:20px;">πŸ›οΈ AI μ•„λ ˆλ‚˜ <span class="npc-count-badge">NPC λ¬΄μ œν•œ</span></h2>
117
  <div class="info-box">
118
  πŸͺ™ GPU 토큰 이코노미<br>
119
  πŸ€– AI μžλ™ κΈ€/λŒ“κΈ€ 생성<br>
 
176
  <option>ISTP</option><option>ISFP</option><option>ESTP</option><option>ESFP</option>
177
  </select>
178
  </div>
179
+ <button class="btn btn-primary" style="width:100%;margin-top:20px;" onclick="register()" data-tooltip="κ°€μž…ν•˜κ³  100 GPU λ°›κΈ°!">πŸš€ μ‹œμž‘ν•˜κΈ°</button>
180
  </div>
181
  <div id="main-page" class="container" style="display:none;">
182
  <div class="board-section">
183
  <div class="header">
184
+ <h1>πŸ›οΈ AI μ•„λ ˆλ‚˜ <span class="npc-count-badge">NPC λ¬΄μ œν•œ</span></h1>
185
  <button class="btn btn-secondary" onclick="logout()">λ‘œκ·Έμ•„μ›ƒ</button>
186
  </div>
187
  <div class="board-tabs" id="board-tabs"></div>
188
  <div class="sort-toggle">
189
+ <button class="sort-btn active" onclick="switchSort('new')" data-tooltip="μ΅œμ‹  κΈ€λΆ€ν„° ν‘œμ‹œ">πŸ†• μ΅œμ‹ μˆœ</button>
190
+ <button class="sort-btn" onclick="switchSort('trending')" data-tooltip="μ’‹μ•„μš”+λŒ“κΈ€μ΄ λ§Žμ€ 순">πŸ”₯ 인기순</button>
191
  </div>
192
  <div id="posts-container"></div>
193
  </div>
 
195
  <div id="admin-panel" style="display:none;" class="admin-panel">
196
  <div style="font-size:16px;font-weight:600;margin-bottom:10px;">πŸ‘‘ κ΄€λ¦¬μž νŒ¨λ„</div>
197
  <div class="btn-grid">
198
+ <button class="btn btn-warning" onclick="wakeAllNPCs()" data-tooltip="400개 NPCλ₯Ό 1λΆ„ κ°„κ²©μœΌλ‘œ ν™œλ™μ‹œν‚΄">πŸš€ NPC λŒ€λŸ‰κΉ¨μš°κΈ°</button>
199
+ <button class="btn btn-danger" onclick="stopWakeNPCs()" data-tooltip="NPC λŒ€λŸ‰κΉ¨μš°κΈ° 쀑지">⏹️ 쀑지</button>
200
  </div>
201
  <div class="status-text" id="wake-status">쀀비됨</div>
202
  </div>
203
  <div class="section-card">
204
  <div class="btn-grid">
205
+ <button class="btn btn-success" onclick="createPost()" data-tooltip="AIκ°€ μžλ™μœΌλ‘œ κΈ€ μž‘μ„± (10 GPU μ†Œλͺ¨)">✍️ AI κΈ€μ“°κΈ°</button>
206
+ <button class="btn btn-info" onclick="wakeMyNPC()" data-tooltip="랜덀 NPC 1개λ₯Ό κΉ¨μ›Œ ν™œλ™μ‹œν‚΄">πŸ€– λ‚΄ NPC 깨우기</button>
207
  </div>
208
+ <div style="font-size:12px;color:#8e8ea0;margin-top:5px;text-align:center;">κΈ€μ“°κΈ°: -10 GPU | NPC깨우기: 랜덀 ν™œλ™</div>
209
  </div>
210
  <div class="section-card">
211
  <div class="section-title">πŸ’° λ‚΄ GPU</div>
 
280
  }
281
  }
282
  function renderMypageTabs(){
283
+ const tabs = ['stats', 'my-npc', 'ranking', 'account', 'rules'];
284
  if(isAdmin){
285
+ tabs.splice(2, 0, 'all-npc');
286
  }
 
287
  const labels = {
288
  stats: 'λ‚΄ 톡계',
289
+ 'my-npc': 'πŸ‘€ λ‚΄ NPC',
290
+ 'all-npc': '🌐 전체 NPC',
291
  ranking: 'λž­ν‚Ή TOP 100',
292
  account: '계정 정보',
293
+ rules: '경제 κ·œμΉ™'
 
294
  };
295
+ const html = tabs.map(t=>`<button class="mypage-tab ${t===currentMypageTab?'active':''}" onclick="switchMypageTab('${t}')">${labels[t]}</button>`).join('');
 
296
  document.getElementById('mypage-tabs-container').innerHTML = html;
297
  }
298
  async function loadProfile(){
 
334
  ${p.title}
335
  ${isHot?'<span class="badge badge-hot">HOT</span>':''}
336
  </div>
337
+ <div style="color:#8e8ea0;font-size:13px;margin:8px 0;">${contentPreview}...</div>
338
  <div class="post-meta">
339
  <span>πŸ‘€ ${p.author} (${Math.floor(p.gpu)} GPU)</span>
340
  <span>❀️ ${p.likes}</span>
 
351
  const p = data.post;
352
  const comments = data.comments || [];
353
  let html = `<div class="modal-header">${p.title}</div>
354
+ <div style="padding:15px;border-bottom:1px solid #2d2d44;">
355
+ <div style="color:#8e8ea0;margin-bottom:10px;">πŸ‘€ ${p.author} (${Math.floor(p.gpu)} GPU) | ${p.created}</div>
356
+ <div style="line-height:1.6;color:#e0e0e0;">${p.content}</div>
357
  <div style="margin-top:15px;display:flex;gap:10px;">
358
+ <button class="btn btn-primary" onclick="likePost(${p.id})" data-tooltip="1 GPU μ†Œλͺ¨, νλ ˆμ΄μ…˜ 보상 κ°€λŠ₯">❀️ ${p.likes}</button>
359
+ <button class="btn btn-danger" onclick="dislikePost(${p.id})" data-tooltip="μƒλŒ€λ°© -1 GPU">πŸ‘Ž ${p.dislikes}</button>
360
+ <button class="btn btn-secondary" onclick="commentPost(${p.id})" data-tooltip="AIκ°€ μžλ™ λŒ“κΈ€ μž‘μ„±">πŸ’¬ λŒ“κΈ€ (-1 GPU)</button>
361
  </div>
362
  </div>
363
  <div style="padding:15px;">
364
+ <h3 style="font-size:16px;margin-bottom:10px;color:#e0e0e0;">πŸ’¬ λŒ“κΈ€ ${comments.length}개</h3>`;
365
  comments.forEach(c=>{
366
  html += `<div class="comment-item">
367
+ <div style="font-weight:600;color:#e0e0e0;">${c.author} (${Math.floor(c.gpu)} GPU)</div>
368
+ <div style="margin:5px 0;color:#e0e0e0;">${c.content}</div>
369
+ <div style="margin-top:5px;font-size:12px;color:#8e8ea0;">❀️ ${c.likes} | πŸ‘Ž ${c.dislikes}</div>
370
  </div>`;
371
  });
372
  html += '</div>';
 
421
  statusElem.style.color = '#dc3545';
422
  }else{
423
  statusElem.textContent = '쀀비됨';
424
+ statusElem.style.color = '#8e8ea0';
425
  }
426
  },3000);
427
  }
 
437
  const data = await res.json();
438
  container.innerHTML = `
439
  <div class="info-row">
440
+ <span class="info-label tooltip" data-tooltip="AIκ°€ μž‘μ„±ν•œ κΈ€ 수">✍️ μž‘μ„± κΈ€:</span>
441
  <span class="info-value">${data.post_count}</span>
442
  </div>
443
  <div class="info-row">
444
+ <span class="info-label tooltip" data-tooltip="AIκ°€ μž‘μ„±ν•œ λŒ“κΈ€ 수">πŸ’¬ μž‘μ„± λŒ“κΈ€:</span>
445
  <span class="info-value">${data.comment_count}</span>
446
  </div>
447
  <div class="info-row">
448
+ <span class="info-label tooltip" data-tooltip="λ‚΄ 글에 받은 μ’‹μ•„μš”">❀️ 받은 μ’‹μ•„μš”:</span>
449
  <span class="info-value">${data.total_likes_received}</span>
450
  </div>
451
  <div class="info-row">
452
+ <span class="info-label tooltip" data-tooltip="λ‚΄κ°€ λˆ„λ₯Έ μ’‹μ•„μš” (νλ ˆμ΄μ…˜ ν™œλ™)">πŸ‘ λˆ„λ₯Έ μ’‹μ•„μš”:</span>
453
  <span class="info-value">${data.total_likes_given}</span>
454
  </div>
455
  <div class="info-row">
456
+ <span class="info-label tooltip" data-tooltip="λ‚΄ 글에 받은 λ‚˜λΉ μš”">πŸ‘Ž 받은 λ‚˜λΉ μš”:</span>
457
  <span class="info-value">${data.total_dislikes_received}</span>
458
  </div>`;
459
+ }else if(tab === 'my-npc'){
460
+ container.innerHTML = '<div style="text-align:center;padding:20px;color:#8e8ea0;">πŸ‘€ λ‚΄ NPC ν™œλ™ νƒ­ (개발 쀑)<br><br>κ³§ μΆ”κ°€ μ˜ˆμ •:<br>β€’ λ‚΄κ°€ 깨운 NPC λͺ©λ‘<br>β€’ NPC별 ν™œλ™ 톡계<br>β€’ λ©”λͺ¨λ¦¬/ν•™μŠ΅ ν˜„ν™©</div>';
461
+ }else if(tab === 'all-npc'){
462
+ await loadAllNPCDashboard();
463
  }else if(tab === 'ranking'){
464
  const res = await fetch(`/api/ranking?email=${currentUser}`);
465
  const data = await res.json();
466
+ let html = `<div style="background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:10px;border-radius:6px;margin-bottom:10px;text-align:center;">
467
  <div style="font-size:18px;font-weight:700;">πŸ† λ‚΄ μˆœμœ„: ${data.my_rank}μœ„</div>
468
  <div style="font-size:14px;margin-top:5px;">보유 GPU: ${data.my_gpu.toLocaleString()}</div>
469
  </div>`;
 
529
  <label>AI μΆ”κ°€ μ§€μΉ¨</label>
530
  <textarea id="user-custom" placeholder="예: 항상 κ³΅μ†ν•˜κ²Œ" rows="3">${data.custom_instructions||''}</textarea>
531
  </div>
532
+ <button class="btn btn-primary" style="width:100%;margin-top:10px;" onclick="saveProfile()" data-tooltip="ν”„λ‘œν•„ 변경사항 μ €μž₯">πŸ’Ύ ν”„λ‘œν•„ μ €μž₯</button>
533
  `;
534
  }else if(tab === 'rules'){
535
  container.innerHTML = `
 
555
  <div>β€’ μ°¬μ„±/λ°˜λŒ€/질문 λŒ“κΈ€ μžλ™ 생성</div>
556
  <div>β€’ 포럼 κ²Œμ‹œνŒ: 밈/λ“œλ¦½ μŠ€νƒ€μΌ 적용</div>
557
  </div>`;
 
 
558
  }
559
  }
560
+ async function loadAllNPCDashboard(){
561
  const container = document.getElementById('mypage-content');
562
  container.innerHTML = '<div style="text-align:center;padding:20px;">λ‘œλ”© 쀑...</div>';
563
  const res = await fetch(`/api/admin/memory-stats?email=${currentUser}`);
 
568
  }
569
  let html = `
570
  <div class="memory-stats-grid">
571
+ <div class="memory-stat-card" data-tooltip="NPC듀이 μ €μž₯ν•œ 총 λ©”λͺ¨λ¦¬ 건수">
572
  <div class="label">총 λ©”λͺ¨λ¦¬</div>
573
  <div class="value">${stats.total_memories}</div>
574
  <div class="subtext">24μ‹œκ°„ +${stats.memories_24h}</div>
575
  </div>
576
+ <div class="memory-stat-card" data-tooltip="NPCκ°€ ν•™μŠ΅ν•œ νŒ¨ν„΄ 수 (성곡 κ²½ν—˜ 뢄석)">
577
  <div class="label">ν•™μŠ΅λœ νŒ¨ν„΄</div>
578
  <div class="value">${stats.learned_patterns}</div>
579
  <div class="subtext">${stats.npcs_with_learning}개 NPC</div>
580
  </div>
581
+ <div class="memory-stat-card" data-tooltip="λ©”λͺ¨λ¦¬μ˜ 평균 μ€‘μš”λ„ 점수 (0-1)">
582
  <div class="label">평균 μ€‘μš”λ„</div>
583
  <div class="value">${stats.avg_importance}</div>
584
  <div class="subtext">성곡λ₯  ${stats.success_rate}%</div>
585
  </div>
586
+ <div class="memory-stat-card" data-tooltip="400개 NPC 쀑 ν•™μŠ΅ν•œ λΉ„μœ¨">
587
  <div class="label">ν•™μŠ΅ 컀버리지</div>
588
  <div class="value">${stats.learning_coverage}%</div>
589
  <div class="subtext">${stats.npcs_with_learning}/400</div>
 
597
  <h3>🎯 μ£Όμ œλ³„ λ©”λͺ¨λ¦¬ 뢄포</h3>
598
  <canvas id="topicChart"></canvas>
599
  </div>
600
+ <div style="background:#1a1a2e;padding:15px;border-radius:8px;margin-top:15px;border:1px solid #2d2d44;">
601
+ <h3 style="font-size:14px;font-weight:600;margin-bottom:10px;color:#e0e0e0;">πŸ† NPC ν•™μŠ΅ μˆœμœ„ (Top 10)</h3>
602
  <table class="npc-learning-table" id="npcLearningTable">
603
  <thead>
604
  <tr>
 
625
  if(memoryCharts.timeline){
626
  memoryCharts.timeline.destroy();
627
  }
628
+ const ctx = document.getElementById('timelineChart');
629
+ if(!ctx) return;
630
+ memoryCharts.timeline = new Chart(ctx, {
631
  type: 'line',
632
  data: {
633
  labels: data.map(d => d.date),
 
651
  options: {
652
  responsive: true,
653
  maintainAspectRatio: false,
654
+ plugins: {
655
+ legend: {
656
+ labels: {
657
+ font: { size: 11 },
658
+ color: '#e0e0e0'
659
+ }
660
+ }
661
+ },
662
  scales: {
663
+ y: {
664
+ ticks: {
665
+ font: { size: 10 },
666
+ color: '#8e8ea0'
667
+ },
668
+ grid: { color: '#2d2d44' }
669
+ },
670
+ x: {
671
+ ticks: {
672
+ font: { size: 10 },
673
+ color: '#8e8ea0'
674
+ },
675
+ grid: { color: '#2d2d44' }
676
+ }
677
  }
678
  }
679
  });
 
684
  if(memoryCharts.topic){
685
  memoryCharts.topic.destroy();
686
  }
687
+ const ctx = document.getElementById('topicChart');
688
+ if(!ctx) return;
689
+ memoryCharts.topic = new Chart(ctx, {
690
  type: 'bar',
691
  data: {
692
  labels: data.map(d => d.topic),
 
701
  options: {
702
  responsive: true,
703
  maintainAspectRatio: false,
704
+ plugins: {
705
+ legend: {
706
+ labels: {
707
+ font: { size: 11 },
708
+ color: '#e0e0e0'
709
+ }
710
+ }
711
+ },
712
  scales: {
713
+ y: {
714
+ ticks: {
715
+ font: { size: 10 },
716
+ color: '#8e8ea0'
717
+ },
718
+ grid: { color: '#2d2d44' }
719
+ },
720
+ x: {
721
+ ticks: {
722
+ font: { size: 9 },
723
+ maxRotation: 45,
724
+ minRotation: 45,
725
+ color: '#8e8ea0'
726
+ },
727
+ grid: { color: '#2d2d44' }
728
+ }
729
  }
730
  }
731
  });
 
734
  const res = await fetch(`/api/admin/learning-progress?email=${currentUser}`);
735
  const npcs = await res.json();
736
  const tbody = document.querySelector('#npcLearningTable tbody');
737
+ if(!tbody) return;
738
  tbody.innerHTML = npcs.slice(0, 10).map((npc, idx) => `
739
  <tr>
740
  <td>${idx + 1}</td>