commit-ai-agent 1.0.0

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/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ GEMINI_API_KEY=your_gemini_api_key_here
2
+ PORT=3000
3
+ DEV_ROOT=C:/dev
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 commit-analyzer contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # ⚡ Commit Analyzer
2
+
3
+ AI가 git 커밋과 현재 변경사항을 한국어로 자동 분석해주는 개발자 도구입니다.
4
+
5
+ ## 요구 사항
6
+
7
+ - [Node.js](https://nodejs.org) 18 이상
8
+ - [Git](https://git-scm.com) 설치
9
+ - Google Gemini API 키 ([무료 발급](https://aistudio.google.com/apikey))
10
+
11
+ ---
12
+
13
+ ## 사용 방법
14
+
15
+ ### 방법 A — npx (설치 없이 바로 실행)
16
+
17
+ ```bash
18
+ npx commit-analyzer
19
+ ```
20
+
21
+ ### 방법 B — 전역 설치 후 명령어로 실행
22
+
23
+ ```bash
24
+ npm install -g commit-analyzer
25
+ commit-analyzer
26
+ ```
27
+
28
+ ### 방법 C — 직접 클론
29
+
30
+ ```bash
31
+ git clone https://github.com/사용자명/commit-analyzer.git
32
+ cd commit-analyzer
33
+ npm install
34
+ npm start
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 환경 설정
40
+
41
+ 처음 실행 전, 실행할 디렉토리에 `.env` 파일을 만드세요.
42
+
43
+ ```env
44
+ GEMINI_API_KEY=여기에_API_키_입력
45
+ DEV_ROOT=C:/Users/이름/dev
46
+ PORT=3000
47
+ ```
48
+
49
+ | 변수 | 설명 |
50
+ |------|------|
51
+ | `GEMINI_API_KEY` | [Google AI Studio](https://aistudio.google.com/apikey)에서 발급 (무료) |
52
+ | `DEV_ROOT` | 분석할 git 프로젝트들이 모여 있는 루트 폴더 |
53
+ | `PORT` | 서버 포트 (기본값 3000) |
54
+
55
+ > Windows 경로는 `\` 대신 `/` 또는 `\\` 사용: `C:/dev`, `D:/projects`
56
+
57
+ ---
58
+
59
+ ## 기능
60
+
61
+ | 모드 | 설명 |
62
+ |------|------|
63
+ | 📦 최근 커밋 분석 | 마지막 커밋의 의도·근거·코드 리뷰 자동 생성 |
64
+ | 🔍 현재 변경사항 분석 | staged/unstaged/untracked 분석 + 커밋 메시지 3가지 제안 |
65
+
66
+ - 분석 결과는 실행 위치의 `reports/` 폴더에 Markdown으로 저장됩니다.
67
+
68
+ ---
69
+
70
+ ## 문제 해결
71
+
72
+ **429 오류 (할당량 초과)**
73
+ → Gemini 무료 티어 한도 도달. 잠시 후 재시도하거나 [유료 플랜](https://ai.google.dev)으로 업그레이드하세요.
74
+
75
+ **프로젝트 목록이 안 뜸**
76
+ → `DEV_ROOT`가 git 저장소가 들어 있는 상위 폴더인지 확인하세요.
77
+
78
+ ---
79
+
80
+ ## 라이선스
81
+
82
+ [MIT](./LICENSE)
package/bin/cli.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * commit-analyzer CLI 진입점
4
+ * npx commit-analyzer 또는 npm install -g 후 commit-analyzer 명령으로 실행
5
+ */
6
+ import dotenv from 'dotenv';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { spawn } from 'child_process';
10
+
11
+ // 사용자 현재 디렉토리의 .env 로드
12
+ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
13
+
14
+ // 패키지 루트 경로를 환경변수로 전달 (server.js가 public/ 위치를 찾기 위함)
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ process.env.COMMIT_ANALYZER_ROOT = path.resolve(__dirname, '..');
17
+
18
+ const PORT = process.env.PORT || 3000;
19
+
20
+ console.log('');
21
+ console.log(' ⚡ Commit Analyzer');
22
+ console.log(` 🌐 http://localhost:${PORT}`);
23
+ console.log(' 종료: Ctrl+C');
24
+ console.log('');
25
+
26
+ // 브라우저 자동 오픈 (1초 지연 - 서버 준비 대기)
27
+ setTimeout(async () => {
28
+ const url = `http://localhost:${PORT}`;
29
+ const platform = process.platform;
30
+ const cmd = platform === 'win32' ? 'start' :
31
+ platform === 'darwin' ? 'open' : 'xdg-open';
32
+ spawn(cmd, [url], { shell: true, stdio: 'ignore', detached: true });
33
+ }, 1200);
34
+
35
+ // 서버 시작
36
+ await import('../src/server.js');
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "commit-ai-agent",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered git commit & working status analyzer with web UI",
5
+ "type": "module",
6
+ "main": "src/server.js",
7
+ "bin": {
8
+ "commit-analyzer": "./bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "public/",
14
+ ".env.example",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "start": "node src/server.js",
19
+ "dev": "node --watch src/server.js"
20
+ },
21
+ "keywords": [
22
+ "git",
23
+ "commit",
24
+ "ai",
25
+ "analyzer",
26
+ "gemini",
27
+ "code-review"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@google/generative-ai": "^0.21.0",
32
+ "express": "^4.18.2",
33
+ "simple-git": "^3.22.0",
34
+ "dotenv": "^16.4.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }
package/public/app.js ADDED
@@ -0,0 +1,363 @@
1
+ /* global marked */
2
+
3
+ // ── State ──
4
+ let selectedProject = null;
5
+ let isAnalyzing = false;
6
+ let analyzeMode = 'commit'; // 'commit' | 'status'
7
+
8
+ // ── Boot ──
9
+ document.addEventListener('DOMContentLoaded', () => {
10
+ init();
11
+ });
12
+
13
+ async function init() {
14
+ setupTabs();
15
+ setupModeToggle();
16
+ await checkConfig();
17
+ await loadProjects();
18
+
19
+ // 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');
28
+ });
29
+ document.getElementById('status-diff-toggle-btn').addEventListener('click', () => {
30
+ togglePre('status-diff-content', 'status-diff-toggle-btn');
31
+ });
32
+ }
33
+
34
+ // ── Config check ──
35
+ async function checkConfig() {
36
+ 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';
41
+ }
42
+ } catch {}
43
+ }
44
+
45
+ // ── Projects ──
46
+ 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>';
49
+ try {
50
+ const res = await fetch('/api/projects');
51
+ const { projects } = await res.json();
52
+ renderProjects(projects);
53
+ } catch (err) {
54
+ grid.innerHTML = `<p style="color:var(--danger);font-size:14px">프로젝트 목록을 불러오지 못했습니다: ${err.message}</p>`;
55
+ }
56
+ }
57
+
58
+ function renderProjects(projects) {
59
+ const grid = document.getElementById('project-grid');
60
+ if (!projects || projects.length === 0) {
61
+ grid.innerHTML = '<p style="color:var(--text3);font-size:14px">git 프로젝트가 없습니다.</p>';
62
+ return;
63
+ }
64
+ grid.innerHTML = projects.map(p => `
65
+ <div class="project-item" data-name="${p.name}">
66
+ <span class="proj-icon">${getProjectIcon(p.name)}</span>
67
+ <span class="proj-name">${p.name}</span>
68
+ </div>
69
+ `).join('');
70
+ grid.querySelectorAll('.project-item').forEach(el => {
71
+ el.addEventListener('click', () => selectProject(el));
72
+ });
73
+ }
74
+
75
+ 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 '📁';
83
+ }
84
+
85
+ // ── Project Selection ──
86
+ async function selectProject(el) {
87
+ document.querySelectorAll('.project-item').forEach(e => e.classList.remove('selected'));
88
+ el.classList.add('selected');
89
+ 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';
94
+
95
+ if (analyzeMode === 'commit') {
96
+ await fetchCommitPreview();
97
+ } else {
98
+ await fetchStatusPreview();
99
+ }
100
+ }
101
+
102
+ async function fetchCommitPreview() {
103
+ try {
104
+ const res = await fetch(`/api/projects/${encodeURIComponent(selectedProject)}/commit`);
105
+ const { commit, error } = await res.json();
106
+ if (error) throw new Error(error);
107
+ renderCommitCard(commit);
108
+ document.getElementById('commit-card').style.display = 'block';
109
+ } catch (e) {
110
+ console.warn('commit preview failed:', e.message);
111
+ }
112
+ }
113
+
114
+ async function fetchStatusPreview() {
115
+ try {
116
+ const res = await fetch(`/api/projects/${encodeURIComponent(selectedProject)}/status`);
117
+ const { status, error } = await res.json();
118
+ if (error) throw new Error(error);
119
+ if (status) {
120
+ renderStatusCard(status);
121
+ document.getElementById('status-card').style.display = 'block';
122
+ } else {
123
+ document.getElementById('selected-hint').textContent = `${selectedProject} — 변경사항 없음`;
124
+ }
125
+ } catch (e) {
126
+ console.warn('status preview failed:', e.message);
127
+ }
128
+ }
129
+
130
+ // ── Commit Card ──
131
+ function renderCommitCard(c) {
132
+ document.getElementById('commit-meta').innerHTML = `
133
+ <div class="meta-item"><div class="meta-label">해시</div><div class="meta-value hash">${c.shortHash}</div></div>
134
+ <div class="meta-item"><div class="meta-label">메시지</div><div class="meta-value">${escHtml(c.message)}</div></div>
135
+ <div class="meta-item"><div class="meta-label">작성자</div><div class="meta-value">${escHtml(c.author)}</div></div>
136
+ <div class="meta-item"><div class="meta-label">날짜</div><div class="meta-value">${escHtml(c.date)}</div></div>
137
+ `;
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 보기 ▾';
142
+ }
143
+
144
+ // ── Status Card ──
145
+ function renderStatusCard(s) {
146
+ 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 = `
154
+ <div class="meta-item" style="grid-column:1/-1">
155
+ <div class="meta-label">변경된 파일 (총 ${s.totalFiles}개)</div>
156
+ <div class="meta-value" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">${badges || '<span style="color:var(--text3)">변경사항 없음</span>'}</div>
157
+ </div>
158
+ `;
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 보기 ▾';
163
+ }
164
+
165
+ // ── Mode Toggle ──
166
+ function setupModeToggle() {
167
+ document.getElementById('mode-commit')?.addEventListener('click', () => switchMode('commit'));
168
+ document.getElementById('mode-status')?.addEventListener('click', () => switchMode('status'));
169
+ }
170
+
171
+ function switchMode(mode) {
172
+ 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';
178
+
179
+ if (!selectedProject) return;
180
+ if (mode === 'commit') fetchCommitPreview();
181
+ else fetchStatusPreview();
182
+ }
183
+
184
+ // ── Analyze button ──
185
+ function onAnalyzeClick() {
186
+ if (analyzeMode === 'commit') startAnalysis('/api/analyze');
187
+ else startAnalysis('/api/analyze-status');
188
+ }
189
+
190
+ // ── Generic SSE Analysis ──
191
+ async function startAnalysis(endpoint) {
192
+ if (isAnalyzing || !selectedProject) return;
193
+ isAnalyzing = true;
194
+
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
+ setStatus('loading', '분석 준비 중...');
205
+ analyzeBtn.disabled = true;
206
+ btnIcon.textContent = '⏳';
207
+
208
+ let fullText = '';
209
+
210
+ try {
211
+ const res = await fetch(endpoint, {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({ projectName: selectedProject }),
215
+ });
216
+
217
+ // 400 에러 처리 (API 키 없음 등)
218
+ if (!res.ok) {
219
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
220
+ setStatus('error', `오류: ${err.error}`);
221
+ return;
222
+ }
223
+
224
+ const reader = res.body.getReader();
225
+ const decoder = new TextDecoder();
226
+ let buffer = '';
227
+
228
+ while (true) {
229
+ const { done, value } = await reader.read();
230
+ if (done) break;
231
+ buffer += decoder.decode(value, { stream: true });
232
+ const lines = buffer.split('\n');
233
+ buffer = lines.pop();
234
+ for (const line of lines) {
235
+ if (!line.startsWith('data: ')) continue;
236
+ try {
237
+ const data = JSON.parse(line.slice(6));
238
+ if (data.type === 'status') {
239
+ setStatus('loading', data.message);
240
+ } else if (data.type === 'commit') {
241
+ renderCommitCard(data.commit);
242
+ document.getElementById('commit-card').style.display = 'block';
243
+ } else if (data.type === 'working-status') {
244
+ renderStatusCard(data.workingStatus);
245
+ document.getElementById('status-card').style.display = 'block';
246
+ } else if (data.type === 'analysis') {
247
+ fullText = data.analysis;
248
+ analysisBody.innerHTML = marked.parse(data.analysis);
249
+ if (data.reportFilename) {
250
+ reportSaved.textContent = `✓ 저장됨: ${data.reportFilename}`;
251
+ }
252
+ } else if (data.type === 'done') {
253
+ setStatus('done', '✅ 분석 완료!');
254
+ } else if (data.type === 'error') {
255
+ setStatus('error', `오류: ${data.message}`);
256
+ }
257
+ } catch {}
258
+ }
259
+ }
260
+ } catch (err) {
261
+ setStatus('error', `네트워크 오류: ${err.message}`);
262
+ } finally {
263
+ isAnalyzing = false;
264
+ analyzeBtn.disabled = false;
265
+ btnIcon.textContent = '🔍';
266
+ document.getElementById('copy-btn')._text = fullText;
267
+ resultCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
268
+ }
269
+ }
270
+
271
+ // ── Status bar ──
272
+ function setStatus(type, msg) {
273
+ const bar = document.getElementById('status-bar');
274
+ const dot = bar.querySelector('.status-dot');
275
+ const msgEl = document.getElementById('status-msg');
276
+ msgEl.textContent = msg;
277
+ dot.className = 'status-dot ' + type;
278
+ bar.className = 'status-bar ' + (type === 'loading' ? '' : type);
279
+ }
280
+
281
+ // ── Copy ──
282
+ function onCopy() {
283
+ const btn = document.getElementById('copy-btn');
284
+ const text = btn._text || document.getElementById('analysis-body').innerText;
285
+ navigator.clipboard.writeText(text).then(() => {
286
+ btn.textContent = '✓ 복사됨';
287
+ setTimeout(() => { btn.textContent = '📋 복사'; }, 2000);
288
+ });
289
+ }
290
+
291
+ // ── Reports Tab ──
292
+ async function loadReports() {
293
+ const listEl = document.getElementById('reports-list');
294
+ listEl.innerHTML = '<p class="empty-state">불러오는 중...</p>';
295
+ try {
296
+ const res = await fetch('/api/reports');
297
+ const { reports } = await res.json();
298
+ if (!reports.length) {
299
+ listEl.innerHTML = '<p class="empty-state">아직 저장된 리포트가 없습니다.</p>';
300
+ return;
301
+ }
302
+ listEl.innerHTML = reports.map(r => {
303
+ const parts = r.replace('.md', '').split('-');
304
+ const date = parts.slice(-2).join(' ');
305
+ const proj = parts.slice(0, -2).join('-');
306
+ return `<div class="report-item" data-file="${r}">
307
+ <span class="report-item-name">📄 ${proj}</span>
308
+ <span class="report-item-date">${date}</span>
309
+ </div>`;
310
+ }).join('');
311
+ listEl.querySelectorAll('.report-item').forEach(el => {
312
+ el.addEventListener('click', () => openReport(el.dataset.file));
313
+ });
314
+ } catch {
315
+ listEl.innerHTML = '<p class="empty-state">리포트를 불러오지 못했습니다.</p>';
316
+ }
317
+ }
318
+
319
+ async function openReport(filename) {
320
+ const viewer = document.getElementById('report-viewer');
321
+ const body = document.getElementById('report-viewer-body');
322
+ document.getElementById('report-viewer-title').textContent = filename.replace('.md', '');
323
+ body.innerHTML = '<p style="color:var(--text2)">불러오는 중...</p>';
324
+ viewer.style.display = 'block';
325
+ try {
326
+ const res = await fetch(`/api/reports/${encodeURIComponent(filename)}`);
327
+ const { content } = await res.json();
328
+ body.innerHTML = marked.parse(content);
329
+ } catch {
330
+ body.innerHTML = '<p style="color:var(--danger)">리포트를 불러오지 못했습니다.</p>';
331
+ }
332
+ viewer.scrollIntoView({ behavior: 'smooth' });
333
+ }
334
+
335
+ // ── Tabs ──
336
+ function setupTabs() {
337
+ document.querySelectorAll('.nav-btn').forEach(btn => {
338
+ btn.addEventListener('click', () => {
339
+ const tab = btn.dataset.tab;
340
+ document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
341
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
342
+ btn.classList.add('active');
343
+ document.getElementById('tab-' + tab).classList.add('active');
344
+ if (tab === 'reports') loadReports();
345
+ });
346
+ });
347
+ }
348
+
349
+ // ── Helpers ──
350
+ function togglePre(preId, btnId) {
351
+ const pre = document.getElementById(preId);
352
+ const btn = document.getElementById(btnId);
353
+ const shown = pre.style.display !== 'none';
354
+ pre.style.display = shown ? 'none' : 'block';
355
+ btn.textContent = shown ? 'diff 보기 ▾' : 'diff 닫기 ▴';
356
+ }
357
+
358
+ function escHtml(str) {
359
+ return String(str)
360
+ .replace(/&/g, '&amp;')
361
+ .replace(/</g, '&lt;')
362
+ .replace(/>/g, '&gt;');
363
+ }
@@ -0,0 +1,128 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Commit Analyzer — AI 커밋 분석기</title>
7
+ <meta name="description" content="AI 기반 git 커밋 문서화 및 코드 리뷰 에이전트" />
8
+ <meta name="theme-color" content="#6366f1" />
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
12
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
13
+ <link rel="stylesheet" href="/style.css" />
14
+ </head>
15
+ <body>
16
+ <!-- ── Header ── -->
17
+ <header class="header">
18
+ <div class="header-inner">
19
+ <div class="logo">
20
+ <span class="logo-icon">⚡</span>
21
+ <span class="logo-text">Commit Analyzer</span>
22
+ <span class="logo-badge">AI</span>
23
+ </div>
24
+ <nav class="header-nav">
25
+ <button class="nav-btn active" data-tab="analyze" id="btn-analyze">분석하기</button>
26
+ <button class="nav-btn" data-tab="reports" id="btn-reports">리포트</button>
27
+ </nav>
28
+ </div>
29
+ </header>
30
+
31
+ <!-- ── Main ── -->
32
+ <main class="main">
33
+
34
+ <!-- ── Tab: Analyze ── -->
35
+ <section class="tab-content active" id="tab-analyze">
36
+
37
+ <!-- API Key Warning -->
38
+ <div class="alert alert-warn" id="api-key-warn" style="display:none">
39
+ ⚠️ <strong>.env</strong> 파일에 <code>GEMINI_API_KEY</code>가 설정되지 않았습니다.
40
+ <code>c:\dev\commit-analyzer\.env</code> 파일을 생성하고 키를 입력해 주세요.
41
+ </div>
42
+
43
+ <!-- Project Selector Card -->
44
+ <div class="card selector-card">
45
+ <div class="card-header">
46
+ <h2 class="card-title">🗂 프로젝트 선택</h2>
47
+ <button class="refresh-btn" id="refresh-projects" title="새로고침">↻</button>
48
+ </div>
49
+ <!-- Mode Toggle -->
50
+ <div class="mode-toggle">
51
+ <button class="mode-btn active" id="mode-commit" data-mode="commit">📦 최근 커밋 분석</button>
52
+ <button class="mode-btn" id="mode-status" data-mode="status">🔍 현재 변경사항 분석</button>
53
+ </div>
54
+ <div class="project-grid" id="project-grid">
55
+ <div class="skeleton-grid">
56
+ <div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
57
+ </div>
58
+ </div>
59
+ <div class="card-footer">
60
+ <span class="selected-hint" id="selected-hint">프로젝트를 선택해 주세요</span>
61
+ <button class="btn-primary" id="analyze-btn" disabled>
62
+ <span class="btn-icon">🔍</span> 커밋 분석하기
63
+ </button>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Working Status Card (hidden until project selected in status mode) -->
68
+ <div class="card commit-card" id="status-card" style="display:none">
69
+ <h2 class="card-title">🔧 현재 변경사항 (미커밋)</h2>
70
+ <div class="commit-meta" id="status-meta"></div>
71
+ <div class="diff-toggle">
72
+ <button class="btn-ghost" id="status-diff-toggle-btn">diff 보기 ▾</button>
73
+ <pre class="diff-content" id="status-diff-content" style="display:none"></pre>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Commit Info Card (hidden until project selected) -->
78
+ <div class="card commit-card" id="commit-card" style="display:none">
79
+ <h2 class="card-title">📦 최근 커밋</h2>
80
+ <div class="commit-meta" id="commit-meta"></div>
81
+ <div class="diff-toggle">
82
+ <button class="btn-ghost" id="diff-toggle-btn">diff 보기 ▾</button>
83
+ <pre class="diff-content" id="diff-content" style="display:none"></pre>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Analysis Output -->
88
+ <div class="card result-card" id="result-card" style="display:none">
89
+ <div class="result-header">
90
+ <h2 class="card-title">📝 AI 분석 결과</h2>
91
+ <div class="result-actions">
92
+ <button class="btn-ghost" id="copy-btn">📋 복사</button>
93
+ <span class="report-saved" id="report-saved"></span>
94
+ </div>
95
+ </div>
96
+ <!-- Status bar -->
97
+ <div class="status-bar" id="status-bar">
98
+ <span class="status-dot loading"></span>
99
+ <span id="status-msg">분석 준비 중...</span>
100
+ </div>
101
+ <!-- Streaming output -->
102
+ <div class="analysis-body" id="analysis-body"></div>
103
+ </div>
104
+
105
+ </section>
106
+
107
+ <!-- ── Tab: Reports ── -->
108
+ <section class="tab-content" id="tab-reports">
109
+ <div class="card">
110
+ <h2 class="card-title">📁 저장된 리포트</h2>
111
+ <div id="reports-list" class="reports-list">
112
+ <p class="empty-state">아직 저장된 리포트가 없습니다.</p>
113
+ </div>
114
+ </div>
115
+ <div class="card" id="report-viewer" style="display:none">
116
+ <div class="result-header">
117
+ <h2 class="card-title" id="report-viewer-title">리포트</h2>
118
+ <button class="btn-ghost" id="close-report-btn">✕ 닫기</button>
119
+ </div>
120
+ <div class="analysis-body" id="report-viewer-body"></div>
121
+ </div>
122
+ </section>
123
+
124
+ </main>
125
+
126
+ <script src="/app.js"></script>
127
+ </body>
128
+ </html>