idea-manager 0.1.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/README.md +36 -0
- package/bin/im.js +4 -0
- package/next.config.ts +8 -0
- package/package.json +55 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/health/route.ts +5 -0
- package/src/app/api/projects/[id]/brainstorm/route.ts +37 -0
- package/src/app/api/projects/[id]/conversations/route.ts +50 -0
- package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +51 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +73 -0
- package/src/app/api/projects/[id]/items/route.ts +17 -0
- package/src/app/api/projects/[id]/memos/route.ts +18 -0
- package/src/app/api/projects/[id]/route.ts +39 -0
- package/src/app/api/projects/[id]/structure/route.ts +28 -0
- package/src/app/api/projects/route.ts +19 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +437 -0
- package/src/app/layout.tsx +42 -0
- package/src/app/page.tsx +175 -0
- package/src/app/projects/[id]/page.tsx +249 -0
- package/src/cli.ts +41 -0
- package/src/components/brainstorm/Editor.tsx +163 -0
- package/src/components/brainstorm/MemoPin.tsx +31 -0
- package/src/components/brainstorm/ResizeHandle.tsx +45 -0
- package/src/components/chat/ChatMessage.tsx +28 -0
- package/src/components/chat/ChatPanel.tsx +100 -0
- package/src/components/tree/ItemDetail.tsx +196 -0
- package/src/components/tree/LockToggle.tsx +23 -0
- package/src/components/tree/StatusBadge.tsx +32 -0
- package/src/components/tree/TreeNode.tsx +118 -0
- package/src/components/tree/TreeView.tsx +60 -0
- package/src/lib/ai/chat-responder.ts +69 -0
- package/src/lib/ai/client.ts +124 -0
- package/src/lib/ai/prompter.ts +83 -0
- package/src/lib/ai/structurer.ts +74 -0
- package/src/lib/db/index.ts +16 -0
- package/src/lib/db/queries/brainstorms.ts +26 -0
- package/src/lib/db/queries/conversations.ts +46 -0
- package/src/lib/db/queries/items.ts +147 -0
- package/src/lib/db/queries/memos.ts +66 -0
- package/src/lib/db/queries/projects.ts +53 -0
- package/src/lib/db/queries/prompts.ts +68 -0
- package/src/lib/db/schema.ts +78 -0
- package/src/lib/mcp/server.ts +117 -0
- package/src/lib/mcp/tools.ts +83 -0
- package/src/lib/utils/id.ts +5 -0
- package/src/lib/utils/paths.ts +16 -0
- package/src/types/index.ts +97 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: 224 20% 10%;
|
|
5
|
+
--foreground: 210 40% 98%;
|
|
6
|
+
--card: 224 20% 13%;
|
|
7
|
+
--card-hover: 220 15% 18%;
|
|
8
|
+
--border: 220 15% 28%;
|
|
9
|
+
--input: 220 15% 24%;
|
|
10
|
+
--primary: 210 85% 60%;
|
|
11
|
+
--primary-hover: 210 85% 50%;
|
|
12
|
+
--accent: 265 70% 60%;
|
|
13
|
+
--success: 142 71% 45%;
|
|
14
|
+
--warning: 38 92% 50%;
|
|
15
|
+
--destructive: 0 70% 55%;
|
|
16
|
+
--muted: 220 15% 20%;
|
|
17
|
+
--muted-foreground: 215 15% 60%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@theme inline {
|
|
21
|
+
--color-background: hsl(var(--background));
|
|
22
|
+
--color-foreground: hsl(var(--foreground));
|
|
23
|
+
--color-card: hsl(var(--card));
|
|
24
|
+
--color-card-hover: hsl(var(--card-hover));
|
|
25
|
+
--color-border: hsl(var(--border));
|
|
26
|
+
--color-input: hsl(var(--input));
|
|
27
|
+
--color-primary: hsl(var(--primary));
|
|
28
|
+
--color-primary-hover: hsl(var(--primary-hover));
|
|
29
|
+
--color-accent: hsl(var(--accent));
|
|
30
|
+
--color-success: hsl(var(--success));
|
|
31
|
+
--color-warning: hsl(var(--warning));
|
|
32
|
+
--color-destructive: hsl(var(--destructive));
|
|
33
|
+
--color-muted: hsl(var(--muted));
|
|
34
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
35
|
+
--font-sans: 'Pretendard', var(--font-geist-sans), system-ui, sans-serif;
|
|
36
|
+
--font-mono: var(--font-geist-mono);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
body {
|
|
40
|
+
background: hsl(var(--background));
|
|
41
|
+
color: hsl(var(--foreground));
|
|
42
|
+
font-family: var(--font-sans);
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
-webkit-font-smoothing: antialiased;
|
|
45
|
+
-moz-osx-font-smoothing: grayscale;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Scrollbar */
|
|
49
|
+
::-webkit-scrollbar {
|
|
50
|
+
width: 6px;
|
|
51
|
+
}
|
|
52
|
+
::-webkit-scrollbar-track {
|
|
53
|
+
background: hsl(var(--background));
|
|
54
|
+
}
|
|
55
|
+
::-webkit-scrollbar-thumb {
|
|
56
|
+
background: hsl(var(--border));
|
|
57
|
+
border-radius: 3px;
|
|
58
|
+
}
|
|
59
|
+
::-webkit-scrollbar-thumb:hover {
|
|
60
|
+
background: hsl(var(--muted-foreground));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Input/Textarea focus ring */
|
|
64
|
+
input:focus,
|
|
65
|
+
textarea:focus {
|
|
66
|
+
outline: none;
|
|
67
|
+
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.3);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Editor container with overlay support */
|
|
71
|
+
.editor-container {
|
|
72
|
+
position: relative;
|
|
73
|
+
flex: 1;
|
|
74
|
+
display: flex;
|
|
75
|
+
min-height: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Memo overlay */
|
|
79
|
+
.memo-overlay {
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 0;
|
|
82
|
+
left: 0;
|
|
83
|
+
right: 0;
|
|
84
|
+
bottom: 0;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.memo-pin {
|
|
90
|
+
position: absolute;
|
|
91
|
+
pointer-events: auto;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
z-index: 10;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.memo-pin-icon {
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3));
|
|
99
|
+
transition: transform 0.15s;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.memo-pin:hover .memo-pin-icon {
|
|
103
|
+
transform: scale(1.3);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.memo-tooltip {
|
|
107
|
+
position: absolute;
|
|
108
|
+
right: 24px;
|
|
109
|
+
top: -4px;
|
|
110
|
+
width: 240px;
|
|
111
|
+
padding: 8px 12px;
|
|
112
|
+
background: hsl(var(--card));
|
|
113
|
+
border: 1px solid hsl(var(--border));
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
116
|
+
z-index: 20;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.memo-tooltip-anchor {
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
color: hsl(var(--muted-foreground));
|
|
122
|
+
font-style: italic;
|
|
123
|
+
margin-bottom: 4px;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
text-overflow: ellipsis;
|
|
126
|
+
white-space: nowrap;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.memo-tooltip-question {
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
color: hsl(var(--foreground));
|
|
132
|
+
line-height: 1.4;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Resize handle */
|
|
136
|
+
.resize-handle {
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
height: 6px;
|
|
139
|
+
cursor: row-resize;
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
background: hsl(var(--background));
|
|
144
|
+
border-top: 1px solid hsl(var(--border));
|
|
145
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
146
|
+
transition: background 0.15s;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.resize-handle:hover {
|
|
150
|
+
background: hsl(var(--muted));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.resize-handle-bar {
|
|
154
|
+
width: 32px;
|
|
155
|
+
height: 2px;
|
|
156
|
+
border-radius: 1px;
|
|
157
|
+
background: hsl(var(--muted-foreground) / 0.4);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.resize-handle:hover .resize-handle-bar {
|
|
161
|
+
background: hsl(var(--muted-foreground));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Chat panel */
|
|
165
|
+
.chat-panel {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
height: 100%;
|
|
169
|
+
min-height: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.chat-header {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
padding: 8px 16px;
|
|
177
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
178
|
+
flex-shrink: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.chat-messages {
|
|
182
|
+
flex: 1;
|
|
183
|
+
overflow-y: auto;
|
|
184
|
+
padding: 12px 16px;
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
min-height: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.chat-empty {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: center;
|
|
195
|
+
height: 100%;
|
|
196
|
+
color: hsl(var(--muted-foreground));
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Chat messages */
|
|
201
|
+
.chat-message {
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
max-width: 85%;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.chat-message-ai {
|
|
208
|
+
align-self: flex-start;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.chat-message-user {
|
|
212
|
+
align-self: flex-end;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.chat-bubble {
|
|
216
|
+
padding: 8px 12px;
|
|
217
|
+
border-radius: 12px;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
line-height: 1.5;
|
|
220
|
+
word-break: break-word;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.chat-bubble-ai {
|
|
224
|
+
background: hsl(var(--muted));
|
|
225
|
+
color: hsl(var(--foreground));
|
|
226
|
+
border-bottom-left-radius: 4px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.chat-bubble-user {
|
|
230
|
+
background: hsl(var(--accent));
|
|
231
|
+
color: white;
|
|
232
|
+
border-bottom-right-radius: 4px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.chat-time {
|
|
236
|
+
font-size: 10px;
|
|
237
|
+
color: hsl(var(--muted-foreground));
|
|
238
|
+
margin-top: 2px;
|
|
239
|
+
padding: 0 4px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.chat-message-user .chat-time {
|
|
243
|
+
text-align: right;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Loading dots */
|
|
247
|
+
.chat-loading {
|
|
248
|
+
display: flex;
|
|
249
|
+
gap: 4px;
|
|
250
|
+
padding: 12px 16px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.chat-loading .dot {
|
|
254
|
+
width: 6px;
|
|
255
|
+
height: 6px;
|
|
256
|
+
border-radius: 50%;
|
|
257
|
+
background: hsl(var(--muted-foreground));
|
|
258
|
+
animation: chatDotBounce 1.4s infinite ease-in-out both;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.chat-loading .dot:nth-child(1) { animation-delay: -0.32s; }
|
|
262
|
+
.chat-loading .dot:nth-child(2) { animation-delay: -0.16s; }
|
|
263
|
+
.chat-loading .dot:nth-child(3) { animation-delay: 0s; }
|
|
264
|
+
|
|
265
|
+
@keyframes chatDotBounce {
|
|
266
|
+
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
|
267
|
+
40% { transform: scale(1); opacity: 1; }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Chat input */
|
|
271
|
+
.chat-input-area {
|
|
272
|
+
display: flex;
|
|
273
|
+
gap: 8px;
|
|
274
|
+
padding: 8px 12px;
|
|
275
|
+
border-top: 1px solid hsl(var(--border));
|
|
276
|
+
flex-shrink: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.chat-input {
|
|
280
|
+
flex: 1;
|
|
281
|
+
padding: 6px 12px;
|
|
282
|
+
background: hsl(var(--input));
|
|
283
|
+
border: 1px solid hsl(var(--border));
|
|
284
|
+
border-radius: 8px;
|
|
285
|
+
color: hsl(var(--foreground));
|
|
286
|
+
font-size: 13px;
|
|
287
|
+
resize: none;
|
|
288
|
+
font-family: var(--font-sans);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.chat-input::placeholder {
|
|
292
|
+
color: hsl(var(--muted-foreground) / 0.5);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.chat-send-btn {
|
|
296
|
+
padding: 6px 14px;
|
|
297
|
+
background: hsl(var(--accent));
|
|
298
|
+
color: white;
|
|
299
|
+
border: none;
|
|
300
|
+
border-radius: 8px;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
transition: opacity 0.15s;
|
|
305
|
+
flex-shrink: 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.chat-send-btn:hover:not(:disabled) {
|
|
309
|
+
opacity: 0.85;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.chat-send-btn:disabled {
|
|
313
|
+
opacity: 0.4;
|
|
314
|
+
cursor: not-allowed;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Lock toggle */
|
|
318
|
+
.lock-toggle {
|
|
319
|
+
background: none;
|
|
320
|
+
border: none;
|
|
321
|
+
cursor: pointer;
|
|
322
|
+
font-size: 14px;
|
|
323
|
+
padding: 0;
|
|
324
|
+
flex-shrink: 0;
|
|
325
|
+
transition: transform 0.15s;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.lock-toggle:hover:not(:disabled) {
|
|
329
|
+
transform: scale(1.2);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.lock-toggle:disabled {
|
|
333
|
+
opacity: 0.4;
|
|
334
|
+
cursor: not-allowed;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Status badge */
|
|
338
|
+
.status-badge {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 2px;
|
|
342
|
+
background: none;
|
|
343
|
+
border: none;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
padding: 1px 4px;
|
|
347
|
+
border-radius: 4px;
|
|
348
|
+
flex-shrink: 0;
|
|
349
|
+
transition: background 0.15s;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.status-badge:hover:not(:disabled) {
|
|
353
|
+
background: hsl(var(--muted));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.status-badge-label {
|
|
357
|
+
font-size: 10px;
|
|
358
|
+
color: hsl(var(--muted-foreground));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Item detail */
|
|
362
|
+
.item-detail {
|
|
363
|
+
margin: 0 8px 8px;
|
|
364
|
+
padding: 12px;
|
|
365
|
+
background: hsl(var(--card));
|
|
366
|
+
border: 1px solid hsl(var(--border));
|
|
367
|
+
border-radius: 8px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* Prompt section */
|
|
371
|
+
.prompt-section {
|
|
372
|
+
border-top: 1px solid hsl(var(--border));
|
|
373
|
+
padding-top: 8px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.prompt-header {
|
|
377
|
+
display: flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
justify-content: space-between;
|
|
380
|
+
margin-bottom: 6px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.prompt-action-btn {
|
|
384
|
+
padding: 2px 8px;
|
|
385
|
+
font-size: 10px;
|
|
386
|
+
color: hsl(var(--muted-foreground));
|
|
387
|
+
background: hsl(var(--muted));
|
|
388
|
+
border: 1px solid hsl(var(--border));
|
|
389
|
+
border-radius: 4px;
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
transition: all 0.15s;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.prompt-action-btn:hover:not(:disabled) {
|
|
395
|
+
color: hsl(var(--foreground));
|
|
396
|
+
background: hsl(var(--card-hover));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.prompt-action-btn:disabled {
|
|
400
|
+
opacity: 0.4;
|
|
401
|
+
cursor: not-allowed;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.prompt-generate-btn {
|
|
405
|
+
color: hsl(var(--accent));
|
|
406
|
+
border-color: hsl(var(--accent) / 0.3);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.prompt-generate-btn:hover:not(:disabled) {
|
|
410
|
+
background: hsl(var(--accent) / 0.1);
|
|
411
|
+
color: hsl(var(--accent));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.prompt-content {
|
|
415
|
+
font-size: 12px;
|
|
416
|
+
line-height: 1.6;
|
|
417
|
+
color: hsl(var(--foreground));
|
|
418
|
+
background: hsl(var(--background));
|
|
419
|
+
padding: 8px 12px;
|
|
420
|
+
border-radius: 6px;
|
|
421
|
+
border: 1px solid hsl(var(--border));
|
|
422
|
+
white-space: pre-wrap;
|
|
423
|
+
font-family: var(--font-mono);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.prompt-edit-textarea {
|
|
427
|
+
width: 100%;
|
|
428
|
+
padding: 8px 12px;
|
|
429
|
+
font-size: 12px;
|
|
430
|
+
line-height: 1.6;
|
|
431
|
+
color: hsl(var(--foreground));
|
|
432
|
+
background: hsl(var(--background));
|
|
433
|
+
border: 1px solid hsl(var(--accent) / 0.5);
|
|
434
|
+
border-radius: 6px;
|
|
435
|
+
resize: vertical;
|
|
436
|
+
font-family: var(--font-mono);
|
|
437
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const geistSans = Geist({
|
|
6
|
+
variable: "--font-geist-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const geistMono = Geist_Mono({
|
|
11
|
+
variable: "--font-geist-mono",
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: "IM - 아이디어 매니저",
|
|
17
|
+
description: "자유롭게 아이디어를 쏟아내면, AI가 구조화해드립니다",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html lang="ko">
|
|
27
|
+
<head>
|
|
28
|
+
<link
|
|
29
|
+
rel="stylesheet"
|
|
30
|
+
as="style"
|
|
31
|
+
crossOrigin="anonymous"
|
|
32
|
+
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
|
|
33
|
+
/>
|
|
34
|
+
</head>
|
|
35
|
+
<body
|
|
36
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
interface IProject {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
created_at: string;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function Dashboard() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const [projects, setProjects] = useState<IProject[]>([]);
|
|
17
|
+
const [showForm, setShowForm] = useState(false);
|
|
18
|
+
const [name, setName] = useState('');
|
|
19
|
+
const [description, setDescription] = useState('');
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
|
|
22
|
+
const fetchProjects = useCallback(async () => {
|
|
23
|
+
const res = await fetch('/api/projects');
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
setProjects(data);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
fetchProjects();
|
|
31
|
+
}, [fetchProjects]);
|
|
32
|
+
|
|
33
|
+
const handleCreate = async (e: React.FormEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (!name.trim()) return;
|
|
36
|
+
|
|
37
|
+
const res = await fetch('/api/projects', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ name: name.trim(), description: description.trim() }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (res.ok) {
|
|
44
|
+
const project = await res.json();
|
|
45
|
+
setName('');
|
|
46
|
+
setDescription('');
|
|
47
|
+
setShowForm(false);
|
|
48
|
+
router.push(`/projects/${project.id}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
if (!confirm('이 프로젝트를 삭제하시겠습니까?')) return;
|
|
55
|
+
|
|
56
|
+
await fetch(`/api/projects/${id}`, { method: 'DELETE' });
|
|
57
|
+
fetchProjects();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const formatDate = (dateStr: string) => {
|
|
61
|
+
return new Date(dateStr).toLocaleDateString('ko-KR', {
|
|
62
|
+
year: 'numeric',
|
|
63
|
+
month: 'short',
|
|
64
|
+
day: 'numeric',
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="min-h-screen p-8 max-w-4xl mx-auto">
|
|
70
|
+
<header className="flex items-center justify-between mb-10">
|
|
71
|
+
<div>
|
|
72
|
+
<h1 className="text-3xl font-bold tracking-tight">
|
|
73
|
+
IM <span className="text-muted-foreground font-normal text-lg ml-2">아이디어 매니저</span>
|
|
74
|
+
</h1>
|
|
75
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
76
|
+
자유롭게 아이디어를 쏟아내면, AI가 구조화해드립니다
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => setShowForm(!showForm)}
|
|
81
|
+
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
|
|
82
|
+
transition-colors font-medium text-sm"
|
|
83
|
+
>
|
|
84
|
+
+ 새 프로젝트
|
|
85
|
+
</button>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
{showForm && (
|
|
89
|
+
<form
|
|
90
|
+
onSubmit={handleCreate}
|
|
91
|
+
className="mb-8 p-5 bg-card rounded-lg border border-border"
|
|
92
|
+
>
|
|
93
|
+
<input
|
|
94
|
+
type="text"
|
|
95
|
+
placeholder="프로젝트 이름"
|
|
96
|
+
value={name}
|
|
97
|
+
onChange={(e) => setName(e.target.value)}
|
|
98
|
+
className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-3
|
|
99
|
+
focus:border-primary focus:outline-none text-foreground"
|
|
100
|
+
autoFocus
|
|
101
|
+
/>
|
|
102
|
+
<input
|
|
103
|
+
type="text"
|
|
104
|
+
placeholder="설명 (선택사항)"
|
|
105
|
+
value={description}
|
|
106
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
107
|
+
className="w-full bg-input border border-border rounded-lg px-4 py-2.5 mb-4
|
|
108
|
+
focus:border-primary focus:outline-none text-foreground"
|
|
109
|
+
/>
|
|
110
|
+
<div className="flex gap-2 justify-end">
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => setShowForm(false)}
|
|
114
|
+
className="px-4 py-2 text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
115
|
+
>
|
|
116
|
+
취소
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
type="submit"
|
|
120
|
+
className="px-4 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg
|
|
121
|
+
transition-colors text-sm"
|
|
122
|
+
>
|
|
123
|
+
만들기
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</form>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{loading ? (
|
|
130
|
+
<div className="text-center text-muted-foreground py-20">로딩 중...</div>
|
|
131
|
+
) : projects.length === 0 ? (
|
|
132
|
+
<div className="text-center py-20">
|
|
133
|
+
<div className="text-5xl mb-4">💡</div>
|
|
134
|
+
<p className="text-muted-foreground text-lg mb-2">아직 프로젝트가 없습니다</p>
|
|
135
|
+
<p className="text-muted-foreground text-sm">
|
|
136
|
+
"새 프로젝트" 버튼을 눌러 시작하세요
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
) : (
|
|
140
|
+
<div className="space-y-3">
|
|
141
|
+
{projects.map((project) => (
|
|
142
|
+
<div
|
|
143
|
+
key={project.id}
|
|
144
|
+
onClick={() => router.push(`/projects/${project.id}`)}
|
|
145
|
+
className="p-5 bg-card hover:bg-card-hover border border-border rounded-lg
|
|
146
|
+
cursor-pointer transition-colors group"
|
|
147
|
+
>
|
|
148
|
+
<div className="flex items-start justify-between">
|
|
149
|
+
<div className="flex-1">
|
|
150
|
+
<h2 className="text-lg font-semibold group-hover:text-primary transition-colors">
|
|
151
|
+
{project.name}
|
|
152
|
+
</h2>
|
|
153
|
+
{project.description && (
|
|
154
|
+
<p className="text-muted-foreground text-sm mt-1">{project.description}</p>
|
|
155
|
+
)}
|
|
156
|
+
<p className="text-muted-foreground text-xs mt-2">
|
|
157
|
+
수정일 {formatDate(project.updated_at)}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
<button
|
|
161
|
+
onClick={(e) => handleDelete(project.id, e)}
|
|
162
|
+
className="text-muted-foreground hover:text-destructive transition-colors opacity-0
|
|
163
|
+
group-hover:opacity-100 p-1 text-sm"
|
|
164
|
+
title="프로젝트 삭제"
|
|
165
|
+
>
|
|
166
|
+
삭제
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|