idea-manager 0.4.0 → 0.5.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/package.json +1 -1
- package/src/app/projects/[id]/page.tsx +0 -4
- package/src/components/brainstorm/Editor.tsx +1 -84
- package/src/lib/ai/client.ts +0 -123
- package/src/lib/db/schema.ts +0 -70
- package/src/types/index.ts +0 -90
- package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
- package/src/app/api/projects/[id]/conversations/route.ts +0 -50
- package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
- package/src/app/api/projects/[id]/items/route.ts +0 -67
- package/src/app/api/projects/[id]/memos/route.ts +0 -18
- package/src/app/api/projects/[id]/scan/route.ts +0 -73
- package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
- package/src/app/api/projects/[id]/structure/route.ts +0 -59
- package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
- package/src/components/ScanPanel.tsx +0 -743
- package/src/components/brainstorm/MemoPin.tsx +0 -117
- package/src/components/tree/CardView.tsx +0 -206
- package/src/components/tree/ItemDetail.tsx +0 -196
- package/src/components/tree/LockToggle.tsx +0 -23
- package/src/components/tree/RefinePopover.tsx +0 -157
- package/src/components/tree/StatusBadge.tsx +0 -32
- package/src/components/tree/TreeNode.tsx +0 -227
- package/src/components/tree/TreeView.tsx +0 -304
- package/src/lib/ai/chat-responder.ts +0 -71
- package/src/lib/ai/cleanup.ts +0 -87
- package/src/lib/ai/prompter.ts +0 -78
- package/src/lib/ai/refiner.ts +0 -128
- package/src/lib/ai/structurer.ts +0 -403
- package/src/lib/db/queries/context.ts +0 -76
- package/src/lib/db/queries/conversations.ts +0 -46
- package/src/lib/db/queries/items.ts +0 -268
- package/src/lib/db/queries/memos.ts +0 -66
- package/src/lib/db/queries/prompts.ts +0 -68
- package/src/lib/scanner.ts +0 -573
- package/src/lib/task-store.ts +0 -97
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
interface StatusBadgeProps {
|
|
4
|
-
status: string;
|
|
5
|
-
onStatusChange: (status: string) => void;
|
|
6
|
-
disabled?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const statusConfig: Record<string, { icon: string; label: string; next: string }> = {
|
|
10
|
-
pending: { icon: '\u{23F3}', label: '대기', next: 'in_progress' },
|
|
11
|
-
in_progress: { icon: '\u{1F504}', label: '진행 중', next: 'done' },
|
|
12
|
-
done: { icon: '\u{2705}', label: '완료', next: 'pending' },
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default function StatusBadge({ status, onStatusChange, disabled }: StatusBadgeProps) {
|
|
16
|
-
const config = statusConfig[status] || statusConfig.pending;
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<button
|
|
20
|
-
onClick={(e) => {
|
|
21
|
-
e.stopPropagation();
|
|
22
|
-
onStatusChange(config.next);
|
|
23
|
-
}}
|
|
24
|
-
disabled={disabled}
|
|
25
|
-
className="status-badge"
|
|
26
|
-
title={`${config.label} → 클릭하여 변경`}
|
|
27
|
-
>
|
|
28
|
-
<span>{config.icon}</span>
|
|
29
|
-
<span className="status-badge-label">{config.label}</span>
|
|
30
|
-
</button>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import LockToggle from './LockToggle';
|
|
5
|
-
import StatusBadge from './StatusBadge';
|
|
6
|
-
import ItemDetail from './ItemDetail';
|
|
7
|
-
import RefinePopover from './RefinePopover';
|
|
8
|
-
|
|
9
|
-
interface IItemTree {
|
|
10
|
-
id: string;
|
|
11
|
-
project_id?: string;
|
|
12
|
-
title: string;
|
|
13
|
-
description: string;
|
|
14
|
-
item_type: string;
|
|
15
|
-
priority: string;
|
|
16
|
-
status: string;
|
|
17
|
-
is_locked: boolean;
|
|
18
|
-
is_pinned: boolean;
|
|
19
|
-
children: IItemTree[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface TreeNodeProps {
|
|
23
|
-
item: IItemTree;
|
|
24
|
-
depth: number;
|
|
25
|
-
projectId: string;
|
|
26
|
-
onItemUpdate: (itemId: string, data: Record<string, unknown>) => void;
|
|
27
|
-
onItemDelete: (itemId: string) => void;
|
|
28
|
-
onTreeRefresh: (tree: IItemTree[]) => void;
|
|
29
|
-
selectMode?: boolean;
|
|
30
|
-
selected?: Set<string>;
|
|
31
|
-
onToggleSelect?: (id: string) => void;
|
|
32
|
-
defaultExpanded?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const typeConfig: Record<string, { icon: string; color: string; label: string }> = {
|
|
36
|
-
feature: { icon: '\u{1F4E6}', color: 'var(--primary)', label: '기능' },
|
|
37
|
-
task: { icon: '\u{2705}', color: 'var(--success)', label: '작업' },
|
|
38
|
-
bug: { icon: '\u{1F41B}', color: 'var(--destructive)', label: '버그' },
|
|
39
|
-
idea: { icon: '\u{1F4A1}', color: 'var(--warning)', label: '아이디어' },
|
|
40
|
-
note: { icon: '\u{1F4DD}', color: 'var(--muted-foreground)', label: '메모' },
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function countDescendantStatus(item: IItemTree): { total: number; done: number } {
|
|
44
|
-
let total = 0;
|
|
45
|
-
let done = 0;
|
|
46
|
-
for (const child of item.children) {
|
|
47
|
-
total++;
|
|
48
|
-
if (child.status === 'done') done++;
|
|
49
|
-
const sub = countDescendantStatus(child);
|
|
50
|
-
total += sub.total;
|
|
51
|
-
done += sub.done;
|
|
52
|
-
}
|
|
53
|
-
return { total, done };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export default function TreeNode({ item, depth, projectId, onItemUpdate, onItemDelete, onTreeRefresh, selectMode, selected, onToggleSelect, defaultExpanded }: TreeNodeProps) {
|
|
57
|
-
const [expanded, setExpanded] = useState(defaultExpanded ?? depth < 2);
|
|
58
|
-
const [showDetail, setShowDetail] = useState(false);
|
|
59
|
-
const [showRefine, setShowRefine] = useState(false);
|
|
60
|
-
const hasChildren = item.children.length > 0;
|
|
61
|
-
const baseCfg = typeConfig[item.item_type] || typeConfig.note;
|
|
62
|
-
const cfg = item.status === 'done'
|
|
63
|
-
? { icon: '\u{2705}', color: 'var(--success)', label: baseCfg.label }
|
|
64
|
-
: item.status === 'in_progress'
|
|
65
|
-
? { ...baseCfg, color: 'var(--primary)' }
|
|
66
|
-
: baseCfg;
|
|
67
|
-
const childStats = hasChildren ? countDescendantStatus(item) : null;
|
|
68
|
-
|
|
69
|
-
const handleDelete = (e: React.MouseEvent) => {
|
|
70
|
-
e.stopPropagation();
|
|
71
|
-
onItemDelete(item.id);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const handlePinToggle = (e: React.MouseEvent) => {
|
|
75
|
-
e.stopPropagation();
|
|
76
|
-
onItemUpdate(item.id, { is_pinned: !item.is_pinned });
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const handleRefineToggle = (e: React.MouseEvent) => {
|
|
80
|
-
e.stopPropagation();
|
|
81
|
-
setShowRefine(!showRefine);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const isDone = item.status === 'done';
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div className="select-none">
|
|
88
|
-
<div
|
|
89
|
-
className={`tree-node-row group ${isDone ? 'opacity-50' : ''}`}
|
|
90
|
-
style={{
|
|
91
|
-
paddingLeft: `${depth * 20 + 8}px`,
|
|
92
|
-
borderLeft: depth === 0 ? `3px solid hsl(${cfg.color})` : undefined,
|
|
93
|
-
}}
|
|
94
|
-
onClick={() => selectMode ? onToggleSelect?.(item.id) : setShowDetail(!showDetail)}
|
|
95
|
-
>
|
|
96
|
-
{selectMode && (
|
|
97
|
-
<input
|
|
98
|
-
type="checkbox"
|
|
99
|
-
checked={selected?.has(item.id) ?? false}
|
|
100
|
-
onChange={() => onToggleSelect?.(item.id)}
|
|
101
|
-
onClick={(e) => e.stopPropagation()}
|
|
102
|
-
className="w-3.5 h-3.5 flex-shrink-0 accent-accent"
|
|
103
|
-
/>
|
|
104
|
-
)}
|
|
105
|
-
|
|
106
|
-
{hasChildren ? (
|
|
107
|
-
<button
|
|
108
|
-
onClick={(e) => {
|
|
109
|
-
e.stopPropagation();
|
|
110
|
-
setExpanded(!expanded);
|
|
111
|
-
}}
|
|
112
|
-
className="text-muted-foreground hover:text-foreground text-[10px] w-4 flex-shrink-0 transition-transform"
|
|
113
|
-
style={{ transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
|
|
114
|
-
>
|
|
115
|
-
▼
|
|
116
|
-
</button>
|
|
117
|
-
) : (
|
|
118
|
-
<span className="w-4 flex-shrink-0 text-center text-muted-foreground/30 text-[10px]">·</span>
|
|
119
|
-
)}
|
|
120
|
-
|
|
121
|
-
{/* Main content area */}
|
|
122
|
-
<div className="flex-1 min-w-0 py-0.5">
|
|
123
|
-
<div className="flex items-center gap-1.5">
|
|
124
|
-
<span className="text-sm flex-shrink-0">{cfg.icon}</span>
|
|
125
|
-
<span className={`text-sm truncate ${isDone ? 'line-through' : ''}`}>
|
|
126
|
-
{item.title}
|
|
127
|
-
</span>
|
|
128
|
-
{item.is_pinned && (
|
|
129
|
-
<span className="text-[10px] flex-shrink-0" title="고정됨">📌</span>
|
|
130
|
-
)}
|
|
131
|
-
{/* Child progress */}
|
|
132
|
-
{childStats && childStats.total > 0 && (
|
|
133
|
-
<span className="tree-progress-badge flex-shrink-0">
|
|
134
|
-
{childStats.done}/{childStats.total}
|
|
135
|
-
</span>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
{/* Description subtitle */}
|
|
139
|
-
{item.description && !showDetail && (
|
|
140
|
-
<p className="text-[11px] text-muted-foreground/50 truncate mt-0.5 leading-tight">
|
|
141
|
-
{item.description}
|
|
142
|
-
</p>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Right side controls */}
|
|
147
|
-
<div className="flex items-center gap-1 flex-shrink-0">
|
|
148
|
-
{/* Hover actions */}
|
|
149
|
-
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
150
|
-
<LockToggle
|
|
151
|
-
isLocked={item.is_locked}
|
|
152
|
-
onToggle={(locked) => onItemUpdate(item.id, { is_locked: locked })}
|
|
153
|
-
/>
|
|
154
|
-
<button onClick={handlePinToggle} className="tree-icon-btn" title={item.is_pinned ? '고정 해제' : '고정'}>
|
|
155
|
-
{item.is_pinned ? '📌' : '📍'}
|
|
156
|
-
</button>
|
|
157
|
-
<button onClick={handleRefineToggle} className="tree-action-btn" title="다듬기">
|
|
158
|
-
다듬기
|
|
159
|
-
</button>
|
|
160
|
-
<button onClick={handleDelete} className="tree-action-btn tree-action-btn-danger" title="삭제">
|
|
161
|
-
✕
|
|
162
|
-
</button>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{/* Always visible: priority + status */}
|
|
166
|
-
<span className="tree-priority-dot" style={{
|
|
167
|
-
background: item.priority === 'high' ? 'hsl(var(--destructive))'
|
|
168
|
-
: item.priority === 'medium' ? 'hsl(var(--warning))'
|
|
169
|
-
: 'hsl(var(--success))'
|
|
170
|
-
}} title={item.priority} />
|
|
171
|
-
<StatusBadge
|
|
172
|
-
status={item.status}
|
|
173
|
-
onStatusChange={(status) => onItemUpdate(item.id, { status })}
|
|
174
|
-
/>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
|
|
178
|
-
{showRefine && (
|
|
179
|
-
<div style={{ marginLeft: `${depth * 20 + 28}px` }}>
|
|
180
|
-
<RefinePopover
|
|
181
|
-
itemId={item.id}
|
|
182
|
-
projectId={projectId}
|
|
183
|
-
title={item.title}
|
|
184
|
-
description={item.description}
|
|
185
|
-
onClose={() => setShowRefine(false)}
|
|
186
|
-
onItemUpdate={onItemUpdate}
|
|
187
|
-
onTreeRefresh={onTreeRefresh}
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{showDetail && (
|
|
193
|
-
<ItemDetail
|
|
194
|
-
itemId={item.id}
|
|
195
|
-
projectId={projectId}
|
|
196
|
-
title={item.title}
|
|
197
|
-
description={item.description}
|
|
198
|
-
itemType={item.item_type}
|
|
199
|
-
priority={item.priority}
|
|
200
|
-
status={item.status}
|
|
201
|
-
isLocked={item.is_locked}
|
|
202
|
-
depth={depth}
|
|
203
|
-
/>
|
|
204
|
-
)}
|
|
205
|
-
|
|
206
|
-
{expanded && hasChildren && (
|
|
207
|
-
<div className={depth === 0 ? 'tree-children-group' : ''}>
|
|
208
|
-
{item.children.map((child) => (
|
|
209
|
-
<TreeNode
|
|
210
|
-
key={child.id}
|
|
211
|
-
item={child}
|
|
212
|
-
depth={depth + 1}
|
|
213
|
-
projectId={projectId}
|
|
214
|
-
onItemUpdate={onItemUpdate}
|
|
215
|
-
onItemDelete={onItemDelete}
|
|
216
|
-
onTreeRefresh={onTreeRefresh}
|
|
217
|
-
selectMode={selectMode}
|
|
218
|
-
selected={selected}
|
|
219
|
-
onToggleSelect={onToggleSelect}
|
|
220
|
-
defaultExpanded={defaultExpanded}
|
|
221
|
-
/>
|
|
222
|
-
))}
|
|
223
|
-
</div>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import TreeNode from './TreeNode';
|
|
5
|
-
import CardView from './CardView';
|
|
6
|
-
|
|
7
|
-
interface IItemTree {
|
|
8
|
-
id: string;
|
|
9
|
-
title: string;
|
|
10
|
-
description: string;
|
|
11
|
-
item_type: string;
|
|
12
|
-
priority: string;
|
|
13
|
-
status: string;
|
|
14
|
-
is_locked: boolean;
|
|
15
|
-
is_pinned: boolean;
|
|
16
|
-
children: IItemTree[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface TreeViewProps {
|
|
20
|
-
items: IItemTree[];
|
|
21
|
-
loading: boolean;
|
|
22
|
-
projectId: string;
|
|
23
|
-
onItemUpdate: (itemId: string, data: Record<string, unknown>) => void;
|
|
24
|
-
onItemDelete: (itemId: string) => void;
|
|
25
|
-
onBulkDelete: (itemIds: string[] | 'all') => void;
|
|
26
|
-
onBulkStatus: (status: string) => void;
|
|
27
|
-
onTreeRefresh: (tree: IItemTree[]) => void;
|
|
28
|
-
onCleanup?: () => void;
|
|
29
|
-
cleaning?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type ViewMode = 'tree' | 'card';
|
|
33
|
-
|
|
34
|
-
function collectIds(item: IItemTree): string[] {
|
|
35
|
-
return [item.id, ...item.children.flatMap(collectIds)];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function filterDone(items: IItemTree[]): IItemTree[] {
|
|
39
|
-
return items
|
|
40
|
-
.filter(item => item.status !== 'done')
|
|
41
|
-
.map(item => ({
|
|
42
|
-
...item,
|
|
43
|
-
children: filterDone(item.children),
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default function TreeView({ items, loading, projectId, onItemUpdate, onItemDelete, onBulkDelete, onBulkStatus, onTreeRefresh, onCleanup, cleaning }: TreeViewProps) {
|
|
48
|
-
const [selectMode, setSelectMode] = useState(false);
|
|
49
|
-
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
50
|
-
const [hideDone, setHideDone] = useState(() => {
|
|
51
|
-
if (typeof window !== 'undefined') {
|
|
52
|
-
return localStorage.getItem('im-hide-done') !== 'false';
|
|
53
|
-
}
|
|
54
|
-
return true;
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
localStorage.setItem('im-hide-done', String(hideDone));
|
|
59
|
-
}, [hideDone]);
|
|
60
|
-
const [viewMode, setViewMode] = useState<ViewMode>('card');
|
|
61
|
-
const [collapseAll, setCollapseAll] = useState(false);
|
|
62
|
-
const [collapseKey, setCollapseKey] = useState(0);
|
|
63
|
-
|
|
64
|
-
const toggleSelect = (id: string) => {
|
|
65
|
-
setSelected(prev => {
|
|
66
|
-
const next = new Set(prev);
|
|
67
|
-
if (next.has(id)) {
|
|
68
|
-
next.delete(id);
|
|
69
|
-
} else {
|
|
70
|
-
next.add(id);
|
|
71
|
-
}
|
|
72
|
-
return next;
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const handleDeleteSelected = () => {
|
|
77
|
-
if (selected.size === 0) return;
|
|
78
|
-
const ids = Array.from(selected);
|
|
79
|
-
onBulkDelete(ids);
|
|
80
|
-
setSelected(new Set());
|
|
81
|
-
setSelectMode(false);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const handleDeleteAll = () => {
|
|
85
|
-
onBulkDelete('all');
|
|
86
|
-
setSelected(new Set());
|
|
87
|
-
setSelectMode(false);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const handleSelectAll = () => {
|
|
91
|
-
const allIds = items.flatMap(collectIds);
|
|
92
|
-
setSelected(new Set(allIds));
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const totalCount = items.reduce((sum, item) => sum + 1 + countChildren(item), 0);
|
|
96
|
-
const doneCount = countByStatus(items, 'done');
|
|
97
|
-
const displayItems = hideDone ? filterDone(items) : items;
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div className="flex flex-col h-full">
|
|
101
|
-
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
|
102
|
-
<div className="flex items-center gap-2">
|
|
103
|
-
<h2 className="text-sm font-medium text-muted-foreground">구조화 뷰</h2>
|
|
104
|
-
{totalCount > 0 && (
|
|
105
|
-
<span className="text-xs text-muted-foreground/60">{totalCount}</span>
|
|
106
|
-
)}
|
|
107
|
-
{doneCount > 0 && (
|
|
108
|
-
<button
|
|
109
|
-
onClick={() => setHideDone(!hideDone)}
|
|
110
|
-
className={`text-xs px-1.5 py-0.5 rounded transition-colors ${
|
|
111
|
-
hideDone
|
|
112
|
-
? 'bg-accent/15 text-accent'
|
|
113
|
-
: 'text-muted-foreground/50 hover:text-muted-foreground'
|
|
114
|
-
}`}
|
|
115
|
-
title={hideDone ? '완료 항목 표시' : '완료 항목 숨기기'}
|
|
116
|
-
>
|
|
117
|
-
{hideDone ? `+${doneCount} 숨김` : `${doneCount} 완료`}
|
|
118
|
-
</button>
|
|
119
|
-
)}
|
|
120
|
-
{loading && (
|
|
121
|
-
<span className="text-xs text-accent animate-pulse">AI 분석 중...</span>
|
|
122
|
-
)}
|
|
123
|
-
{cleaning && !loading && (
|
|
124
|
-
<span className="text-xs text-muted-foreground animate-pulse">정리 중...</span>
|
|
125
|
-
)}
|
|
126
|
-
</div>
|
|
127
|
-
<div className="flex items-center gap-2">
|
|
128
|
-
{/* View mode toggle */}
|
|
129
|
-
<div className="view-toggle">
|
|
130
|
-
<button
|
|
131
|
-
onClick={() => setViewMode('card')}
|
|
132
|
-
className={`view-toggle-btn ${viewMode === 'card' ? 'view-toggle-btn-active' : ''}`}
|
|
133
|
-
>
|
|
134
|
-
카드
|
|
135
|
-
</button>
|
|
136
|
-
<button
|
|
137
|
-
onClick={() => setViewMode('tree')}
|
|
138
|
-
className={`view-toggle-btn ${viewMode === 'tree' ? 'view-toggle-btn-active' : ''}`}
|
|
139
|
-
>
|
|
140
|
-
트리
|
|
141
|
-
</button>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{items.length > 0 && viewMode === 'tree' && (
|
|
145
|
-
<div className="flex items-center gap-1">
|
|
146
|
-
{selectMode ? (
|
|
147
|
-
<>
|
|
148
|
-
<button
|
|
149
|
-
onClick={handleSelectAll}
|
|
150
|
-
className="text-xs px-2 py-1 text-muted-foreground hover:text-foreground rounded transition-colors"
|
|
151
|
-
>
|
|
152
|
-
전체선택
|
|
153
|
-
</button>
|
|
154
|
-
<button
|
|
155
|
-
onClick={handleDeleteSelected}
|
|
156
|
-
disabled={selected.size === 0}
|
|
157
|
-
className="text-xs px-2 py-1 text-destructive hover:bg-destructive/10 rounded transition-colors disabled:opacity-30"
|
|
158
|
-
>
|
|
159
|
-
선택삭제 ({selected.size})
|
|
160
|
-
</button>
|
|
161
|
-
<button
|
|
162
|
-
onClick={() => { setSelectMode(false); setSelected(new Set()); }}
|
|
163
|
-
className="text-xs px-2 py-1 text-muted-foreground hover:text-foreground rounded transition-colors"
|
|
164
|
-
>
|
|
165
|
-
취소
|
|
166
|
-
</button>
|
|
167
|
-
</>
|
|
168
|
-
) : (
|
|
169
|
-
<>
|
|
170
|
-
{onCleanup && (
|
|
171
|
-
<button
|
|
172
|
-
onClick={onCleanup}
|
|
173
|
-
disabled={cleaning || loading}
|
|
174
|
-
className="text-xs px-2 py-1 text-accent hover:bg-accent/10 rounded transition-colors disabled:opacity-30"
|
|
175
|
-
>
|
|
176
|
-
정리
|
|
177
|
-
</button>
|
|
178
|
-
)}
|
|
179
|
-
<button
|
|
180
|
-
onClick={() => { setCollapseAll(!collapseAll); setCollapseKey(k => k + 1); }}
|
|
181
|
-
className="text-xs px-2 py-1 text-muted-foreground hover:text-foreground rounded transition-colors"
|
|
182
|
-
title={collapseAll ? '전체 펼치기' : '전체 접기'}
|
|
183
|
-
>
|
|
184
|
-
{collapseAll ? '펼치기' : '접기'}
|
|
185
|
-
</button>
|
|
186
|
-
<button
|
|
187
|
-
onClick={() => onBulkStatus('done')}
|
|
188
|
-
className="text-xs px-2 py-1 text-success hover:bg-success/10 rounded transition-colors"
|
|
189
|
-
>
|
|
190
|
-
전체완료
|
|
191
|
-
</button>
|
|
192
|
-
<button
|
|
193
|
-
onClick={() => setSelectMode(true)}
|
|
194
|
-
className="text-xs px-2 py-1 text-muted-foreground hover:text-foreground rounded transition-colors"
|
|
195
|
-
>
|
|
196
|
-
선택
|
|
197
|
-
</button>
|
|
198
|
-
<button
|
|
199
|
-
onClick={handleDeleteAll}
|
|
200
|
-
className="text-xs px-2 py-1 text-destructive hover:bg-destructive/10 rounded transition-colors"
|
|
201
|
-
>
|
|
202
|
-
전체삭제
|
|
203
|
-
</button>
|
|
204
|
-
</>
|
|
205
|
-
)}
|
|
206
|
-
</div>
|
|
207
|
-
)}
|
|
208
|
-
|
|
209
|
-
{items.length > 0 && viewMode === 'card' && (
|
|
210
|
-
<div className="flex items-center gap-1">
|
|
211
|
-
{onCleanup && (
|
|
212
|
-
<button
|
|
213
|
-
onClick={onCleanup}
|
|
214
|
-
disabled={cleaning || loading}
|
|
215
|
-
className="text-xs px-2 py-1 text-accent hover:bg-accent/10 rounded transition-colors disabled:opacity-30"
|
|
216
|
-
>
|
|
217
|
-
정리
|
|
218
|
-
</button>
|
|
219
|
-
)}
|
|
220
|
-
<button
|
|
221
|
-
onClick={() => onBulkStatus('done')}
|
|
222
|
-
className="text-xs px-2 py-1 text-success hover:bg-success/10 rounded transition-colors"
|
|
223
|
-
>
|
|
224
|
-
전체완료
|
|
225
|
-
</button>
|
|
226
|
-
<button
|
|
227
|
-
onClick={handleDeleteAll}
|
|
228
|
-
className="text-xs px-2 py-1 text-destructive hover:bg-destructive/10 rounded transition-colors"
|
|
229
|
-
>
|
|
230
|
-
전체삭제
|
|
231
|
-
</button>
|
|
232
|
-
</div>
|
|
233
|
-
)}
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
<div className="flex-1 overflow-auto">
|
|
238
|
-
{displayItems.length === 0 && !loading ? (
|
|
239
|
-
<div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm p-4">
|
|
240
|
-
{items.length > 0 && hideDone ? (
|
|
241
|
-
<>
|
|
242
|
-
<div className="text-4xl mb-3">✅</div>
|
|
243
|
-
<p className="mb-2">모든 항목이 완료되었습니다</p>
|
|
244
|
-
<button
|
|
245
|
-
onClick={() => setHideDone(false)}
|
|
246
|
-
className="text-xs text-accent hover:underline"
|
|
247
|
-
>
|
|
248
|
-
완료 항목 보기
|
|
249
|
-
</button>
|
|
250
|
-
</>
|
|
251
|
-
) : (
|
|
252
|
-
<>
|
|
253
|
-
<div className="text-4xl mb-3">🗂</div>
|
|
254
|
-
<p className="mb-2">아직 구조화된 항목이 없습니다</p>
|
|
255
|
-
<p className="text-xs text-center">
|
|
256
|
-
왼쪽 패널에서 아이디어를 입력해보세요.
|
|
257
|
-
<br />
|
|
258
|
-
입력을 멈추면 3초 후 AI가 자동으로 구조화합니다.
|
|
259
|
-
</p>
|
|
260
|
-
</>
|
|
261
|
-
)}
|
|
262
|
-
</div>
|
|
263
|
-
) : viewMode === 'card' ? (
|
|
264
|
-
<CardView
|
|
265
|
-
items={displayItems}
|
|
266
|
-
onItemUpdate={onItemUpdate}
|
|
267
|
-
onItemDelete={onItemDelete}
|
|
268
|
-
/>
|
|
269
|
-
) : (
|
|
270
|
-
<div className="p-2">
|
|
271
|
-
{displayItems.map((item) => (
|
|
272
|
-
<TreeNode
|
|
273
|
-
key={`${item.id}-${collapseKey}`}
|
|
274
|
-
item={item}
|
|
275
|
-
depth={0}
|
|
276
|
-
projectId={projectId}
|
|
277
|
-
onItemUpdate={onItemUpdate}
|
|
278
|
-
onItemDelete={onItemDelete}
|
|
279
|
-
onTreeRefresh={onTreeRefresh}
|
|
280
|
-
selectMode={selectMode}
|
|
281
|
-
selected={selected}
|
|
282
|
-
onToggleSelect={toggleSelect}
|
|
283
|
-
defaultExpanded={!collapseAll}
|
|
284
|
-
/>
|
|
285
|
-
))}
|
|
286
|
-
</div>
|
|
287
|
-
)}
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function countChildren(item: IItemTree): number {
|
|
294
|
-
return item.children.reduce((sum, child) => sum + 1 + countChildren(child), 0);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function countByStatus(items: IItemTree[], status: string): number {
|
|
298
|
-
let count = 0;
|
|
299
|
-
for (const item of items) {
|
|
300
|
-
if (item.status === status) count++;
|
|
301
|
-
count += countByStatus(item.children, status);
|
|
302
|
-
}
|
|
303
|
-
return count;
|
|
304
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { runStructureWithQuestions, type IStructuredItem } from './client';
|
|
2
|
-
import { replaceItems } from '../db/queries/items';
|
|
3
|
-
import { getRecentConversations, addMessage } from '../db/queries/conversations';
|
|
4
|
-
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
5
|
-
import { getProjectContextSummary } from '../db/queries/context';
|
|
6
|
-
import { resolveMemos, createMemosFromQuestions } from '../db/queries/memos';
|
|
7
|
-
import type { IItemTree, IMemo, IConversation } from '@/types';
|
|
8
|
-
|
|
9
|
-
export async function handleChatResponse(
|
|
10
|
-
projectId: string,
|
|
11
|
-
brainstormId: string,
|
|
12
|
-
userMessage: string,
|
|
13
|
-
): Promise<{ items: IItemTree[]; memos: IMemo[]; messages: IConversation[] }> {
|
|
14
|
-
// Save user message
|
|
15
|
-
const userMsg = addMessage(projectId, 'user', userMessage);
|
|
16
|
-
|
|
17
|
-
// Load brainstorm content
|
|
18
|
-
const brainstorm = getBrainstorm(projectId);
|
|
19
|
-
if (!brainstorm || !brainstorm.content.trim()) {
|
|
20
|
-
return { items: [], memos: [], messages: [userMsg] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Resolve old memos before generating new ones
|
|
24
|
-
resolveMemos(projectId);
|
|
25
|
-
|
|
26
|
-
// Load full conversation history (limited to 20)
|
|
27
|
-
const history = getRecentConversations(projectId, 20);
|
|
28
|
-
const historyForAi = history.map(h => ({
|
|
29
|
-
role: h.role,
|
|
30
|
-
content: h.content,
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
// AI call with updated conversation context + project docs
|
|
34
|
-
const projectContext = getProjectContextSummary(projectId) || undefined;
|
|
35
|
-
const result = await runStructureWithQuestions(brainstorm.content, historyForAi, projectContext);
|
|
36
|
-
|
|
37
|
-
// Replace items in DB
|
|
38
|
-
const dbItems = mapToDbFormat(result.items as IStructuredItem[]);
|
|
39
|
-
const tree = replaceItems(projectId, brainstormId, dbItems);
|
|
40
|
-
|
|
41
|
-
// Build AI response + new memos
|
|
42
|
-
const newMessages: IConversation[] = [userMsg];
|
|
43
|
-
let memos: IMemo[] = [];
|
|
44
|
-
|
|
45
|
-
if (result.questions.length > 0) {
|
|
46
|
-
const messageContent = result.questions
|
|
47
|
-
.map((q, i) => `${i + 1}. ${q.question}`)
|
|
48
|
-
.join('\n');
|
|
49
|
-
|
|
50
|
-
const aiMsg = addMessage(projectId, 'assistant', messageContent);
|
|
51
|
-
newMessages.push(aiMsg);
|
|
52
|
-
memos = createMemosFromQuestions(projectId, aiMsg.id, result.questions);
|
|
53
|
-
} else {
|
|
54
|
-
// Even without questions, acknowledge the refinement
|
|
55
|
-
const aiMsg = addMessage(projectId, 'assistant', '답변을 반영하여 구조를 업데이트했습니다.');
|
|
56
|
-
newMessages.push(aiMsg);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return { items: tree, memos, messages: newMessages };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function mapToDbFormat(items: IStructuredItem[]): Parameters<typeof replaceItems>[2] {
|
|
63
|
-
return items.map((item) => ({
|
|
64
|
-
parent_id: null,
|
|
65
|
-
title: item.title,
|
|
66
|
-
description: item.description,
|
|
67
|
-
item_type: item.item_type,
|
|
68
|
-
priority: item.priority,
|
|
69
|
-
children: item.children ? mapToDbFormat(item.children) : undefined,
|
|
70
|
-
}));
|
|
71
|
-
}
|