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
@@ -1,249 +1,432 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef, use } from 'react';
4
- import { useRouter } from 'next/navigation';
3
+ import { useState, useEffect, useCallback, useRef, use, Suspense } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
5
  import Editor from '@/components/brainstorm/Editor';
6
- import TreeView from '@/components/tree/TreeView';
7
- import ChatPanel from '@/components/chat/ChatPanel';
8
- import ResizeHandle from '@/components/brainstorm/ResizeHandle';
6
+ import ProjectTree from '@/components/task/ProjectTree';
7
+ import TaskDetail from '@/components/task/TaskDetail';
8
+ import DirectoryPicker from '@/components/DirectoryPicker';
9
+ import ConfirmDialog from '@/components/ui/ConfirmDialog';
10
+ import type { ISubProject, ITask, TaskStatus, ISubProjectWithStats } from '@/types';
9
11
 
10
12
  interface IProject {
11
13
  id: string;
12
14
  name: string;
13
15
  description: string;
16
+ project_path: string | null;
14
17
  }
15
18
 
16
- interface IItemTree {
17
- id: string;
18
- title: string;
19
- description: string;
20
- item_type: string;
21
- priority: string;
22
- status: string;
23
- is_locked: boolean;
24
- children: IItemTree[];
25
- }
19
+ function WorkspaceInner({ id }: { id: string }) {
20
+ const router = useRouter();
21
+ const searchParams = useSearchParams();
22
+ const initialUrlSub = useRef(searchParams.get('sub'));
23
+ const initialUrlTask = useRef(searchParams.get('task'));
26
24
 
27
- interface IMessage {
28
- id: string;
29
- role: 'assistant' | 'user';
30
- content: string;
31
- created_at: string;
32
- }
25
+ const [project, setProject] = useState<IProject | null>(null);
26
+ const [subProjects, setSubProjects] = useState<ISubProjectWithStats[]>([]);
27
+ const [selectedSubId, setSelectedSubId] = useState<string | null>(null);
28
+ const [tasks, setTasks] = useState<ITask[]>([]);
29
+ const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
30
+ const [showDirPicker, setShowDirPicker] = useState(false);
31
+ const [confirmAction, setConfirmAction] = useState<{ type: 'delete-sub' | 'delete-task'; id: string } | null>(null);
32
+ const [showAddSub, setShowAddSub] = useState(false);
33
+ const [showBrainstorm, setShowBrainstorm] = useState(true);
34
+ const [newSubName, setNewSubName] = useState('');
33
35
 
34
- interface IMemo {
35
- id: string;
36
- anchor_text: string;
37
- question: string;
38
- is_resolved: boolean;
39
- }
36
+ // Resizable panel widths
37
+ const [leftWidth, setLeftWidth] = useState(280);
38
+ const [centerWidth, setCenterWidth] = useState(280);
39
+ const containerRef = useRef<HTMLDivElement>(null);
40
+ const draggingRef = useRef<'left' | 'center' | null>(null);
41
+ const startXRef = useRef(0);
42
+ const startWidthRef = useRef(0);
40
43
 
41
- export default function ProjectWorkspace({ params }: { params: Promise<{ id: string }> }) {
42
- const { id } = use(params);
43
- const router = useRouter();
44
- const [project, setProject] = useState<IProject | null>(null);
45
- const [items, setItems] = useState<IItemTree[]>([]);
46
- const [messages, setMessages] = useState<IMessage[]>([]);
47
- const [memos, setMemos] = useState<IMemo[]>([]);
48
- const [structuring, setStructuring] = useState(false);
49
- const [chatLoading, setChatLoading] = useState(false);
50
- const [error, setError] = useState<string | null>(null);
51
- const [editorPercent, setEditorPercent] = useState(60);
52
- const leftPanelRef = useRef<HTMLDivElement>(null);
44
+ const handleMouseDown = useCallback((panel: 'left' | 'center', e: React.MouseEvent) => {
45
+ e.preventDefault();
46
+ draggingRef.current = panel;
47
+ startXRef.current = e.clientX;
48
+ startWidthRef.current = panel === 'left' ? leftWidth : centerWidth;
49
+ }, [leftWidth, centerWidth]);
53
50
 
54
51
  useEffect(() => {
55
- const loadProject = async () => {
56
- const res = await fetch(`/api/projects/${id}`);
57
- if (!res.ok) {
58
- router.push('/');
59
- return;
60
- }
61
- setProject(await res.json());
52
+ const handleMouseMove = (e: MouseEvent) => {
53
+ if (!draggingRef.current) return;
54
+ const delta = e.clientX - startXRef.current;
55
+ const newWidth = Math.max(180, Math.min(500, startWidthRef.current + delta));
56
+ if (draggingRef.current === 'left') setLeftWidth(newWidth);
57
+ else setCenterWidth(newWidth);
62
58
  };
63
-
64
- const loadItems = async () => {
65
- const res = await fetch(`/api/projects/${id}/items`);
66
- if (res.ok) {
67
- setItems(await res.json());
68
- }
59
+ const handleMouseUp = () => { draggingRef.current = null; };
60
+ window.addEventListener('mousemove', handleMouseMove);
61
+ window.addEventListener('mouseup', handleMouseUp);
62
+ return () => {
63
+ window.removeEventListener('mousemove', handleMouseMove);
64
+ window.removeEventListener('mouseup', handleMouseUp);
69
65
  };
66
+ }, []);
70
67
 
71
- const loadConversations = async () => {
72
- const res = await fetch(`/api/projects/${id}/conversations`);
73
- if (res.ok) {
74
- setMessages(await res.json());
75
- }
76
- };
68
+ // Load project
69
+ useEffect(() => {
70
+ fetch(`/api/projects/${id}`)
71
+ .then(r => { if (!r.ok) { router.push('/'); return null; } return r.json(); })
72
+ .then(data => data && setProject(data));
73
+ }, [id, router]);
77
74
 
78
- const loadMemos = async () => {
79
- const res = await fetch(`/api/projects/${id}/memos?unresolved=true`);
80
- if (res.ok) {
81
- setMemos(await res.json());
75
+ // Load sub-projects (stable callback, no deps on selection state)
76
+ const loadSubProjects = useCallback(async () => {
77
+ const res = await fetch(`/api/projects/${id}/sub-projects`);
78
+ if (!res.ok) return;
79
+ const data: ISubProjectWithStats[] = await res.json();
80
+ setSubProjects(data);
81
+ return data;
82
+ }, [id]);
83
+
84
+ // Initial load: sub-projects + auto-select from URL
85
+ useEffect(() => {
86
+ loadSubProjects().then(data => {
87
+ if (!data || data.length === 0) return;
88
+ const urlSub = initialUrlSub.current;
89
+ if (urlSub && data.some(s => s.id === urlSub)) {
90
+ setSelectedSubId(urlSub);
91
+ } else {
92
+ setSelectedSubId(data[0].id);
82
93
  }
83
- };
94
+ });
95
+ }, [loadSubProjects]);
84
96
 
85
- loadProject();
86
- loadItems();
87
- loadConversations();
88
- loadMemos();
89
- }, [id, router]);
97
+ // Load tasks when sub-project changes
98
+ useEffect(() => {
99
+ if (!selectedSubId) { setTasks([]); return; }
100
+ fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`)
101
+ .then(r => r.json())
102
+ .then((data: ITask[]) => {
103
+ setTasks(data);
104
+ // Auto-select from URL on first load only
105
+ const urlTask = initialUrlTask.current;
106
+ if (urlTask && data.some(t => t.id === urlTask)) {
107
+ setSelectedTaskId(urlTask);
108
+ initialUrlTask.current = null; // consume once
109
+ }
110
+ });
111
+ }, [id, selectedSubId]);
90
112
 
91
- const handleStructure = useCallback(async (_content: string) => {
92
- setStructuring(true);
93
- setError(null);
113
+ const selectedTask = tasks.find(t => t.id === selectedTaskId) ?? null;
94
114
 
95
- try {
96
- const res = await fetch(`/api/projects/${id}/structure`, {
97
- method: 'POST',
98
- });
115
+ const handleCreateSubProject = async () => {
116
+ if (!newSubName.trim()) return;
117
+ const res = await fetch(`/api/projects/${id}/sub-projects`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ name: newSubName.trim() }),
121
+ });
122
+ if (res.ok) {
123
+ const sp: ISubProject = await res.json();
124
+ setNewSubName('');
125
+ setShowAddSub(false);
126
+ await loadSubProjects();
127
+ setSelectedSubId(sp.id);
128
+ }
129
+ };
99
130
 
100
- if (res.ok) {
101
- const data = await res.json();
102
- setItems(data.items);
103
- if (data.message) {
104
- setMessages(prev => [...prev, data.message]);
105
- }
106
- if (data.memos) {
107
- setMemos(data.memos);
108
- }
109
- } else {
110
- const data = await res.json();
111
- setError(data.error || '구조화에 실패했습니다');
112
- }
113
- } catch {
114
- setError('AI 연결에 실패했습니다');
115
- } finally {
116
- setStructuring(false);
131
+ const handleDeleteSubProject = (subId: string) => {
132
+ setConfirmAction({ type: 'delete-sub', id: subId });
133
+ };
134
+
135
+ const handleCreateTask = async (title: string) => {
136
+ if (!selectedSubId) return;
137
+ const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks`, {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json' },
140
+ body: JSON.stringify({ title }),
141
+ });
142
+ if (res.ok) {
143
+ const task: ITask = await res.json();
144
+ setTasks(prev => [...prev, task]);
145
+ setSelectedTaskId(task.id);
146
+ loadSubProjects();
117
147
  }
118
- }, [id]);
148
+ };
119
149
 
120
- const handleItemUpdate = useCallback(async (itemId: string, data: Record<string, unknown>) => {
121
- try {
122
- const res = await fetch(`/api/projects/${id}/items/${itemId}`, {
123
- method: 'PUT',
124
- headers: { 'Content-Type': 'application/json' },
125
- body: JSON.stringify(data),
126
- });
150
+ const handleTaskStatusChange = async (taskId: string, status: TaskStatus) => {
151
+ const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${taskId}`, {
152
+ method: 'PUT',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ status }),
155
+ });
156
+ if (res.ok) {
157
+ const updated: ITask = await res.json();
158
+ setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
159
+ loadSubProjects();
160
+ }
161
+ };
127
162
 
128
- if (res.ok) {
129
- // Reload items tree to reflect changes (including cascaded lock)
130
- const itemsRes = await fetch(`/api/projects/${id}/items`);
131
- if (itemsRes.ok) {
132
- setItems(await itemsRes.json());
133
- }
163
+ const handleTaskTodayToggle = async (taskId: string, isToday: boolean) => {
164
+ const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${taskId}`, {
165
+ method: 'PUT',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify({ is_today: isToday }),
168
+ });
169
+ if (res.ok) {
170
+ const updated: ITask = await res.json();
171
+ setTasks(prev => prev.map(t => t.id === taskId ? updated : t));
172
+ }
173
+ };
174
+
175
+ const handleTaskUpdate = async (data: Partial<ITask>) => {
176
+ if (!selectedTaskId || !selectedSubId) return;
177
+ const res = await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${selectedTaskId}`, {
178
+ method: 'PUT',
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify(data),
181
+ });
182
+ if (res.ok) {
183
+ const updated: ITask = await res.json();
184
+ setTasks(prev => prev.map(t => t.id === selectedTaskId ? updated : t));
185
+ loadSubProjects();
186
+ }
187
+ };
188
+
189
+ const handleTaskDelete = () => {
190
+ if (!selectedTaskId) return;
191
+ setConfirmAction({ type: 'delete-task', id: selectedTaskId });
192
+ };
193
+
194
+ const handleConfirmAction = async () => {
195
+ if (!confirmAction) return;
196
+ if (confirmAction.type === 'delete-sub') {
197
+ await fetch(`/api/projects/${id}/sub-projects/${confirmAction.id}`, { method: 'DELETE' });
198
+ if (selectedSubId === confirmAction.id) {
199
+ setSelectedSubId(null);
200
+ setSelectedTaskId(null);
134
201
  }
135
- } catch {
136
- setError('항목 업데이트에 실패했습니다');
202
+ loadSubProjects();
203
+ } else if (confirmAction.type === 'delete-task') {
204
+ await fetch(`/api/projects/${id}/sub-projects/${selectedSubId}/tasks/${confirmAction.id}`, { method: 'DELETE' });
205
+ setTasks(prev => prev.filter(t => t.id !== confirmAction.id));
206
+ if (selectedTaskId === confirmAction.id) setSelectedTaskId(null);
207
+ loadSubProjects();
137
208
  }
138
- }, [id]);
209
+ setConfirmAction(null);
210
+ };
139
211
 
140
- const handleSendMessage = useCallback(async (message: string) => {
141
- setChatLoading(true);
142
- setError(null);
212
+ const handleSetPath = async (selectedPath: string) => {
213
+ const res = await fetch(`/api/projects/${id}`, {
214
+ method: 'PUT',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify({ project_path: selectedPath }),
217
+ });
218
+ if (res.ok) {
219
+ setProject(await res.json());
220
+ setShowDirPicker(false);
221
+ }
222
+ };
143
223
 
144
- // Optimistically add user message
145
- const tempUserMsg: IMessage = {
146
- id: `temp-${Date.now()}`,
147
- role: 'user',
148
- content: message,
149
- created_at: new Date().toISOString(),
150
- };
151
- setMessages(prev => [...prev, tempUserMsg]);
224
+ // Keyboard shortcuts (use e.code for Korean IME compatibility)
225
+ useEffect(() => {
226
+ const handler = (e: KeyboardEvent) => {
227
+ const isInput = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement;
152
228
 
153
- try {
154
- const res = await fetch(`/api/projects/${id}/conversations`, {
155
- method: 'POST',
156
- headers: { 'Content-Type': 'application/json' },
157
- body: JSON.stringify({ message }),
158
- });
229
+ // B — toggle brainstorming panel
230
+ if (!isInput && e.code === 'KeyB' && !e.metaKey && !e.ctrlKey) {
231
+ e.preventDefault();
232
+ setShowBrainstorm(prev => !prev);
233
+ return;
234
+ }
235
+
236
+ // N — new sub-project (when not in input)
237
+ if (!isInput && e.code === 'KeyN' && !e.metaKey && !e.ctrlKey) {
238
+ e.preventDefault();
239
+ setShowAddSub(true);
240
+ return;
241
+ }
242
+
243
+ // T — new task (when sub-project selected, not in input)
244
+ if (!isInput && e.code === 'KeyT' && !e.metaKey && !e.ctrlKey && selectedSubId) {
245
+ e.preventDefault();
246
+ const addBtn = document.querySelector('[data-add-task]') as HTMLButtonElement;
247
+ addBtn?.click();
248
+ return;
249
+ }
250
+
251
+ // Cmd+1~6 — status change
252
+ if (selectedTaskId && selectedSubId && !isInput) {
253
+ const statusMap: Record<string, TaskStatus> = {
254
+ 'Digit1': 'idea', 'Digit2': 'writing', 'Digit3': 'submitted',
255
+ 'Digit4': 'testing', 'Digit5': 'done', 'Digit6': 'problem',
256
+ };
159
257
 
160
- if (res.ok) {
161
- const data = await res.json();
162
- setItems(data.items);
163
- // Replace the temp message with real messages
164
- setMessages(prev => {
165
- const withoutTemp = prev.filter(m => m.id !== tempUserMsg.id);
166
- return [...withoutTemp, ...data.messages];
167
- });
168
- if (data.memos) {
169
- setMemos(data.memos);
258
+ if ((e.metaKey || e.ctrlKey) && statusMap[e.code]) {
259
+ e.preventDefault();
260
+ handleTaskStatusChange(selectedTaskId, statusMap[e.code]);
170
261
  }
171
- } else {
172
- const data = await res.json();
173
- setError(data.error || '응답에 실패했습니다');
174
- // Remove temp message on error
175
- setMessages(prev => prev.filter(m => m.id !== tempUserMsg.id));
176
262
  }
177
- } catch {
178
- setError('AI 연결에 실패했습니다');
179
- setMessages(prev => prev.filter(m => m.id !== tempUserMsg.id));
180
- } finally {
181
- setChatLoading(false);
182
- }
183
- }, [id]);
263
+ };
264
+ window.addEventListener('keydown', handler);
265
+ return () => window.removeEventListener('keydown', handler);
266
+ });
184
267
 
185
268
  if (!project) {
186
- return (
187
- <div className="min-h-screen flex items-center justify-center text-muted-foreground">
188
- 로딩 중...
189
- </div>
190
- );
269
+ return <div className="min-h-screen flex items-center justify-center text-muted-foreground">Loading...</div>;
191
270
  }
192
271
 
193
272
  return (
194
273
  <div className="h-screen flex flex-col">
195
274
  {/* Header */}
196
- <header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card">
275
+ <header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card flex-shrink-0">
197
276
  <div className="flex items-center gap-3">
198
277
  <button
199
278
  onClick={() => router.push('/')}
200
279
  className="text-muted-foreground hover:text-foreground hover:bg-muted transition-colors text-sm px-2 py-1 rounded-md"
201
280
  >
202
- &larr; 뒤로
281
+ &larr; Back
203
282
  </button>
204
283
  <span className="text-border">|</span>
205
284
  <h1 className="text-sm font-semibold">{project.name}</h1>
206
- {project.description && (
207
- <span className="text-xs text-muted-foreground">{project.description}</span>
285
+ {project.project_path && (
286
+ <span className="text-xs text-muted-foreground font-mono truncate max-w-48" title={project.project_path}>
287
+ {project.project_path}
288
+ </span>
208
289
  )}
209
290
  </div>
210
291
  <div className="flex items-center gap-2">
211
- {error && (
212
- <span className="text-xs text-destructive">{error}</span>
292
+ {!project.project_path && (
293
+ <button
294
+ onClick={() => setShowDirPicker(true)}
295
+ className="px-3 py-1.5 text-xs bg-muted hover:bg-card-hover text-foreground
296
+ border border-border rounded-md transition-colors"
297
+ >
298
+ Link folder
299
+ </button>
213
300
  )}
301
+ </div>
302
+ </header>
303
+
304
+ {/* 3-Panel Layout with resize handles */}
305
+ <div ref={containerRef} className="flex-1 flex overflow-hidden">
306
+ {/* Left: Brainstorming (collapsible) */}
307
+ {showBrainstorm ? (
308
+ <>
309
+ <div style={{ width: leftWidth }} className="border-r border-border flex flex-col flex-shrink-0">
310
+ <Editor
311
+ projectId={id}
312
+ onContentChange={() => {}}
313
+ onSendMessage={() => {}}
314
+ memos={[]}
315
+ chatLoading={false}
316
+ onCollapse={() => setShowBrainstorm(false)}
317
+ />
318
+ </div>
319
+ {/* Resize handle: left */}
320
+ <div
321
+ className="panel-resize-handle"
322
+ onMouseDown={(e) => handleMouseDown('left', e)}
323
+ >
324
+ <div className="panel-resize-handle-bar" />
325
+ </div>
326
+ </>
327
+ ) : (
214
328
  <button
215
- onClick={() => handleStructure('')}
216
- disabled={structuring}
217
- className="px-3 py-1.5 text-xs bg-accent hover:bg-accent/80 text-white
218
- rounded-md transition-colors disabled:opacity-50"
329
+ onClick={() => setShowBrainstorm(true)}
330
+ className="w-8 border-r border-border flex-shrink-0 flex items-center justify-center
331
+ text-muted-foreground hover:text-foreground hover:bg-card-hover transition-colors
332
+ text-xs"
333
+ title="Show brainstorming (B)"
334
+ style={{ writingMode: 'vertical-rl' }}
219
335
  >
220
- {structuring ? '구조화 중...' : '지금 구조화'}
336
+ Brainstorm
221
337
  </button>
338
+ )}
339
+
340
+ {/* Center: Tree (Sub-projects + Tasks) */}
341
+ <div style={{ width: centerWidth }} className="border-r border-border flex flex-col flex-shrink-0">
342
+ {/* Add sub-project input */}
343
+ {showAddSub && (
344
+ <div className="px-3 py-2 border-b border-border">
345
+ <input
346
+ type="text"
347
+ value={newSubName}
348
+ onChange={(e) => setNewSubName(e.target.value)}
349
+ onKeyDown={(e) => {
350
+ if (e.key === 'Enter') handleCreateSubProject();
351
+ if (e.key === 'Escape') { setNewSubName(''); setShowAddSub(false); }
352
+ }}
353
+ placeholder="Sub-project name..."
354
+ className="w-full bg-input border border-border rounded px-2 py-1 text-xs
355
+ focus:border-primary focus:outline-none text-foreground"
356
+ autoFocus
357
+ />
358
+ </div>
359
+ )}
360
+
361
+ <ProjectTree
362
+ subProjects={subProjects}
363
+ tasks={tasks}
364
+ selectedSubId={selectedSubId}
365
+ selectedTaskId={selectedTaskId}
366
+ onSelectSub={(subId) => { setSelectedSubId(subId); setSelectedTaskId(null); }}
367
+ onSelectTask={setSelectedTaskId}
368
+ onCreateSub={() => setShowAddSub(true)}
369
+ onDeleteSub={handleDeleteSubProject}
370
+ onCreateTask={handleCreateTask}
371
+ onStatusChange={handleTaskStatusChange}
372
+ onTodayToggle={handleTaskTodayToggle}
373
+ />
222
374
  </div>
223
- </header>
224
375
 
225
- {/* 2-Panel Layout */}
226
- <div className="flex-1 flex overflow-hidden">
227
- {/* Left: Editor (top) + Chat (bottom) */}
228
- <div ref={leftPanelRef} className="w-1/2 border-r border-border flex flex-col">
229
- <div style={{ height: `${editorPercent}%` }} className="flex flex-col min-h-0">
230
- <Editor projectId={id} onContentChange={handleStructure} memos={memos} />
231
- </div>
232
- <ResizeHandle onResize={setEditorPercent} containerRef={leftPanelRef} />
233
- <div style={{ height: `${100 - editorPercent}%` }} className="flex flex-col min-h-0">
234
- <ChatPanel
235
- messages={messages}
236
- loading={chatLoading || structuring}
237
- onSendMessage={handleSendMessage}
238
- />
239
- </div>
376
+ {/* Resize handle: center */}
377
+ <div
378
+ className="panel-resize-handle"
379
+ onMouseDown={(e) => handleMouseDown('center', e)}
380
+ >
381
+ <div className="panel-resize-handle-bar" />
240
382
  </div>
241
383
 
242
- {/* Right: Tree View */}
243
- <div className="w-1/2 flex flex-col">
244
- <TreeView items={items} loading={structuring} projectId={id} onItemUpdate={handleItemUpdate} />
384
+ {/* Right: Task Detail */}
385
+ <div className="flex-1 min-w-0">
386
+ {selectedTask ? (
387
+ <TaskDetail
388
+ task={selectedTask}
389
+ projectId={id}
390
+ subProjectId={selectedSubId!}
391
+ onUpdate={handleTaskUpdate}
392
+ onDelete={handleTaskDelete}
393
+ />
394
+ ) : (
395
+ <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
396
+ {tasks.length > 0 ? 'Select a task' : selectedSubId ? 'Create a task to get started' : 'Select a sub-project'}
397
+ </div>
398
+ )}
245
399
  </div>
246
400
  </div>
401
+
402
+ {showDirPicker && (
403
+ <DirectoryPicker
404
+ onSelect={handleSetPath}
405
+ onCancel={() => setShowDirPicker(false)}
406
+ initialPath={project.project_path || undefined}
407
+ />
408
+ )}
409
+
410
+ <ConfirmDialog
411
+ open={!!confirmAction}
412
+ title={confirmAction?.type === 'delete-sub' ? 'Delete sub-project?' : 'Delete task?'}
413
+ description={confirmAction?.type === 'delete-sub'
414
+ ? 'This will delete the sub-project and all its tasks.'
415
+ : 'This task will be permanently deleted.'}
416
+ confirmLabel="Delete"
417
+ variant="danger"
418
+ onConfirm={handleConfirmAction}
419
+ onCancel={() => setConfirmAction(null)}
420
+ />
247
421
  </div>
248
422
  );
249
423
  }
424
+
425
+ export default function ProjectWorkspace({ params }: { params: Promise<{ id: string }> }) {
426
+ const { id } = use(params);
427
+ return (
428
+ <Suspense fallback={<div className="min-h-screen flex items-center justify-center text-muted-foreground">Loading...</div>}>
429
+ <WorkspaceInner id={id} />
430
+ </Suspense>
431
+ );
432
+ }