commit-ai-agent 1.0.6 → 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,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,81 @@ 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';
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";
77
91
  });
78
- document.getElementById('diff-toggle-btn').addEventListener('click', () => {
79
- togglePre('diff-content', 'diff-toggle-btn');
92
+ document.getElementById("diff-toggle-btn").addEventListener("click", () => {
93
+ togglePre("diff-content", "diff-toggle-btn");
80
94
  });
81
- document.getElementById('status-diff-toggle-btn').addEventListener('click', () => {
82
- togglePre('status-diff-content', 'status-diff-toggle-btn');
83
- });
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 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
+
99
141
  // ── Projects ──
100
142
  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>';
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>';
103
146
  try {
104
- const res = await fetch('/api/projects');
147
+ const res = await fetch("/api/projects");
105
148
  const { projects } = await res.json();
106
149
  renderProjects(projects);
107
150
  } catch (err) {
@@ -110,43 +153,51 @@ async function loadProjects() {
110
153
  }
111
154
 
112
155
  function renderProjects(projects) {
113
- const grid = document.getElementById('project-grid');
156
+ const grid = document.getElementById("project-grid");
114
157
  if (!projects || projects.length === 0) {
115
- 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>';
116
160
  return;
117
161
  }
118
- grid.innerHTML = projects.map(p => `
162
+ grid.innerHTML = projects
163
+ .map(
164
+ (p) => `
119
165
  <div class="project-item" data-name="${p.name}">
120
166
  <span class="proj-icon">${getProjectIcon(p.name)}</span>
121
167
  <span class="proj-name">${p.name}</span>
122
168
  </div>
123
- `).join('');
124
- grid.querySelectorAll('.project-item').forEach(el => {
125
- el.addEventListener('click', () => selectProject(el));
169
+ `,
170
+ )
171
+ .join("");
172
+ grid.querySelectorAll(".project-item").forEach((el) => {
173
+ el.addEventListener("click", () => selectProject(el));
126
174
  });
127
175
  }
128
176
 
129
177
  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 '📁';
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 "📁";
137
185
  }
138
186
 
139
187
  // ── Project Selection ──
140
188
  async function selectProject(el) {
141
- document.querySelectorAll('.project-item').forEach(e => e.classList.remove('selected'));
142
- el.classList.add('selected');
189
+ document
190
+ .querySelectorAll(".project-item")
191
+ .forEach((e) => e.classList.remove("selected"));
192
+ el.classList.add("selected");
143
193
  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';
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";
148
199
 
149
- if (analyzeMode === 'commit') {
200
+ if (analyzeMode === "commit") {
150
201
  await fetchCommitPreview();
151
202
  } else {
152
203
  await fetchStatusPreview();
@@ -154,99 +205,127 @@ async function selectProject(el) {
154
205
  }
155
206
 
156
207
  async function fetchCommitPreview() {
208
+ const displayName = isSingleProject ? singleProjectName : selectedProject;
157
209
  try {
158
- const res = await fetch(`/api/projects/${encodeURIComponent(selectedProject)}/commit`);
210
+ const res = await fetch(
211
+ `/api/projects/${encodeURIComponent(selectedProject)}/commit`,
212
+ );
159
213
  const { commit, error } = await res.json();
160
214
  if (error) throw new Error(error);
161
215
  renderCommitCard(commit);
162
- document.getElementById('commit-card').style.display = 'block';
163
- setAriaState('ready-commit', { project: selectedProject });
216
+ document.getElementById("commit-card").style.display = "block";
217
+ setAriaState("ready-commit", { project: displayName });
164
218
  } catch (e) {
165
- console.warn('commit preview failed:', e.message);
166
- setAriaState('ready-commit', { project: selectedProject });
219
+ console.warn("commit preview failed:", e.message);
220
+ setAriaState("ready-commit", { project: displayName });
167
221
  }
168
222
  }
169
223
 
170
224
  async function fetchStatusPreview() {
225
+ const displayName = isSingleProject ? singleProjectName : selectedProject;
171
226
  try {
172
- const res = await fetch(`/api/projects/${encodeURIComponent(selectedProject)}/status`);
227
+ const res = await fetch(
228
+ `/api/projects/${encodeURIComponent(selectedProject)}/status`,
229
+ );
173
230
  const { status, error } = await res.json();
174
231
  if (error) throw new Error(error);
175
232
  if (status) {
176
233
  renderStatusCard(status);
177
- document.getElementById('status-card').style.display = 'block';
178
- setAriaState('ready-status', { project: selectedProject, n: status.totalFiles });
234
+ document.getElementById("status-card").style.display = "block";
235
+ setAriaState("ready-status", {
236
+ project: displayName,
237
+ n: status.totalFiles,
238
+ });
179
239
  } else {
180
- document.getElementById('selected-hint').textContent = `${selectedProject} — 변경사항 없음`;
181
- setAriaState('ready-status', { project: selectedProject, n: 0 });
240
+ const hint = document.getElementById("selected-hint");
241
+ if (hint) hint.textContent = `${displayName} 변경사항 없음`;
242
+ setAriaState("ready-status", { project: displayName, n: 0 });
182
243
  }
183
244
  } catch (e) {
184
- console.warn('status preview failed:', e.message);
185
- setAriaState('ready-status', { project: selectedProject, n: 0 });
245
+ console.warn("status preview failed:", e.message);
246
+ setAriaState("ready-status", { project: displayName, n: 0 });
186
247
  }
187
248
  }
188
249
 
189
250
  // ── Commit Card ──
190
251
  function renderCommitCard(c) {
191
- document.getElementById('commit-meta').innerHTML = `
252
+ document.getElementById("commit-meta").innerHTML = `
192
253
  <div class="meta-item"><div class="meta-label">해시</div><div class="meta-value hash">${c.shortHash}</div></div>
193
254
  <div class="meta-item"><div class="meta-label">메시지</div><div class="meta-value">${escHtml(c.message)}</div></div>
194
255
  <div class="meta-item"><div class="meta-label">작성자</div><div class="meta-value">${escHtml(c.author)}</div></div>
195
256
  <div class="meta-item"><div class="meta-label">날짜</div><div class="meta-value">${escHtml(c.date)}</div></div>
196
257
  `;
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 보기 ▾';
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 보기 ▾";
201
262
  }
202
263
 
203
264
  // ── Status Card ──
204
265
  function renderStatusCard(s) {
205
266
  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 = `
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 = `
213
284
  <div class="meta-item" style="grid-column:1/-1">
214
285
  <div class="meta-label">변경된 파일 (총 ${s.totalFiles}개)</div>
215
286
  <div class="meta-value" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">${badges || '<span style="color:var(--text3)">변경사항 없음</span>'}</div>
216
287
  </div>
217
288
  `;
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 보기 ▾';
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 보기 ▾";
222
293
  }
223
294
 
224
295
  // ── Mode Toggle ──
225
296
  function setupModeToggle() {
226
- document.getElementById('mode-commit')?.addEventListener('click', () => switchMode('commit'));
227
- 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"));
228
303
  }
229
304
 
230
305
  function switchMode(mode) {
231
306
  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에게 리뷰 요청';
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에게 리뷰 요청";
240
319
 
241
320
  if (!selectedProject) return;
242
- if (mode === 'commit') fetchCommitPreview();
321
+ if (mode === "commit") fetchCommitPreview();
243
322
  else fetchStatusPreview();
244
323
  }
245
324
 
246
325
  // ── Analyze button ──
247
326
  function onAnalyzeClick() {
248
- if (analyzeMode === 'commit') startAnalysis('/api/analyze');
249
- else startAnalysis('/api/analyze-status');
327
+ if (analyzeMode === "commit") startAnalysis("/api/analyze");
328
+ else startAnalysis("/api/analyze-status");
250
329
  }
251
330
 
252
331
  // ── Generic SSE Analysis ──
@@ -254,162 +333,178 @@ async function startAnalysis(endpoint) {
254
333
  if (isAnalyzing || !selectedProject) return;
255
334
  isAnalyzing = true;
256
335
 
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');
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");
269
348
  analyzeBtn.disabled = true;
270
- btnIcon.textContent = '';
349
+ btnIcon.textContent = "";
271
350
 
272
- let fullText = '';
351
+ let fullText = "";
273
352
 
274
353
  try {
275
354
  const res = await fetch(endpoint, {
276
- method: 'POST',
277
- headers: { 'Content-Type': 'application/json' },
355
+ method: "POST",
356
+ headers: { "Content-Type": "application/json" },
278
357
  body: JSON.stringify({ projectName: selectedProject }),
279
358
  });
280
359
 
281
360
  // 400 에러 처리 (API 키 없음 등)
282
361
  if (!res.ok) {
283
- const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
284
- setStatus('error', `오류: ${err.error}`);
362
+ const err = await res
363
+ .json()
364
+ .catch(() => ({ error: `HTTP ${res.status}` }));
365
+ setStatus("error", `오류: ${err.error}`);
285
366
  return;
286
367
  }
287
368
 
288
369
  const reader = res.body.getReader();
289
370
  const decoder = new TextDecoder();
290
- let buffer = '';
371
+ let buffer = "";
291
372
 
292
373
  while (true) {
293
374
  const { done, value } = await reader.read();
294
375
  if (done) break;
295
376
  buffer += decoder.decode(value, { stream: true });
296
- const lines = buffer.split('\n');
377
+ const lines = buffer.split("\n");
297
378
  buffer = lines.pop();
298
379
  for (const line of lines) {
299
- if (!line.startsWith('data: ')) continue;
380
+ if (!line.startsWith("data: ")) continue;
300
381
  try {
301
382
  const data = JSON.parse(line.slice(6));
302
- if (data.type === 'status') {
303
- setStatus('loading', data.message);
304
- } else if (data.type === 'commit') {
383
+ if (data.type === "status") {
384
+ setStatus("loading", data.message);
385
+ } else if (data.type === "commit") {
305
386
  renderCommitCard(data.commit);
306
- document.getElementById('commit-card').style.display = 'block';
307
- } else if (data.type === 'working-status') {
387
+ document.getElementById("commit-card").style.display = "block";
388
+ } else if (data.type === "working-status") {
308
389
  renderStatusCard(data.workingStatus);
309
- document.getElementById('status-card').style.display = 'block';
310
- } else if (data.type === 'analysis') {
390
+ document.getElementById("status-card").style.display = "block";
391
+ } else if (data.type === "analysis") {
311
392
  fullText = data.analysis;
312
393
  analysisBody.innerHTML = marked.parse(data.analysis);
313
394
  if (data.reportFilename) {
314
395
  reportSaved.textContent = `✓ 저장됨: ${data.reportFilename}`;
315
396
  }
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');
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");
323
404
  }
324
405
  } catch {}
325
406
  }
326
407
  }
327
408
  } catch (err) {
328
- setStatus('error', `네트워크 오류: ${err.message}`);
329
- setAriaState('error');
409
+ setStatus("error", `네트워크 오류: ${err.message}`);
410
+ setAriaState("error");
330
411
  } finally {
331
412
  isAnalyzing = false;
332
413
  analyzeBtn.disabled = false;
333
- btnIcon.textContent = '🤖';
334
- document.getElementById('copy-btn')._text = fullText;
335
- resultCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
414
+ btnIcon.textContent = "🤖";
415
+ document.getElementById("copy-btn")._text = fullText;
416
+ resultCard.scrollIntoView({ behavior: "smooth", block: "start" });
336
417
  }
337
418
  }
338
419
 
339
420
  // ── Status bar ──
340
421
  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');
422
+ const bar = document.getElementById("status-bar");
423
+ const dot = bar.querySelector(".status-dot");
424
+ const msgEl = document.getElementById("status-msg");
344
425
  msgEl.textContent = msg;
345
- dot.className = 'status-dot ' + type;
346
- bar.className = 'status-bar ' + (type === 'loading' ? '' : type);
426
+ dot.className = "status-dot " + type;
427
+ bar.className = "status-bar " + (type === "loading" ? "" : type);
347
428
  }
348
429
 
349
430
  // ── Copy ──
350
431
  function onCopy() {
351
- const btn = document.getElementById('copy-btn');
352
- 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;
353
434
  navigator.clipboard.writeText(text).then(() => {
354
- btn.textContent = '✓ 복사됨';
355
- setTimeout(() => { btn.textContent = '📋 복사'; }, 2000);
435
+ btn.textContent = "✓ 복사됨";
436
+ setTimeout(() => {
437
+ btn.textContent = "📋 복사";
438
+ }, 2000);
356
439
  });
357
440
  }
358
441
 
359
442
  // ── Reports Tab ──
360
443
  async function loadReports() {
361
- const listEl = document.getElementById('reports-list');
444
+ const listEl = document.getElementById("reports-list");
362
445
  listEl.innerHTML = '<p class="empty-state">불러오는 중...</p>';
363
446
  try {
364
- const res = await fetch('/api/reports');
447
+ const res = await fetch("/api/reports");
365
448
  const { reports } = await res.json();
366
449
  if (!reports.length) {
367
- listEl.innerHTML = '<p class="empty-state">아직 저장된 리포트가 없습니다.</p>';
450
+ listEl.innerHTML =
451
+ '<p class="empty-state">아직 저장된 리포트가 없습니다.</p>';
368
452
  return;
369
453
  }
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}">
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}">
375
460
  <span class="report-item-name">📄 ${proj}</span>
376
461
  <span class="report-item-date">${date}</span>
377
462
  </div>`;
378
- }).join('');
379
- listEl.querySelectorAll('.report-item').forEach(el => {
380
- 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));
381
467
  });
382
468
  } catch {
383
- listEl.innerHTML = '<p class="empty-state">리포트를 불러오지 못했습니다.</p>';
469
+ listEl.innerHTML =
470
+ '<p class="empty-state">리포트를 불러오지 못했습니다.</p>';
384
471
  }
385
472
  }
386
473
 
387
474
  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', '');
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
+ );
391
481
  body.innerHTML = '<p style="color:var(--text2)">불러오는 중...</p>';
392
- viewer.style.display = 'block';
482
+ viewer.style.display = "block";
393
483
  try {
394
484
  const res = await fetch(`/api/reports/${encodeURIComponent(filename)}`);
395
485
  const { content } = await res.json();
396
486
  body.innerHTML = marked.parse(content);
397
487
  } catch {
398
- body.innerHTML = '<p style="color:var(--danger)">리포트를 불러오지 못했습니다.</p>';
488
+ body.innerHTML =
489
+ '<p style="color:var(--danger)">리포트를 불러오지 못했습니다.</p>';
399
490
  }
400
- viewer.scrollIntoView({ behavior: 'smooth' });
491
+ viewer.scrollIntoView({ behavior: "smooth" });
401
492
  }
402
493
 
403
494
  // ── Tabs ──
404
495
  function setupTabs() {
405
- document.querySelectorAll('.nav-btn').forEach(btn => {
406
- btn.addEventListener('click', () => {
496
+ document.querySelectorAll(".nav-btn").forEach((btn) => {
497
+ btn.addEventListener("click", () => {
407
498
  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();
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();
413
508
  });
414
509
  });
415
510
  }
@@ -418,14 +513,14 @@ function setupTabs() {
418
513
  function togglePre(preId, btnId) {
419
514
  const pre = document.getElementById(preId);
420
515
  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 닫기 ▴';
516
+ const shown = pre.style.display !== "none";
517
+ pre.style.display = shown ? "none" : "block";
518
+ btn.textContent = shown ? "diff 보기 ▾" : "diff 닫기 ▴";
424
519
  }
425
520
 
426
521
  function escHtml(str) {
427
522
  return String(str)
428
- .replace(/&/g, '&amp;')
429
- .replace(/</g, '&lt;')
430
- .replace(/>/g, '&gt;');
523
+ .replace(/&/g, "&amp;")
524
+ .replace(/</g, "&lt;")
525
+ .replace(/>/g, "&gt;");
431
526
  }