commit-ai-agent 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -3,10 +3,66 @@
3
3
  // ── State ──
4
4
  let selectedProject = null;
5
5
  let isAnalyzing = false;
6
- let analyzeMode = 'commit'; // 'commit' | 'status'
6
+ let analyzeMode = "commit"; // 'commit' | 'status'
7
+ let isSingleProject = false;
8
+ let singleProjectName = "";
9
+
10
+ // ── Aria State Machine ──
11
+ function setAriaState(state, opts = {}) {
12
+ const robotWrap = document.getElementById("aria-robot-wrap");
13
+ const bubbleText = document.getElementById("aria-bubble-text");
14
+ const typingDots = document.getElementById("aria-typing-dots");
15
+ const chipDot = document.getElementById("aria-chip-dot");
16
+ const chipText = document.getElementById("aria-chip-text");
17
+ if (!robotWrap || !bubbleText) return;
18
+
19
+ // Base state (strip -commit / -status suffix for robot/chip)
20
+ const baseState = state.startsWith("ready") ? "ready" : state;
21
+
22
+ // Robot animation class
23
+ robotWrap.className = `aria-robot-wrap ${baseState}`;
24
+
25
+ // Header chip
26
+ const chipMap = {
27
+ idle: { cls: "idle", label: "Hanni · 대기 중" },
28
+ ready: { cls: "ready", label: "Hanni · 준비됨" },
29
+ thinking: { cls: "thinking", label: "Hanni · 분석 중..." },
30
+ done: { cls: "done", label: "Hanni · 완료" },
31
+ error: { cls: "error", label: "Hanni · 오류" },
32
+ };
33
+ const cm = chipMap[baseState] || chipMap.idle;
34
+ if (chipDot) chipDot.className = `aria-chip-dot ${cm.cls}`;
35
+ if (chipText) chipText.textContent = cm.label;
36
+
37
+ // Bubble messages
38
+ const p = opts.project ? `<strong>${opts.project}</strong>` : "";
39
+ const msgMap = {
40
+ idle: "어떤 프로젝트의 커밋을 분석해드릴까요?",
41
+ "ready-commit": `${p} 최근 커밋을 확인했어요. 분석을 시작할까요? 👀`,
42
+ "ready-status":
43
+ opts.n > 0
44
+ ? `${p}에서 변경된 파일 <strong>${opts.n}개</strong>를 발견했어요. 리뷰해드릴까요?`
45
+ : `${p}에 현재 변경사항이 없어요.`,
46
+ thinking: "코드를 꼼꼼히 살펴보고 있어요",
47
+ done: "분석 완료! 리포트를 확인해보세요. 😊",
48
+ error: "앗, 문제가 발생했어요. 다시 시도해볼까요?",
49
+ };
50
+ const newMsg = msgMap[state] || msgMap.idle;
51
+
52
+ // Fade transition
53
+ bubbleText.style.opacity = "0";
54
+ bubbleText.style.transform = "translateY(4px)";
55
+ setTimeout(() => {
56
+ bubbleText.innerHTML = newMsg;
57
+ if (typingDots)
58
+ typingDots.style.display = state === "thinking" ? "inline-flex" : "none";
59
+ bubbleText.style.opacity = "1";
60
+ bubbleText.style.transform = "translateY(0)";
61
+ }, 180);
62
+ }
7
63
 
8
64
  // ── Boot ──
9
- document.addEventListener('DOMContentLoaded', () => {
65
+ document.addEventListener("DOMContentLoaded", () => {
10
66
  init();
11
67
  });
12
68
 
@@ -14,40 +70,81 @@ async function init() {
14
70
  setupTabs();
15
71
  setupModeToggle();
16
72
  await checkConfig();
17
- await loadProjects();
73
+
74
+ if (isSingleProject) {
75
+ await enterSingleProjectMode();
76
+ } else {
77
+ await loadProjects();
78
+ setAriaState("idle");
79
+ }
18
80
 
19
81
  // wire static event listeners (elements guaranteed to exist now)
20
- document.getElementById('refresh-projects').addEventListener('click', loadProjects);
21
- document.getElementById('analyze-btn').addEventListener('click', onAnalyzeClick);
22
- document.getElementById('copy-btn').addEventListener('click', onCopy);
23
- document.getElementById('close-report-btn').addEventListener('click', () => {
24
- document.getElementById('report-viewer').style.display = 'none';
25
- });
26
- document.getElementById('diff-toggle-btn').addEventListener('click', () => {
27
- togglePre('diff-content', 'diff-toggle-btn');
82
+ document
83
+ .getElementById("refresh-projects")
84
+ .addEventListener("click", loadProjects);
85
+ document
86
+ .getElementById("analyze-btn")
87
+ .addEventListener("click", onAnalyzeClick);
88
+ document.getElementById("copy-btn").addEventListener("click", onCopy);
89
+ document.getElementById("close-report-btn").addEventListener("click", () => {
90
+ document.getElementById("report-viewer").style.display = "none";
28
91
  });
29
- document.getElementById('status-diff-toggle-btn').addEventListener('click', () => {
30
- togglePre('status-diff-content', 'status-diff-toggle-btn');
92
+ document.getElementById("diff-toggle-btn").addEventListener("click", () => {
93
+ togglePre("diff-content", "diff-toggle-btn");
31
94
  });
95
+ document
96
+ .getElementById("status-diff-toggle-btn")
97
+ .addEventListener("click", () => {
98
+ togglePre("status-diff-content", "status-diff-toggle-btn");
99
+ });
32
100
  }
33
101
 
34
102
  // ── Config check ──
35
103
  async function checkConfig() {
36
104
  try {
37
- const res = await fetch('/api/config');
38
- const { hasKey } = await res.json();
39
- if (!hasKey) {
40
- document.getElementById('api-key-warn').style.display = 'flex';
105
+ const res = await fetch("/api/config");
106
+ const data = await res.json();
107
+ if (!data.hasKey) {
108
+ document.getElementById("api-key-warn").style.display = "flex";
109
+ }
110
+ if (data.isSingleProject) {
111
+ isSingleProject = true;
112
+ singleProjectName = data.singleProjectName || "project";
41
113
  }
42
114
  } catch {}
43
115
  }
44
116
 
117
+ // ── Single Project Mode ──
118
+ async function enterSingleProjectMode() {
119
+ // 프로젝트 선택 UI 숨김
120
+ const header = document.querySelector(".selector-card .card-header");
121
+ const modeToggle = document.querySelector(".mode-toggle");
122
+ const projectGrid = document.getElementById("project-grid");
123
+ const selectedHint = document.getElementById("selected-hint");
124
+ if (header) header.style.display = "none";
125
+ if (modeToggle) modeToggle.style.display = "none";
126
+ if (projectGrid) projectGrid.style.display = "none";
127
+ if (selectedHint) selectedHint.style.display = "none";
128
+
129
+ // 현재 디렉토리를 프로젝트로 자동 선택
130
+ selectedProject = "__self__";
131
+ analyzeMode = "status";
132
+
133
+ document.getElementById("analyze-btn").disabled = false;
134
+ const btnText = document.getElementById("analyze-btn-text");
135
+ if (btnText) btnText.textContent = "Hanni에게 리뷰 요청";
136
+
137
+ // 변경사항 미리 로드
138
+ await fetchStatusPreview();
139
+ }
140
+
45
141
  // ── Projects ──
46
142
  async function loadProjects() {
47
- const grid = document.getElementById('project-grid');
48
- grid.innerHTML = '<div class="skeleton-grid"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>';
143
+ const grid = document.getElementById("project-grid");
144
+ grid.innerHTML =
145
+ '<div class="skeleton-grid"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>';
49
146
  try {
50
- const res = await fetch('/api/projects');
147
+ const res = await fetch("/api/projects");
51
148
  const { projects } = await res.json();
52
149
  renderProjects(projects);
53
150
  } catch (err) {
@@ -56,43 +153,51 @@ async function loadProjects() {
56
153
  }
57
154
 
58
155
  function renderProjects(projects) {
59
- const grid = document.getElementById('project-grid');
156
+ const grid = document.getElementById("project-grid");
60
157
  if (!projects || projects.length === 0) {
61
- grid.innerHTML = '<p style="color:var(--text3);font-size:14px">git 프로젝트가 없습니다.</p>';
158
+ grid.innerHTML =
159
+ '<p style="color:var(--text3);font-size:14px">git 프로젝트가 없습니다.</p>';
62
160
  return;
63
161
  }
64
- grid.innerHTML = projects.map(p => `
162
+ grid.innerHTML = projects
163
+ .map(
164
+ (p) => `
65
165
  <div class="project-item" data-name="${p.name}">
66
166
  <span class="proj-icon">${getProjectIcon(p.name)}</span>
67
167
  <span class="proj-name">${p.name}</span>
68
168
  </div>
69
- `).join('');
70
- grid.querySelectorAll('.project-item').forEach(el => {
71
- el.addEventListener('click', () => selectProject(el));
169
+ `,
170
+ )
171
+ .join("");
172
+ grid.querySelectorAll(".project-item").forEach((el) => {
173
+ el.addEventListener("click", () => selectProject(el));
72
174
  });
73
175
  }
74
176
 
75
177
  function getProjectIcon(name) {
76
- if (name.includes('next') || name.includes('react')) return '⚛️';
77
- if (name.includes('nest') || name.includes('api')) return '🐉';
78
- if (name.includes('hook')) return '🪝';
79
- if (name.includes('portfolio')) return '🎨';
80
- if (name.includes('todo')) return '';
81
- if (name.includes('doc')) return '📚';
82
- return '📁';
178
+ if (name.includes("next") || name.includes("react")) return "⚛️";
179
+ if (name.includes("nest") || name.includes("api")) return "🐉";
180
+ if (name.includes("hook")) return "🪝";
181
+ if (name.includes("portfolio")) return "🎨";
182
+ if (name.includes("todo")) return "";
183
+ if (name.includes("doc")) return "📚";
184
+ return "📁";
83
185
  }
84
186
 
85
187
  // ── Project Selection ──
86
188
  async function selectProject(el) {
87
- document.querySelectorAll('.project-item').forEach(e => e.classList.remove('selected'));
88
- el.classList.add('selected');
189
+ document
190
+ .querySelectorAll(".project-item")
191
+ .forEach((e) => e.classList.remove("selected"));
192
+ el.classList.add("selected");
89
193
  selectedProject = el.dataset.name;
90
- document.getElementById('selected-hint').textContent = `선택됨: ${selectedProject}`;
91
- document.getElementById('analyze-btn').disabled = false;
92
- document.getElementById('commit-card').style.display = 'none';
93
- document.getElementById('status-card').style.display = 'none';
194
+ document.getElementById("selected-hint").textContent =
195
+ `선택됨: ${selectedProject}`;
196
+ document.getElementById("analyze-btn").disabled = false;
197
+ document.getElementById("commit-card").style.display = "none";
198
+ document.getElementById("status-card").style.display = "none";
94
199
 
95
- if (analyzeMode === 'commit') {
200
+ if (analyzeMode === "commit") {
96
201
  await fetchCommitPreview();
97
202
  } else {
98
203
  await fetchStatusPreview();
@@ -100,91 +205,127 @@ async function selectProject(el) {
100
205
  }
101
206
 
102
207
  async function fetchCommitPreview() {
208
+ const displayName = isSingleProject ? singleProjectName : selectedProject;
103
209
  try {
104
- const res = await fetch(`/api/projects/${encodeURIComponent(selectedProject)}/commit`);
210
+ const res = await fetch(
211
+ `/api/projects/${encodeURIComponent(selectedProject)}/commit`,
212
+ );
105
213
  const { commit, error } = await res.json();
106
214
  if (error) throw new Error(error);
107
215
  renderCommitCard(commit);
108
- document.getElementById('commit-card').style.display = 'block';
216
+ document.getElementById("commit-card").style.display = "block";
217
+ setAriaState("ready-commit", { project: displayName });
109
218
  } catch (e) {
110
- console.warn('commit preview failed:', e.message);
219
+ console.warn("commit preview failed:", e.message);
220
+ setAriaState("ready-commit", { project: displayName });
111
221
  }
112
222
  }
113
223
 
114
224
  async function fetchStatusPreview() {
225
+ const displayName = isSingleProject ? singleProjectName : selectedProject;
115
226
  try {
116
- const res = await fetch(`/api/projects/${encodeURIComponent(selectedProject)}/status`);
227
+ const res = await fetch(
228
+ `/api/projects/${encodeURIComponent(selectedProject)}/status`,
229
+ );
117
230
  const { status, error } = await res.json();
118
231
  if (error) throw new Error(error);
119
232
  if (status) {
120
233
  renderStatusCard(status);
121
- document.getElementById('status-card').style.display = 'block';
234
+ document.getElementById("status-card").style.display = "block";
235
+ setAriaState("ready-status", {
236
+ project: displayName,
237
+ n: status.totalFiles,
238
+ });
122
239
  } else {
123
- document.getElementById('selected-hint').textContent = `${selectedProject} — 변경사항 없음`;
240
+ const hint = document.getElementById("selected-hint");
241
+ if (hint) hint.textContent = `${displayName} — 변경사항 없음`;
242
+ setAriaState("ready-status", { project: displayName, n: 0 });
124
243
  }
125
244
  } catch (e) {
126
- console.warn('status preview failed:', e.message);
245
+ console.warn("status preview failed:", e.message);
246
+ setAriaState("ready-status", { project: displayName, n: 0 });
127
247
  }
128
248
  }
129
249
 
130
250
  // ── Commit Card ──
131
251
  function renderCommitCard(c) {
132
- document.getElementById('commit-meta').innerHTML = `
252
+ document.getElementById("commit-meta").innerHTML = `
133
253
  <div class="meta-item"><div class="meta-label">해시</div><div class="meta-value hash">${c.shortHash}</div></div>
134
254
  <div class="meta-item"><div class="meta-label">메시지</div><div class="meta-value">${escHtml(c.message)}</div></div>
135
255
  <div class="meta-item"><div class="meta-label">작성자</div><div class="meta-value">${escHtml(c.author)}</div></div>
136
256
  <div class="meta-item"><div class="meta-label">날짜</div><div class="meta-value">${escHtml(c.date)}</div></div>
137
257
  `;
138
- const pre = document.getElementById('diff-content');
139
- pre.textContent = c.diffContent || '(diff 없음)';
140
- pre.style.display = 'none';
141
- document.getElementById('diff-toggle-btn').textContent = 'diff 보기 ▾';
258
+ const pre = document.getElementById("diff-content");
259
+ pre.textContent = c.diffContent || "(diff 없음)";
260
+ pre.style.display = "none";
261
+ document.getElementById("diff-toggle-btn").textContent = "diff 보기 ▾";
142
262
  }
143
263
 
144
264
  // ── Status Card ──
145
265
  function renderStatusCard(s) {
146
266
  const badges = [
147
- s.stagedCount ? `<span class="stat-chip staged">${s.stagedCount} staged</span>` : '',
148
- s.modifiedCount ? `<span class="stat-chip modified">${s.modifiedCount} modified</span>` : '',
149
- s.deletedCount ? `<span class="stat-chip deleted">${s.deletedCount} deleted</span>` : '',
150
- s.untrackedCount ? `<span class="stat-chip untracked">${s.untrackedCount} untracked</span>` : '',
151
- ].filter(Boolean).join('');
152
-
153
- document.getElementById('status-meta').innerHTML = `
267
+ s.stagedCount
268
+ ? `<span class="stat-chip staged">${s.stagedCount} staged</span>`
269
+ : "",
270
+ s.modifiedCount
271
+ ? `<span class="stat-chip modified">${s.modifiedCount} modified</span>`
272
+ : "",
273
+ s.deletedCount
274
+ ? `<span class="stat-chip deleted">${s.deletedCount} deleted</span>`
275
+ : "",
276
+ s.untrackedCount
277
+ ? `<span class="stat-chip untracked">${s.untrackedCount} untracked</span>`
278
+ : "",
279
+ ]
280
+ .filter(Boolean)
281
+ .join("");
282
+
283
+ document.getElementById("status-meta").innerHTML = `
154
284
  <div class="meta-item" style="grid-column:1/-1">
155
285
  <div class="meta-label">변경된 파일 (총 ${s.totalFiles}개)</div>
156
286
  <div class="meta-value" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">${badges || '<span style="color:var(--text3)">변경사항 없음</span>'}</div>
157
287
  </div>
158
288
  `;
159
- const pre = document.getElementById('status-diff-content');
160
- pre.textContent = s.diffContent || '(diff 없음)';
161
- pre.style.display = 'none';
162
- document.getElementById('status-diff-toggle-btn').textContent = 'diff 보기 ▾';
289
+ const pre = document.getElementById("status-diff-content");
290
+ pre.textContent = s.diffContent || "(diff 없음)";
291
+ pre.style.display = "none";
292
+ document.getElementById("status-diff-toggle-btn").textContent = "diff 보기 ▾";
163
293
  }
164
294
 
165
295
  // ── Mode Toggle ──
166
296
  function setupModeToggle() {
167
- document.getElementById('mode-commit')?.addEventListener('click', () => switchMode('commit'));
168
- document.getElementById('mode-status')?.addEventListener('click', () => switchMode('status'));
297
+ document
298
+ .getElementById("mode-commit")
299
+ ?.addEventListener("click", () => switchMode("commit"));
300
+ document
301
+ .getElementById("mode-status")
302
+ ?.addEventListener("click", () => switchMode("status"));
169
303
  }
170
304
 
171
305
  function switchMode(mode) {
172
306
  analyzeMode = mode;
173
- document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
174
- document.getElementById(`mode-${mode}`).classList.add('active');
175
- document.getElementById('commit-card').style.display = 'none';
176
- document.getElementById('status-card').style.display = 'none';
177
- document.getElementById('result-card').style.display = 'none';
307
+ document
308
+ .querySelectorAll(".mode-btn")
309
+ .forEach((b) => b.classList.remove("active"));
310
+ document.getElementById(`mode-${mode}`).classList.add("active");
311
+ document.getElementById("commit-card").style.display = "none";
312
+ document.getElementById("status-card").style.display = "none";
313
+ document.getElementById("result-card").style.display = "none";
314
+
315
+ const btnText = document.getElementById("analyze-btn-text");
316
+ if (btnText)
317
+ btnText.textContent =
318
+ mode === "commit" ? "Hanni에게 분석 요청" : "Hanni에게 리뷰 요청";
178
319
 
179
320
  if (!selectedProject) return;
180
- if (mode === 'commit') fetchCommitPreview();
321
+ if (mode === "commit") fetchCommitPreview();
181
322
  else fetchStatusPreview();
182
323
  }
183
324
 
184
325
  // ── Analyze button ──
185
326
  function onAnalyzeClick() {
186
- if (analyzeMode === 'commit') startAnalysis('/api/analyze');
187
- else startAnalysis('/api/analyze-status');
327
+ if (analyzeMode === "commit") startAnalysis("/api/analyze");
328
+ else startAnalysis("/api/analyze-status");
188
329
  }
189
330
 
190
331
  // ── Generic SSE Analysis ──
@@ -192,158 +333,178 @@ async function startAnalysis(endpoint) {
192
333
  if (isAnalyzing || !selectedProject) return;
193
334
  isAnalyzing = true;
194
335
 
195
- const resultCard = document.getElementById('result-card');
196
- const analysisBody = document.getElementById('analysis-body');
197
- const reportSaved = document.getElementById('report-saved');
198
- const analyzeBtn = document.getElementById('analyze-btn');
199
- const btnIcon = analyzeBtn.querySelector('.btn-icon');
200
-
201
- resultCard.style.display = 'block';
202
- analysisBody.innerHTML = '';
203
- reportSaved.textContent = '';
204
- document.getElementById('copy-btn').style.display = 'none'; // 분석 시작 시 숨김
205
- setStatus('loading', '분석 준비 중...');
336
+ const resultCard = document.getElementById("result-card");
337
+ const analysisBody = document.getElementById("analysis-body");
338
+ const reportSaved = document.getElementById("report-saved");
339
+ const analyzeBtn = document.getElementById("analyze-btn");
340
+ const btnIcon = analyzeBtn.querySelector(".btn-icon");
341
+
342
+ resultCard.style.display = "block";
343
+ analysisBody.innerHTML = "";
344
+ reportSaved.textContent = "";
345
+ document.getElementById("copy-btn").style.display = "none"; // 분석 시작 시 숨김
346
+ setStatus("loading", "Hanni가 코드를 살펴보고 있어요...");
347
+ setAriaState("thinking");
206
348
  analyzeBtn.disabled = true;
207
- btnIcon.textContent = '';
349
+ btnIcon.textContent = "";
208
350
 
209
- let fullText = '';
351
+ let fullText = "";
210
352
 
211
353
  try {
212
354
  const res = await fetch(endpoint, {
213
- method: 'POST',
214
- headers: { 'Content-Type': 'application/json' },
355
+ method: "POST",
356
+ headers: { "Content-Type": "application/json" },
215
357
  body: JSON.stringify({ projectName: selectedProject }),
216
358
  });
217
359
 
218
360
  // 400 에러 처리 (API 키 없음 등)
219
361
  if (!res.ok) {
220
- const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
221
- setStatus('error', `오류: ${err.error}`);
362
+ const err = await res
363
+ .json()
364
+ .catch(() => ({ error: `HTTP ${res.status}` }));
365
+ setStatus("error", `오류: ${err.error}`);
222
366
  return;
223
367
  }
224
368
 
225
369
  const reader = res.body.getReader();
226
370
  const decoder = new TextDecoder();
227
- let buffer = '';
371
+ let buffer = "";
228
372
 
229
373
  while (true) {
230
374
  const { done, value } = await reader.read();
231
375
  if (done) break;
232
376
  buffer += decoder.decode(value, { stream: true });
233
- const lines = buffer.split('\n');
377
+ const lines = buffer.split("\n");
234
378
  buffer = lines.pop();
235
379
  for (const line of lines) {
236
- if (!line.startsWith('data: ')) continue;
380
+ if (!line.startsWith("data: ")) continue;
237
381
  try {
238
382
  const data = JSON.parse(line.slice(6));
239
- if (data.type === 'status') {
240
- setStatus('loading', data.message);
241
- } else if (data.type === 'commit') {
383
+ if (data.type === "status") {
384
+ setStatus("loading", data.message);
385
+ } else if (data.type === "commit") {
242
386
  renderCommitCard(data.commit);
243
- document.getElementById('commit-card').style.display = 'block';
244
- } else if (data.type === 'working-status') {
387
+ document.getElementById("commit-card").style.display = "block";
388
+ } else if (data.type === "working-status") {
245
389
  renderStatusCard(data.workingStatus);
246
- document.getElementById('status-card').style.display = 'block';
247
- } else if (data.type === 'analysis') {
390
+ document.getElementById("status-card").style.display = "block";
391
+ } else if (data.type === "analysis") {
248
392
  fullText = data.analysis;
249
393
  analysisBody.innerHTML = marked.parse(data.analysis);
250
394
  if (data.reportFilename) {
251
395
  reportSaved.textContent = `✓ 저장됨: ${data.reportFilename}`;
252
396
  }
253
- } else if (data.type === 'done') {
254
- setStatus('done', '✅ 분석 완료!');
255
- document.getElementById('copy-btn').style.display = 'inline-flex'; // 완료 시에만 표시
256
- } else if (data.type === 'error') {
257
- setStatus('error', `오류: ${data.message}`);
397
+ } else if (data.type === "done") {
398
+ setStatus("done", "✅ 분석 완료!");
399
+ setAriaState("done");
400
+ document.getElementById("copy-btn").style.display = "inline-flex"; // 완료 시에만 표시
401
+ } else if (data.type === "error") {
402
+ setStatus("error", `오류: ${data.message}`);
403
+ setAriaState("error");
258
404
  }
259
405
  } catch {}
260
406
  }
261
407
  }
262
408
  } catch (err) {
263
- setStatus('error', `네트워크 오류: ${err.message}`);
409
+ setStatus("error", `네트워크 오류: ${err.message}`);
410
+ setAriaState("error");
264
411
  } finally {
265
412
  isAnalyzing = false;
266
413
  analyzeBtn.disabled = false;
267
- btnIcon.textContent = '🔍';
268
- document.getElementById('copy-btn')._text = fullText;
269
- resultCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
414
+ btnIcon.textContent = "🤖";
415
+ document.getElementById("copy-btn")._text = fullText;
416
+ resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
270
417
  }
271
418
  }
272
419
 
273
420
  // ── Status bar ──
274
421
  function setStatus(type, msg) {
275
- const bar = document.getElementById('status-bar');
276
- const dot = bar.querySelector('.status-dot');
277
- const msgEl = document.getElementById('status-msg');
422
+ const bar = document.getElementById("status-bar");
423
+ const dot = bar.querySelector(".status-dot");
424
+ const msgEl = document.getElementById("status-msg");
278
425
  msgEl.textContent = msg;
279
- dot.className = 'status-dot ' + type;
280
- bar.className = 'status-bar ' + (type === 'loading' ? '' : type);
426
+ dot.className = "status-dot " + type;
427
+ bar.className = "status-bar " + (type === "loading" ? "" : type);
281
428
  }
282
429
 
283
430
  // ── Copy ──
284
431
  function onCopy() {
285
- const btn = document.getElementById('copy-btn');
286
- const text = btn._text || document.getElementById('analysis-body').innerText;
432
+ const btn = document.getElementById("copy-btn");
433
+ const text = btn._text || document.getElementById("analysis-body").innerText;
287
434
  navigator.clipboard.writeText(text).then(() => {
288
- btn.textContent = '✓ 복사됨';
289
- setTimeout(() => { btn.textContent = '📋 복사'; }, 2000);
435
+ btn.textContent = "✓ 복사됨";
436
+ setTimeout(() => {
437
+ btn.textContent = "📋 복사";
438
+ }, 2000);
290
439
  });
291
440
  }
292
441
 
293
442
  // ── Reports Tab ──
294
443
  async function loadReports() {
295
- const listEl = document.getElementById('reports-list');
444
+ const listEl = document.getElementById("reports-list");
296
445
  listEl.innerHTML = '<p class="empty-state">불러오는 중...</p>';
297
446
  try {
298
- const res = await fetch('/api/reports');
447
+ const res = await fetch("/api/reports");
299
448
  const { reports } = await res.json();
300
449
  if (!reports.length) {
301
- listEl.innerHTML = '<p class="empty-state">아직 저장된 리포트가 없습니다.</p>';
450
+ listEl.innerHTML =
451
+ '<p class="empty-state">아직 저장된 리포트가 없습니다.</p>';
302
452
  return;
303
453
  }
304
- listEl.innerHTML = reports.map(r => {
305
- const parts = r.replace('.md', '').split('-');
306
- const date = parts.slice(-2).join(' ');
307
- const proj = parts.slice(0, -2).join('-');
308
- return `<div class="report-item" data-file="${r}">
454
+ listEl.innerHTML = reports
455
+ .map((r) => {
456
+ const parts = r.replace(".md", "").split("-");
457
+ const date = parts.slice(-2).join(" ");
458
+ const proj = parts.slice(0, -2).join("-");
459
+ return `<div class="report-item" data-file="${r}">
309
460
  <span class="report-item-name">📄 ${proj}</span>
310
461
  <span class="report-item-date">${date}</span>
311
462
  </div>`;
312
- }).join('');
313
- listEl.querySelectorAll('.report-item').forEach(el => {
314
- el.addEventListener('click', () => openReport(el.dataset.file));
463
+ })
464
+ .join("");
465
+ listEl.querySelectorAll(".report-item").forEach((el) => {
466
+ el.addEventListener("click", () => openReport(el.dataset.file));
315
467
  });
316
468
  } catch {
317
- listEl.innerHTML = '<p class="empty-state">리포트를 불러오지 못했습니다.</p>';
469
+ listEl.innerHTML =
470
+ '<p class="empty-state">리포트를 불러오지 못했습니다.</p>';
318
471
  }
319
472
  }
320
473
 
321
474
  async function openReport(filename) {
322
- const viewer = document.getElementById('report-viewer');
323
- const body = document.getElementById('report-viewer-body');
324
- document.getElementById('report-viewer-title').textContent = filename.replace('.md', '');
475
+ const viewer = document.getElementById("report-viewer");
476
+ const body = document.getElementById("report-viewer-body");
477
+ document.getElementById("report-viewer-title").textContent = filename.replace(
478
+ ".md",
479
+ "",
480
+ );
325
481
  body.innerHTML = '<p style="color:var(--text2)">불러오는 중...</p>';
326
- viewer.style.display = 'block';
482
+ viewer.style.display = "block";
327
483
  try {
328
484
  const res = await fetch(`/api/reports/${encodeURIComponent(filename)}`);
329
485
  const { content } = await res.json();
330
486
  body.innerHTML = marked.parse(content);
331
487
  } catch {
332
- body.innerHTML = '<p style="color:var(--danger)">리포트를 불러오지 못했습니다.</p>';
488
+ body.innerHTML =
489
+ '<p style="color:var(--danger)">리포트를 불러오지 못했습니다.</p>';
333
490
  }
334
- viewer.scrollIntoView({ behavior: 'smooth' });
491
+ viewer.scrollIntoView({ behavior: "smooth" });
335
492
  }
336
493
 
337
494
  // ── Tabs ──
338
495
  function setupTabs() {
339
- document.querySelectorAll('.nav-btn').forEach(btn => {
340
- btn.addEventListener('click', () => {
496
+ document.querySelectorAll(".nav-btn").forEach((btn) => {
497
+ btn.addEventListener("click", () => {
341
498
  const tab = btn.dataset.tab;
342
- document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
343
- document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
344
- btn.classList.add('active');
345
- document.getElementById('tab-' + tab).classList.add('active');
346
- if (tab === 'reports') loadReports();
499
+ document
500
+ .querySelectorAll(".nav-btn")
501
+ .forEach((b) => b.classList.remove("active"));
502
+ document
503
+ .querySelectorAll(".tab-content")
504
+ .forEach((t) => t.classList.remove("active"));
505
+ btn.classList.add("active");
506
+ document.getElementById("tab-" + tab).classList.add("active");
507
+ if (tab === "reports") loadReports();
347
508
  });
348
509
  });
349
510
  }
@@ -352,14 +513,14 @@ function setupTabs() {
352
513
  function togglePre(preId, btnId) {
353
514
  const pre = document.getElementById(preId);
354
515
  const btn = document.getElementById(btnId);
355
- const shown = pre.style.display !== 'none';
356
- pre.style.display = shown ? 'none' : 'block';
357
- btn.textContent = shown ? 'diff 보기 ▾' : 'diff 닫기 ▴';
516
+ const shown = pre.style.display !== "none";
517
+ pre.style.display = shown ? "none" : "block";
518
+ btn.textContent = shown ? "diff 보기 ▾" : "diff 닫기 ▴";
358
519
  }
359
520
 
360
521
  function escHtml(str) {
361
522
  return String(str)
362
- .replace(/&/g, '&amp;')
363
- .replace(/</g, '&lt;')
364
- .replace(/>/g, '&gt;');
523
+ .replace(/&/g, "&amp;")
524
+ .replace(/</g, "&lt;")
525
+ .replace(/>/g, "&gt;");
365
526
  }