commit-ai-agent 1.0.6 → 1.0.8

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