idea-manager 0.1.2 → 0.3.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.
Files changed (63) hide show
  1. package/README.md +19 -10
  2. package/next.config.ts +0 -1
  3. package/package.json +2 -2
  4. package/public/favicon.svg +10 -0
  5. package/public/icon.svg +2 -11
  6. package/src/app/api/filesystem/route.ts +49 -0
  7. package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
  8. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
  9. package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
  10. package/src/app/api/projects/[id]/items/route.ts +51 -1
  11. package/src/app/api/projects/[id]/scan/route.ts +73 -0
  12. package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
  13. package/src/app/api/projects/[id]/structure/route.ts +34 -3
  14. package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
  15. package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
  16. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
  17. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
  18. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
  19. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
  20. package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
  21. package/src/app/api/projects/route.ts +1 -1
  22. package/src/app/globals.css +465 -5
  23. package/src/app/layout.tsx +3 -0
  24. package/src/app/page.tsx +260 -88
  25. package/src/app/projects/[id]/page.tsx +366 -183
  26. package/src/cli.ts +44 -12
  27. package/src/components/DirectoryPicker.tsx +137 -0
  28. package/src/components/ScanPanel.tsx +743 -0
  29. package/src/components/brainstorm/Editor.tsx +20 -4
  30. package/src/components/brainstorm/MemoPin.tsx +91 -5
  31. package/src/components/dashboard/SubProjectCard.tsx +76 -0
  32. package/src/components/dashboard/TabBar.tsx +42 -0
  33. package/src/components/task/ProjectTree.tsx +223 -0
  34. package/src/components/task/PromptEditor.tsx +107 -0
  35. package/src/components/task/StatusFlow.tsx +43 -0
  36. package/src/components/task/TaskChat.tsx +134 -0
  37. package/src/components/task/TaskDetail.tsx +205 -0
  38. package/src/components/task/TaskList.tsx +119 -0
  39. package/src/components/tree/CardView.tsx +206 -0
  40. package/src/components/tree/RefinePopover.tsx +157 -0
  41. package/src/components/tree/TreeNode.tsx +147 -38
  42. package/src/components/tree/TreeView.tsx +270 -26
  43. package/src/components/ui/ConfirmDialog.tsx +88 -0
  44. package/src/lib/ai/chat-responder.ts +4 -2
  45. package/src/lib/ai/cleanup.ts +87 -0
  46. package/src/lib/ai/client.ts +175 -58
  47. package/src/lib/ai/prompter.ts +19 -24
  48. package/src/lib/ai/refiner.ts +128 -0
  49. package/src/lib/ai/structurer.ts +340 -11
  50. package/src/lib/db/queries/context.ts +76 -0
  51. package/src/lib/db/queries/items.ts +133 -12
  52. package/src/lib/db/queries/projects.ts +12 -8
  53. package/src/lib/db/queries/sub-projects.ts +122 -0
  54. package/src/lib/db/queries/task-conversations.ts +27 -0
  55. package/src/lib/db/queries/task-prompts.ts +32 -0
  56. package/src/lib/db/queries/tasks.ts +133 -0
  57. package/src/lib/db/schema.ts +75 -0
  58. package/src/lib/mcp/server.ts +38 -39
  59. package/src/lib/mcp/tools.ts +47 -45
  60. package/src/lib/scanner.ts +573 -0
  61. package/src/lib/task-store.ts +97 -0
  62. package/src/types/index.ts +65 -0
  63. package/src/app/icon.svg +0 -19
package/README.md CHANGED
@@ -38,33 +38,36 @@ AI가 브레인스토밍 텍스트를 분석하여 계층형 작업 트리로
38
38
 
39
39
  내장된 MCP Server를 통해 Claude 등 AI 에이전트가 작업을 조회하고, 상태를 업데이트하며, 순차적으로 실행할 수 있습니다.
40
40
 
41
- ## 설치 및 사용
41
+ ## 설치
42
42
 
43
43
  ```bash
44
44
  npm install -g idea-manager
45
45
  ```
46
46
 
47
- ### 웹 UI
47
+ ## 사용법
48
48
 
49
- ```bash
50
- # 개발 서버
51
- npm run dev
49
+ ### 웹 UI 실행
52
50
 
53
- # 또는 CLI로 실행
51
+ ```bash
54
52
  im start
55
53
  ```
56
54
 
57
- `http://localhost:3456` 에서 웹 UI 접속합니다.
55
+ 자동으로 `http://localhost:3456`에서 웹 UI 열립니다.
56
+
57
+ ```bash
58
+ # 포트 변경
59
+ im start -p 4000
60
+ ```
58
61
 
59
- ### MCP Server
62
+ ### MCP Server 실행
60
63
 
61
64
  ```bash
62
65
  im mcp
63
66
  ```
64
67
 
65
- Claude Desktop 등에서 MCP Server로 연결하여 사용할 수 있습니다.
68
+ Claude Desktop, Claude Code 등에서 MCP Server로 연결하여 AI 에이전트가 작업을 자율 실행할 수 있습니다.
66
69
 
67
- #### claude_desktop_config.json 설정 예시
70
+ #### Claude Desktop 설정 (claude_desktop_config.json)
68
71
 
69
72
  ```json
70
73
  {
@@ -77,6 +80,12 @@ Claude Desktop 등에서 MCP Server로 연결하여 사용할 수 있습니다.
77
80
  }
78
81
  ```
79
82
 
83
+ #### Claude Code 설정
84
+
85
+ ```bash
86
+ claude mcp add idea-manager -- npx -y idea-manager mcp
87
+ ```
88
+
80
89
  ### MCP 제공 도구
81
90
 
82
91
  | 도구 | 설명 |
package/next.config.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
3
  const nextConfig: NextConfig = {
4
- output: 'standalone',
5
4
  serverExternalPackages: ['better-sqlite3'],
6
5
  };
7
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "idea-manager",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
5
5
  "keywords": [
6
6
  "brainstorm",
@@ -37,7 +37,7 @@
37
37
  "mcp": "tsx src/cli.ts mcp"
38
38
  },
39
39
  "dependencies": {
40
- "@anthropic-ai/claude-agent-sdk": "^0.2.66",
40
+ "@anthropic-ai/sdk": "^0.78.0",
41
41
  "@modelcontextprotocol/sdk": "^1.27.1",
42
42
  "better-sqlite3": "^12.6.2",
43
43
  "commander": "^14.0.3",
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#6366f1"/>
5
+ <stop offset="100%" stop-color="#8b5cf6"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="512" height="512" rx="96" fill="url(#bg)"/>
9
+ <text x="256" y="390" font-family="Georgia, 'Times New Roman', serif" font-size="420" font-weight="700" fill="white" text-anchor="middle">I</text>
10
+ </svg>
package/public/icon.svg CHANGED
@@ -5,15 +5,6 @@
5
5
  <stop offset="100%" stop-color="#8b5cf6"/>
6
6
  </linearGradient>
7
7
  </defs>
8
- <rect width="512" height="512" rx="96" fill="url(#bg)"/>
9
- <text x="256" y="310" font-family="system-ui, -apple-system, sans-serif" font-size="260" font-weight="800" fill="white" text-anchor="middle" letter-spacing="-12">IM</text>
10
- <circle cx="390" cy="130" r="28" fill="#fbbf24"/>
11
- <line x1="390" y1="90" x2="390" y2="70" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
12
- <line x1="390" y1="170" x2="390" y2="190" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
13
- <line x1="350" y1="130" x2="330" y2="130" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
14
- <line x1="430" y1="130" x2="450" y2="130" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
15
- <line x1="362" y1="102" x2="348" y2="88" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
16
- <line x1="418" y1="158" x2="432" y2="172" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
17
- <line x1="418" y1="102" x2="432" y2="88" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
18
- <line x1="362" y1="158" x2="348" y2="172" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
8
+ <rect width="512" height="512" rx="108" fill="url(#bg)"/>
9
+ <text x="256" y="390" font-family="system-ui, -apple-system, sans-serif" font-size="420" font-weight="900" fill="white" text-anchor="middle">I</text>
19
10
  </svg>
@@ -0,0 +1,49 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const IGNORED = new Set([
7
+ 'node_modules', '.git', '.next', 'dist', 'build', '__pycache__',
8
+ '.cache', '.tmp', '.DS_Store', 'coverage', '.turbo', '.vercel',
9
+ ]);
10
+
11
+ export async function GET(request: NextRequest) {
12
+ const dirPath = request.nextUrl.searchParams.get('path') || os.homedir();
13
+
14
+ try {
15
+ const resolved = path.resolve(dirPath);
16
+
17
+ if (!fs.existsSync(resolved)) {
18
+ return NextResponse.json({ error: '경로가 존재하지 않습니다' }, { status: 404 });
19
+ }
20
+
21
+ const stat = fs.statSync(resolved);
22
+ if (!stat.isDirectory()) {
23
+ return NextResponse.json({ error: '디렉토리가 아닙니다' }, { status: 400 });
24
+ }
25
+
26
+ const entries = fs.readdirSync(resolved, { withFileTypes: true });
27
+ const dirs = entries
28
+ .filter(e => e.isDirectory() && !e.name.startsWith('.') && !IGNORED.has(e.name))
29
+ .map(e => ({
30
+ name: e.name,
31
+ path: path.join(resolved, e.name),
32
+ }))
33
+ .sort((a, b) => a.name.localeCompare(b.name));
34
+
35
+ // Check for project markers
36
+ const hasPackageJson = fs.existsSync(path.join(resolved, 'package.json'));
37
+ const hasReadme = fs.existsSync(path.join(resolved, 'README.md'));
38
+ const hasGit = fs.existsSync(path.join(resolved, '.git'));
39
+
40
+ return NextResponse.json({
41
+ current: resolved,
42
+ parent: path.dirname(resolved) !== resolved ? path.dirname(resolved) : null,
43
+ dirs,
44
+ isProject: hasPackageJson || hasReadme || hasGit,
45
+ });
46
+ } catch {
47
+ return NextResponse.json({ error: '디렉토리를 읽을 수 없습니다' }, { status: 500 });
48
+ }
49
+ }
@@ -0,0 +1,32 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getProject } from '@/lib/db/queries/projects';
3
+ import { getItemTree } from '@/lib/db/queries/items';
4
+ import { getBrainstorm } from '@/lib/db/queries/brainstorms';
5
+ import { cleanupItems } from '@/lib/ai/cleanup';
6
+
7
+ export async function POST(
8
+ _request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> },
10
+ ) {
11
+ const { id } = await params;
12
+ const project = getProject(id);
13
+ if (!project) {
14
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
15
+ }
16
+
17
+ const items = getItemTree(id);
18
+ if (items.length === 0) {
19
+ return NextResponse.json({ items: [], changed: false });
20
+ }
21
+
22
+ const brainstorm = getBrainstorm(id);
23
+ const brainstormContent = brainstorm?.content || '';
24
+
25
+ try {
26
+ const result = await cleanupItems(id, brainstorm?.id || '', items, brainstormContent);
27
+ return NextResponse.json(result);
28
+ } catch (error) {
29
+ const message = error instanceof Error ? error.message : 'Cleanup failed';
30
+ return NextResponse.json({ error: message }, { status: 500 });
31
+ }
32
+ }
@@ -0,0 +1,36 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getItem } from '@/lib/db/queries/items';
3
+ import { getProject } from '@/lib/db/queries/projects';
4
+ import { refineItem } from '@/lib/ai/refiner';
5
+
6
+ export async function POST(
7
+ request: NextRequest,
8
+ { params }: { params: Promise<{ id: string; itemId: string }> },
9
+ ) {
10
+ const { id: projectId, itemId } = await params;
11
+
12
+ const project = getProject(projectId);
13
+ if (!project) {
14
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
15
+ }
16
+
17
+ const item = getItem(itemId);
18
+ if (!item || item.project_id !== projectId) {
19
+ return NextResponse.json({ error: 'Item not found' }, { status: 404 });
20
+ }
21
+
22
+ const body = await request.json();
23
+ const { message } = body;
24
+
25
+ if (!message || typeof message !== 'string' || !message.trim()) {
26
+ return NextResponse.json({ error: 'Message is required' }, { status: 400 });
27
+ }
28
+
29
+ try {
30
+ const result = await refineItem(item, message.trim());
31
+ return NextResponse.json(result);
32
+ } catch (error) {
33
+ const msg = error instanceof Error ? error.message : 'Refine failed';
34
+ return NextResponse.json({ error: msg }, { status: 500 });
35
+ }
36
+ }
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getDb } from '@/lib/db/index';
3
- import { updateItem } from '@/lib/db/queries/items';
3
+ import { updateItem, deleteItem } from '@/lib/db/queries/items';
4
4
  import type { IItem, ItemStatus } from '@/types';
5
5
 
6
6
  export async function GET(
@@ -48,6 +48,10 @@ export async function PUT(
48
48
  }
49
49
  }
50
50
 
51
+ if (body.is_pinned !== undefined) {
52
+ updates.is_pinned = Boolean(body.is_pinned);
53
+ }
54
+
51
55
  if (body.title !== undefined) updates.title = body.title;
52
56
  if (body.description !== undefined) updates.description = body.description;
53
57
  if (body.priority !== undefined) updates.priority = body.priority;
@@ -61,6 +65,24 @@ export async function PUT(
61
65
  return NextResponse.json(updated);
62
66
  }
63
67
 
68
+ export async function DELETE(
69
+ _request: NextRequest,
70
+ { params }: { params: Promise<{ id: string; itemId: string }> },
71
+ ) {
72
+ const { id: projectId, itemId } = await params;
73
+ const db = getDb();
74
+
75
+ const item = db.prepare('SELECT * FROM items WHERE id = ? AND project_id = ?')
76
+ .get(itemId, projectId) as IItem | undefined;
77
+
78
+ if (!item) {
79
+ return NextResponse.json({ error: 'Item not found' }, { status: 404 });
80
+ }
81
+
82
+ deleteItem(itemId);
83
+ return NextResponse.json({ success: true });
84
+ }
85
+
64
86
  function lockChildrenRecursive(db: ReturnType<typeof getDb>, parentId: string, locked: boolean) {
65
87
  const now = new Date().toISOString();
66
88
  const children = db.prepare('SELECT id FROM items WHERE parent_id = ?').all(parentId) as { id: string }[];
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getProject } from '@/lib/db/queries/projects';
3
- import { getItemTree } from '@/lib/db/queries/items';
3
+ import { getItemTree, deleteItem, deleteItemsByProject, bulkUpdateStatus } from '@/lib/db/queries/items';
4
4
 
5
5
  export async function GET(
6
6
  _request: NextRequest,
@@ -15,3 +15,53 @@ export async function GET(
15
15
  const tree = getItemTree(id);
16
16
  return NextResponse.json(tree);
17
17
  }
18
+
19
+ // Bulk delete: DELETE /api/projects/[id]/items
20
+ // body: { itemIds: string[] } or { all: true }
21
+ export async function DELETE(
22
+ request: NextRequest,
23
+ { params }: { params: Promise<{ id: string }> },
24
+ ) {
25
+ const { id } = await params;
26
+ const project = getProject(id);
27
+ if (!project) {
28
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
29
+ }
30
+
31
+ const body = await request.json();
32
+
33
+ if (body.all) {
34
+ deleteItemsByProject(id);
35
+ } else if (Array.isArray(body.itemIds)) {
36
+ for (const itemId of body.itemIds) {
37
+ deleteItem(itemId);
38
+ }
39
+ } else {
40
+ return NextResponse.json({ error: 'itemIds or all required' }, { status: 400 });
41
+ }
42
+
43
+ const tree = getItemTree(id);
44
+ return NextResponse.json(tree);
45
+ }
46
+
47
+ // Bulk update status: PATCH /api/projects/[id]/items
48
+ // body: { status: 'done' | 'pending' | 'in_progress' }
49
+ export async function PATCH(
50
+ request: NextRequest,
51
+ { params }: { params: Promise<{ id: string }> },
52
+ ) {
53
+ const { id } = await params;
54
+ const project = getProject(id);
55
+ if (!project) {
56
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
57
+ }
58
+
59
+ const body = await request.json();
60
+ if (!body.status) {
61
+ return NextResponse.json({ error: 'status required' }, { status: 400 });
62
+ }
63
+
64
+ bulkUpdateStatus(id, body.status);
65
+ const tree = getItemTree(id);
66
+ return NextResponse.json(tree);
67
+ }
@@ -0,0 +1,73 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { getProject } from '@/lib/db/queries/projects';
3
+ import { getProjectContexts, replaceProjectContexts } from '@/lib/db/queries/context';
4
+ import { scanProjectDirectory } from '@/lib/scanner';
5
+ import { getFileCategory } from '@/lib/scanner';
6
+
7
+ export async function GET(
8
+ _request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> },
10
+ ) {
11
+ const { id } = await params;
12
+ const project = getProject(id);
13
+ if (!project) {
14
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
15
+ }
16
+
17
+ const contexts = getProjectContexts(id);
18
+ if (contexts.length === 0) {
19
+ return NextResponse.json({ exists: false, files: [], total: 0, totalSize: 0, scannedAt: null });
20
+ }
21
+
22
+ const files = contexts.map(c => ({
23
+ file_path: c.file_path,
24
+ size: c.content.length,
25
+ category: getFileCategory(c.file_path),
26
+ folder: getFolder(c.file_path),
27
+ }));
28
+
29
+ return NextResponse.json({
30
+ exists: true,
31
+ files,
32
+ total: contexts.length,
33
+ totalSize: contexts.reduce((s, c) => s + c.content.length, 0),
34
+ scannedAt: contexts[0]?.scanned_at || null,
35
+ });
36
+ }
37
+
38
+ function getFolder(filePath: string): string {
39
+ const parts = filePath.split('/');
40
+ if (parts.length <= 1 || filePath.startsWith('__')) return '(root)';
41
+ return parts[0];
42
+ }
43
+
44
+ export async function POST(
45
+ _request: NextRequest,
46
+ { params }: { params: Promise<{ id: string }> },
47
+ ) {
48
+ const { id } = await params;
49
+ const project = getProject(id);
50
+ if (!project) {
51
+ return NextResponse.json({ error: 'Project not found' }, { status: 404 });
52
+ }
53
+
54
+ if (!project.project_path) {
55
+ return NextResponse.json({ error: '프로젝트 경로가 설정되지 않았습니다' }, { status: 400 });
56
+ }
57
+
58
+ try {
59
+ const scanned = scanProjectDirectory(project.project_path);
60
+ const contexts = replaceProjectContexts(id, scanned);
61
+
62
+ return NextResponse.json({
63
+ files: contexts.map(c => ({
64
+ file_path: c.file_path,
65
+ size: c.content.length,
66
+ })),
67
+ total: contexts.length,
68
+ });
69
+ } catch (error) {
70
+ const msg = error instanceof Error ? error.message : 'Scan failed';
71
+ return NextResponse.json({ error: msg }, { status: 500 });
72
+ }
73
+ }
@@ -0,0 +1,112 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { getProject } from '@/lib/db/queries/projects';
3
+ import { replaceProjectContexts } from '@/lib/db/queries/context';
4
+ import { scanProjectDirectoryStream } from '@/lib/scanner';
5
+ import { runAnalysis } from '@/lib/ai/client';
6
+
7
+ export async function GET(
8
+ _request: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> },
10
+ ) {
11
+ const { id } = await params;
12
+ const project = getProject(id);
13
+ if (!project) {
14
+ return new Response('Project not found', { status: 404 });
15
+ }
16
+
17
+ if (!project.project_path) {
18
+ return new Response('No project path', { status: 400 });
19
+ }
20
+
21
+ const projectPath = project.project_path;
22
+ const projectId = id;
23
+
24
+ const stream = new ReadableStream({
25
+ async start(controller) {
26
+ const encoder = new TextEncoder();
27
+
28
+ const send = (event: string, data: unknown) => {
29
+ try {
30
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
31
+ } catch { /* closed */ }
32
+ };
33
+ let directoryTree = '';
34
+ let readmeContent = '';
35
+ let packageJsonContent = '';
36
+
37
+ try {
38
+ const generator = scanProjectDirectoryStream(projectPath);
39
+
40
+ for (const event of generator) {
41
+ if (event.type === 'scanning_dir') {
42
+ send('scanning', { dir: event.dir });
43
+ } else if (event.type === 'file_found') {
44
+ send('file', {
45
+ file_path: event.file!.file_path,
46
+ size: event.file!.size,
47
+ category: event.file!.category,
48
+ folder: event.file!.folder,
49
+ summarized: event.file!.summarized,
50
+ });
51
+ } else if (event.type === 'done') {
52
+ replaceProjectContexts(projectId, event.results!);
53
+ directoryTree = event.results?.find(r => r.file_path === '__directory_tree.txt')?.content || '';
54
+ readmeContent = event.results?.find(r => r.file_path.match(/^README\.md$/i))?.content || '';
55
+ packageJsonContent = event.results?.find(r => r.file_path === 'package.json')?.content || '';
56
+ send('scan_complete', {
57
+ total: event.total,
58
+ totalSize: event.totalSize,
59
+ });
60
+ }
61
+ }
62
+ } catch (error) {
63
+ const msg = error instanceof Error ? error.message : 'Scan failed';
64
+ send('error', { error: msg });
65
+ controller.close();
66
+ return;
67
+ }
68
+
69
+ // Phase 2: Auto-analyze project with AI
70
+ send('analyzing', { message: '프로젝트를 분석하고 있습니다...' });
71
+
72
+ const analysisPrompt = `아래 프로젝트의 디렉토리 구조와 핵심 파일을 보고, 이 프로젝트에 대해 간결하게 설명해주세요.
73
+
74
+ 다음 형식으로 작성해주세요 (마크다운 없이 평문):
75
+ 1. 프로젝트 목적 (1줄)
76
+ 2. 기술 스택 (1줄)
77
+ 3. 주요 서브 프로젝트/모듈 구성 (2-3줄)
78
+ 4. 현재 개발 상태 추정 (1줄)
79
+
80
+ 간결하고 핵심만 담아주세요. 한국어로 작성하세요.
81
+
82
+ === 디렉토리 구조 ===
83
+ ${directoryTree.slice(0, 5000)}
84
+
85
+ ${readmeContent ? `=== README.md ===\n${readmeContent.slice(0, 3000)}` : ''}
86
+
87
+ ${packageJsonContent ? `=== package.json ===\n${packageJsonContent.slice(0, 2000)}` : ''}`;
88
+
89
+ try {
90
+ const analysisResult = await runAnalysis(analysisPrompt, (text) => {
91
+ send('analysis_text', { text });
92
+ });
93
+ send('analysis_done', { description: analysisResult });
94
+ } catch (err) {
95
+ const msg = err instanceof Error ? err.message : 'Analysis failed';
96
+ console.error('[scan] Auto-analysis failed:', msg);
97
+ send('analysis_done', { description: '', error: msg });
98
+ }
99
+
100
+ controller.close();
101
+ },
102
+ });
103
+
104
+ return new Response(stream, {
105
+ headers: {
106
+ 'Content-Type': 'text/event-stream',
107
+ 'Cache-Control': 'no-cache',
108
+ 'Connection': 'keep-alive',
109
+ 'X-Accel-Buffering': 'no',
110
+ },
111
+ });
112
+ }
@@ -1,7 +1,26 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getProject } from '@/lib/db/queries/projects';
3
3
  import { getBrainstorm } from '@/lib/db/queries/brainstorms';
4
+ import { getProjectContextSummary } from '@/lib/db/queries/context';
4
5
  import { structureWithChat } from '@/lib/ai/structurer';
6
+ import { getTask } from '@/lib/task-store';
7
+
8
+ export async function GET(
9
+ _request: NextRequest,
10
+ { params }: { params: Promise<{ id: string }> },
11
+ ) {
12
+ const { id } = await params;
13
+ const task = getTask(id);
14
+ if (!task) {
15
+ return NextResponse.json({ active: false });
16
+ }
17
+ return NextResponse.json({
18
+ active: task.status === 'running',
19
+ status: task.status,
20
+ startedAt: task.startedAt,
21
+ eventCount: task.events.length,
22
+ });
23
+ }
5
24
 
6
25
  export async function POST(
7
26
  _request: NextRequest,
@@ -14,12 +33,24 @@ export async function POST(
14
33
  }
15
34
 
16
35
  const brainstorm = getBrainstorm(id);
17
- if (!brainstorm || !brainstorm.content.trim()) {
18
- return NextResponse.json({ error: 'No brainstorm content to structure' }, { status: 400 });
36
+ if (!brainstorm) {
37
+ return NextResponse.json({ error: 'Project not initialized' }, { status: 400 });
19
38
  }
20
39
 
40
+ const hasContent = brainstorm.content.trim();
41
+ const hasContext = !!getProjectContextSummary(id);
42
+
43
+ if (!hasContent && !hasContext) {
44
+ return NextResponse.json({ error: '브레인스토밍 내용이나 프로젝트 스캔 결과가 필요합니다' }, { status: 400 });
45
+ }
46
+
47
+ // If brainstorm is empty but project context exists, use a placeholder prompt
48
+ const content = hasContent
49
+ ? brainstorm.content
50
+ : '프로젝트 스캔 결과를 분석하여 현재 프로젝트의 구조, 진행 상황, TODO 항목을 파악해주세요.';
51
+
21
52
  try {
22
- const result = await structureWithChat(id, brainstorm.id, brainstorm.content);
53
+ const result = await structureWithChat(id, brainstorm.id, content);
23
54
  return NextResponse.json(result);
24
55
  } catch (error) {
25
56
  const message = error instanceof Error ? error.message : 'AI structuring failed';