idea-manager 0.2.0 → 0.3.1
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/README.md +33 -41
- package/next.config.ts +0 -1
- package/package.json +2 -2
- package/{src/app/icon.svg → public/favicon.svg} +2 -2
- package/src/app/api/filesystem/route.ts +49 -0
- package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
- package/src/app/api/projects/[id]/items/route.ts +51 -1
- package/src/app/api/projects/[id]/scan/route.ts +73 -0
- package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
- package/src/app/api/projects/[id]/structure/route.ts +34 -3
- package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/globals.css +465 -5
- package/src/app/layout.tsx +3 -0
- package/src/app/page.tsx +260 -88
- package/src/app/projects/[id]/page.tsx +366 -183
- package/src/cli.ts +10 -10
- package/src/components/DirectoryPicker.tsx +137 -0
- package/src/components/ScanPanel.tsx +743 -0
- package/src/components/brainstorm/Editor.tsx +20 -4
- package/src/components/brainstorm/MemoPin.tsx +91 -5
- package/src/components/dashboard/SubProjectCard.tsx +76 -0
- package/src/components/dashboard/TabBar.tsx +42 -0
- package/src/components/task/ProjectTree.tsx +223 -0
- package/src/components/task/PromptEditor.tsx +107 -0
- package/src/components/task/StatusFlow.tsx +43 -0
- package/src/components/task/TaskChat.tsx +134 -0
- package/src/components/task/TaskDetail.tsx +205 -0
- package/src/components/task/TaskList.tsx +119 -0
- package/src/components/tree/CardView.tsx +206 -0
- package/src/components/tree/RefinePopover.tsx +157 -0
- package/src/components/tree/TreeNode.tsx +147 -38
- package/src/components/tree/TreeView.tsx +270 -26
- package/src/components/ui/ConfirmDialog.tsx +88 -0
- package/src/lib/ai/chat-responder.ts +4 -2
- package/src/lib/ai/cleanup.ts +87 -0
- package/src/lib/ai/client.ts +175 -58
- package/src/lib/ai/prompter.ts +19 -24
- package/src/lib/ai/refiner.ts +128 -0
- package/src/lib/ai/structurer.ts +340 -11
- package/src/lib/db/queries/context.ts +76 -0
- package/src/lib/db/queries/items.ts +133 -12
- package/src/lib/db/queries/projects.ts +12 -8
- package/src/lib/db/queries/sub-projects.ts +122 -0
- package/src/lib/db/queries/task-conversations.ts +27 -0
- package/src/lib/db/queries/task-prompts.ts +32 -0
- package/src/lib/db/queries/tasks.ts +133 -0
- package/src/lib/db/schema.ts +75 -0
- package/src/lib/mcp/server.ts +38 -39
- package/src/lib/mcp/tools.ts +47 -45
- package/src/lib/scanner.ts +573 -0
- package/src/lib/task-store.ts +97 -0
- package/src/types/index.ts +65 -0
package/README.md
CHANGED
|
@@ -1,42 +1,34 @@
|
|
|
1
1
|
# IM (Idea Manager)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 아이디어에서 실행 가능한 프롬프트까지, 멀티 프로젝트 워크플로우 매니저
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
여러 프로젝트를 동시에 진행하는 개발자를 위한 태스크 관리 도구입니다. 아이디어를 서브 프로젝트와 태스크로 조직화하고, 각 태스크별 프롬프트를 정제하여 Claude Code 등 AI 에이전트에게 전달할 수 있습니다. MCP Server를 내장하고 있어 AI 에이전트가 자율적으로 태스크를 가져가 실행할 수 있습니다.
|
|
6
6
|
|
|
7
7
|
## 핵심 워크플로우
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
브레인스토밍 →
|
|
10
|
+
브레인스토밍 → 서브 프로젝트/태스크 조직화 → 프롬프트 정제 → MCP로 AI 실행
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
###
|
|
14
|
-
|
|
15
|
-
자유로운 형태로 아이디어를 작성합니다. 구조나 형식에 구애받지 않고 생각나는 대로 써내려갑니다.
|
|
16
|
-
|
|
17
|
-
### 2. AI 구조화
|
|
18
|
-
|
|
19
|
-
AI가 브레인스토밍 텍스트를 분석하여 계층형 작업 트리로 변환합니다.
|
|
20
|
-
모호한 부분이 있으면 AI가 질문을 던지고, 채팅을 통해 답변하면 구조가 점점 정교해집니다.
|
|
13
|
+
### 계층 구조
|
|
21
14
|
|
|
22
15
|
```
|
|
23
16
|
프로젝트
|
|
24
|
-
├──
|
|
25
|
-
│ ├──
|
|
26
|
-
│
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
├── 서브 프로젝트 A
|
|
18
|
+
│ ├── 태스크 1 → 프롬프트
|
|
19
|
+
│ ├── 태스크 2 → 프롬프트
|
|
20
|
+
│ └── 태스크 3 → 프롬프트
|
|
21
|
+
└── 서브 프로젝트 B
|
|
22
|
+
├── 태스크 4 → 프롬프트
|
|
23
|
+
└── 태스크 5 → 프롬프트
|
|
30
24
|
```
|
|
31
25
|
|
|
32
|
-
###
|
|
33
|
-
|
|
34
|
-
구조화된 각 항목에 대해 AI가 실행 가능한 프롬프트를 자동 생성합니다.
|
|
35
|
-
수동으로 편집하거나 직접 작성할 수도 있습니다.
|
|
26
|
+
### 태스크 상태 흐름
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
```
|
|
29
|
+
💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
|
|
30
|
+
🔴 Problem
|
|
31
|
+
```
|
|
40
32
|
|
|
41
33
|
## 설치
|
|
42
34
|
|
|
@@ -52,7 +44,7 @@ npm install -g idea-manager
|
|
|
52
44
|
im start
|
|
53
45
|
```
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
`http://localhost:3456`에서 웹 UI가 열립니다.
|
|
56
48
|
|
|
57
49
|
```bash
|
|
58
50
|
# 포트 변경
|
|
@@ -65,8 +57,6 @@ im start -p 4000
|
|
|
65
57
|
im mcp
|
|
66
58
|
```
|
|
67
59
|
|
|
68
|
-
Claude Desktop, Claude Code 등에서 MCP Server로 연결하여 AI 에이전트가 작업을 자율 실행할 수 있습니다.
|
|
69
|
-
|
|
70
60
|
#### Claude Desktop 설정 (claude_desktop_config.json)
|
|
71
61
|
|
|
72
62
|
```json
|
|
@@ -91,27 +81,29 @@ claude mcp add idea-manager -- npx -y idea-manager mcp
|
|
|
91
81
|
| 도구 | 설명 |
|
|
92
82
|
|------|------|
|
|
93
83
|
| `list-projects` | 프로젝트 목록 조회 |
|
|
94
|
-
| `get-project-context` | 프로젝트
|
|
95
|
-
| `get-next-task` | 다음
|
|
96
|
-
| `get-prompt` | 특정
|
|
97
|
-
| `update-status` |
|
|
98
|
-
| `report-completion` |
|
|
84
|
+
| `get-project-context` | 서브 프로젝트 + 태스크 트리 전체 조회 |
|
|
85
|
+
| `get-next-task` | 다음 실행할 태스크와 프롬프트 조회 (status=submitted) |
|
|
86
|
+
| `get-task-prompt` | 특정 태스크의 프롬프트 조회 |
|
|
87
|
+
| `update-status` | 태스크 상태 변경 (idea/writing/submitted/testing/done/problem) |
|
|
88
|
+
| `report-completion` | 태스크 완료 보고 |
|
|
99
89
|
|
|
100
90
|
## 주요 기능
|
|
101
91
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
- **
|
|
108
|
-
-
|
|
92
|
+
- **3-패널 워크스페이스** — 브레인스토밍 | 프로젝트 트리 | 태스크 상세, 패널 간 드래그로 크기 조절
|
|
93
|
+
- **트리형 프로젝트 구조** — 서브 프로젝트 아래 태스크가 계층적으로 표시
|
|
94
|
+
- **브레인스토밍 패널** — 자유 형식 메모, 접기/펼치기 가능
|
|
95
|
+
- **프롬프트 에디터** — 태스크별 프롬프트 작성/편집/복사, AI 다듬기
|
|
96
|
+
- **AI 채팅** — 태스크별 AI 대화로 프롬프트 구체화
|
|
97
|
+
- **3탭 대시보드** — 진행 중 / 전체 / 오늘 할 일
|
|
98
|
+
- **키보드 단축키** — 한영 전환 상관없이 동작 (B: 브레인스토밍 토글, N: 서브 프로젝트 추가, T: 태스크 추가, Cmd+1~6: 상태 변경)
|
|
99
|
+
- **MCP Server 내장** — AI 에이전트 자율 실행 지원
|
|
100
|
+
- **로컬 우선** — SQLite 기반, 데이터는 `~/.idea-manager/`에 저장
|
|
109
101
|
|
|
110
102
|
## 기술 스택
|
|
111
103
|
|
|
112
104
|
| 영역 | 기술 |
|
|
113
105
|
|------|------|
|
|
114
|
-
| Frontend | Next.js, React 19, TypeScript, Tailwind CSS 4 |
|
|
106
|
+
| Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
|
|
115
107
|
| Backend | Next.js API Routes |
|
|
116
108
|
| Database | SQLite (better-sqlite3) |
|
|
117
109
|
| AI | Anthropic Claude (Agent SDK) |
|
|
@@ -121,7 +113,7 @@ claude mcp add idea-manager -- npx -y idea-manager mcp
|
|
|
121
113
|
## 환경 변수
|
|
122
114
|
|
|
123
115
|
```bash
|
|
124
|
-
ANTHROPIC_API_KEY=sk-ant-... # Claude API 키 (AI
|
|
116
|
+
ANTHROPIC_API_KEY=sk-ant-... # Claude API 키 (AI 채팅/다듬기에 필요, 없어도 기본 기능은 동작)
|
|
125
117
|
```
|
|
126
118
|
|
|
127
119
|
## 라이선스
|
package/next.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idea-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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/
|
|
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",
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
|
6
6
|
</linearGradient>
|
|
7
7
|
</defs>
|
|
8
|
-
<rect width="512" height="512" rx="
|
|
9
|
-
<text x="256" y="390" font-family="
|
|
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
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
|
|
18
|
-
return NextResponse.json({ error: '
|
|
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,
|
|
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';
|