create-arete-workspace 0.2.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 (180) hide show
  1. package/README.md +77 -0
  2. package/bin/arete.js +156 -0
  3. package/bin/create.js +111 -0
  4. package/lib/install-openclaw.js +50 -0
  5. package/lib/scaffold.js +213 -0
  6. package/lib/setup-wizard.js +88 -0
  7. package/lib/updater.js +130 -0
  8. package/package.json +34 -0
  9. package/packages/gatsaeng-os/README.md +36 -0
  10. package/packages/gatsaeng-os/components.json +23 -0
  11. package/packages/gatsaeng-os/eslint.config.mjs +18 -0
  12. package/packages/gatsaeng-os/next.config.ts +7 -0
  13. package/packages/gatsaeng-os/package.json +59 -0
  14. package/packages/gatsaeng-os/postcss.config.mjs +7 -0
  15. package/packages/gatsaeng-os/public/file.svg +1 -0
  16. package/packages/gatsaeng-os/public/globe.svg +1 -0
  17. package/packages/gatsaeng-os/public/next.svg +1 -0
  18. package/packages/gatsaeng-os/public/vercel.svg +1 -0
  19. package/packages/gatsaeng-os/public/window.svg +1 -0
  20. package/packages/gatsaeng-os/python/api_server.py +248 -0
  21. package/packages/gatsaeng-os/python/briefing.py +145 -0
  22. package/packages/gatsaeng-os/python/config.py +55 -0
  23. package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
  24. package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
  25. package/packages/gatsaeng-os/python/proactive.py +158 -0
  26. package/packages/gatsaeng-os/python/requirements.txt +11 -0
  27. package/packages/gatsaeng-os/python/run.py +28 -0
  28. package/packages/gatsaeng-os/python/scoring.py +44 -0
  29. package/packages/gatsaeng-os/python/streak.py +70 -0
  30. package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
  31. package/packages/gatsaeng-os/python/timing_engine.py +117 -0
  32. package/packages/gatsaeng-os/python/vault_io.py +423 -0
  33. package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
  34. package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
  35. package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
  36. package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
  37. package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
  38. package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
  39. package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
  40. package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
  41. package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
  42. package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
  43. package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
  44. package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
  45. package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
  46. package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
  47. package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
  48. package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
  49. package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
  50. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
  51. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
  52. package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
  53. package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
  54. package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
  55. package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
  56. package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
  57. package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
  58. package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
  59. package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
  60. package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
  61. package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
  62. package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
  63. package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
  64. package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
  65. package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
  66. package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
  67. package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
  68. package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
  69. package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
  70. package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
  71. package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
  72. package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
  73. package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
  74. package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
  75. package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
  76. package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
  77. package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
  78. package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
  79. package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
  80. package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
  81. package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
  82. package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
  83. package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
  84. package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
  85. package/packages/gatsaeng-os/src/app/error.tsx +30 -0
  86. package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
  87. package/packages/gatsaeng-os/src/app/globals.css +208 -0
  88. package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
  89. package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
  90. package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
  91. package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
  92. package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
  93. package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
  94. package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
  95. package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
  96. package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
  97. package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
  98. package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
  99. package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
  100. package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
  101. package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
  102. package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
  103. package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
  104. package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
  105. package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
  106. package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
  107. package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
  108. package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
  109. package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
  110. package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
  111. package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
  112. package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
  113. package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
  114. package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
  115. package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
  116. package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
  117. package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
  118. package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
  119. package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
  120. package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
  121. package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
  122. package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
  123. package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
  124. package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
  125. package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
  126. package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
  127. package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
  128. package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
  129. package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
  130. package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
  131. package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
  132. package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
  133. package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
  134. package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
  135. package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
  136. package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
  137. package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
  138. package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
  139. package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
  140. package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
  141. package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
  142. package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
  143. package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
  144. package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
  145. package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
  146. package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
  147. package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
  148. package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
  149. package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
  150. package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
  151. package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
  152. package/packages/gatsaeng-os/src/lib/date.ts +7 -0
  153. package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
  154. package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
  155. package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
  156. package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
  157. package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
  158. package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
  159. package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
  160. package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
  161. package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
  162. package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
  163. package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
  164. package/packages/gatsaeng-os/src/middleware.ts +34 -0
  165. package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
  166. package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
  167. package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
  168. package/packages/gatsaeng-os/src/types/index.ts +320 -0
  169. package/packages/gatsaeng-os/tsconfig.json +34 -0
  170. package/templates/scripts/forge_qa.sh.tmpl +237 -0
  171. package/templates/scripts/forge_ship.sh.tmpl +183 -0
  172. package/templates/scripts/session_indexer.py.tmpl +420 -0
  173. package/templates/scripts/tracer.py.tmpl +266 -0
  174. package/templates/workspace/AGENTS.md.tmpl +190 -0
  175. package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
  176. package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
  177. package/templates/workspace/MEMORY.md.tmpl +35 -0
  178. package/templates/workspace/SOUL.md.tmpl +258 -0
  179. package/templates/workspace/TOOLS.md.tmpl +28 -0
  180. package/templates/workspace/USER.md.tmpl +43 -0
@@ -0,0 +1,215 @@
1
+ 'use client'
2
+
3
+ import { use } from 'react'
4
+ import { useQuery } from '@tanstack/react-query'
5
+ import { useGoals } from '@/hooks/useGoals'
6
+ import { useProjects } from '@/hooks/useProjects'
7
+ import { useNotes } from '@/hooks/useNotes'
8
+ import { DDayBadge } from '@/components/tasks/DDayBadge'
9
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
10
+ import { Badge } from '@/components/ui/badge'
11
+ import { Progress } from '@/components/ui/progress'
12
+ import { Button } from '@/components/ui/button'
13
+ import Link from 'next/link'
14
+ import {
15
+ ArrowLeft,
16
+ Target,
17
+ FolderKanban,
18
+ StickyNote,
19
+ RotateCcw,
20
+ } from 'lucide-react'
21
+ import type { Area, Routine } from '@/types'
22
+
23
+ export default function AreaDetailPage({ params }: { params: Promise<{ id: string }> }) {
24
+ const { id } = use(params)
25
+
26
+ const { data: area, isLoading } = useQuery({
27
+ queryKey: ['areas', id],
28
+ queryFn: async (): Promise<Area> => {
29
+ const res = await fetch(`/api/areas/${id}`)
30
+ return res.json()
31
+ },
32
+ })
33
+
34
+ const { data: goals = [] } = useGoals()
35
+ const { data: projects = [] } = useProjects()
36
+ const { data: notes = [] } = useNotes(undefined, id)
37
+ const { data: routines = [] } = useQuery({
38
+ queryKey: ['routines'],
39
+ queryFn: async (): Promise<Routine[]> => {
40
+ const res = await fetch('/api/routines')
41
+ return res.json()
42
+ },
43
+ })
44
+
45
+ const areaGoals = goals.filter(g => g.area_id === id)
46
+ const areaProjects = projects.filter(p => areaGoals.some(g => g.linked_projects?.includes(p.id)) || p.goal_id && areaGoals.some(g => g.id === p.goal_id))
47
+ const areaRoutines = routines.filter(r => r.area_id === id || r.goal_id && areaGoals.some(g => g.id === r.goal_id))
48
+
49
+ if (isLoading) {
50
+ return (
51
+ <div className="max-w-4xl mx-auto space-y-4">
52
+ {[1, 2, 3].map(i => <div key={i} className="h-24 bg-card rounded-lg animate-pulse" />)}
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (!area) {
58
+ return (
59
+ <div className="max-w-4xl mx-auto text-center py-12 text-muted-foreground">
60
+ 영역을 찾을 수 없습니다
61
+ </div>
62
+ )
63
+ }
64
+
65
+ return (
66
+ <div className="max-w-4xl mx-auto">
67
+ <Link href="/areas" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4">
68
+ <ArrowLeft className="w-4 h-4" /> 영역 목록
69
+ </Link>
70
+
71
+ <div className="flex items-center gap-3 mb-8">
72
+ <span className="text-3xl">{area.icon}</span>
73
+ <div>
74
+ <h1 className="text-2xl font-bold text-foreground">{area.title}</h1>
75
+ <p className="text-sm text-muted-foreground">
76
+ 목표 {areaGoals.length} · 프로젝트 {areaProjects.length} · 루틴 {areaRoutines.length} · 노트 {notes.length}
77
+ </p>
78
+ </div>
79
+ </div>
80
+
81
+ <div className="space-y-8">
82
+ {/* Goals */}
83
+ <section>
84
+ <div className="flex items-center gap-2 mb-3">
85
+ <Target className="w-4 h-4 text-muted-foreground" />
86
+ <h2 className="text-sm font-medium">목표</h2>
87
+ <Badge variant="outline" className="text-[10px]">{areaGoals.length}</Badge>
88
+ </div>
89
+ {areaGoals.length === 0 ? (
90
+ <p className="text-xs text-muted-foreground pl-6">연결된 목표가 없습니다</p>
91
+ ) : (
92
+ <div className="space-y-2">
93
+ {areaGoals.map(goal => {
94
+ const progress = goal.target_value && goal.current_value !== undefined
95
+ ? Math.round(((goal.current_value ?? 0) / goal.target_value) * 100)
96
+ : null
97
+ return (
98
+ <Link key={goal.id} href={`/goals/${goal.id}`}>
99
+ <Card className="hover:border-primary/30 transition-colors">
100
+ <CardContent className="py-3 px-4">
101
+ <div className="flex items-center gap-3">
102
+ <div className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: goal.color }} />
103
+ <div className="flex-1 min-w-0">
104
+ <div className="flex items-center gap-2">
105
+ <span className="text-sm font-medium truncate">{goal.title}</span>
106
+ <Badge variant="outline" className="text-[10px]">{goal.status}</Badge>
107
+ </div>
108
+ {progress !== null && (
109
+ <div className="flex items-center gap-2 mt-1.5">
110
+ <Progress value={progress} className="h-1.5 flex-1" />
111
+ <span className="text-[10px] text-muted-foreground tabular-nums">{progress}%</span>
112
+ </div>
113
+ )}
114
+ </div>
115
+ {goal.due_date && <DDayBadge dueDate={goal.due_date} />}
116
+ </div>
117
+ </CardContent>
118
+ </Card>
119
+ </Link>
120
+ )
121
+ })}
122
+ </div>
123
+ )}
124
+ </section>
125
+
126
+ {/* Projects */}
127
+ <section>
128
+ <div className="flex items-center gap-2 mb-3">
129
+ <FolderKanban className="w-4 h-4 text-muted-foreground" />
130
+ <h2 className="text-sm font-medium">프로젝트</h2>
131
+ <Badge variant="outline" className="text-[10px]">{areaProjects.length}</Badge>
132
+ </div>
133
+ {areaProjects.length === 0 ? (
134
+ <p className="text-xs text-muted-foreground pl-6">연결된 프로젝트가 없습니다</p>
135
+ ) : (
136
+ <div className="space-y-2">
137
+ {areaProjects.map(project => (
138
+ <Link key={project.id} href={`/projects/${project.id}`}>
139
+ <Card className="hover:border-primary/30 transition-colors">
140
+ <CardContent className="py-3 px-4">
141
+ <div className="flex items-center gap-3">
142
+ <div className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: project.color }} />
143
+ <span className="text-sm font-medium truncate flex-1">{project.title}</span>
144
+ <Badge variant="outline" className="text-[10px]">{project.status}</Badge>
145
+ {project.due_date && <DDayBadge dueDate={project.due_date} />}
146
+ </div>
147
+ </CardContent>
148
+ </Card>
149
+ </Link>
150
+ ))}
151
+ </div>
152
+ )}
153
+ </section>
154
+
155
+ {/* Routines */}
156
+ <section>
157
+ <div className="flex items-center gap-2 mb-3">
158
+ <RotateCcw className="w-4 h-4 text-muted-foreground" />
159
+ <h2 className="text-sm font-medium">루틴</h2>
160
+ <Badge variant="outline" className="text-[10px]">{areaRoutines.length}</Badge>
161
+ </div>
162
+ {areaRoutines.length === 0 ? (
163
+ <p className="text-xs text-muted-foreground pl-6">연결된 루틴이 없습니다</p>
164
+ ) : (
165
+ <div className="space-y-2">
166
+ {areaRoutines.map(routine => (
167
+ <Card key={routine.id}>
168
+ <CardContent className="py-3 px-4">
169
+ <div className="flex items-center gap-3">
170
+ <RotateCcw className="w-4 h-4 text-muted-foreground flex-shrink-0" />
171
+ <span className="text-sm font-medium truncate flex-1">{routine.title}</span>
172
+ {routine.streak > 0 && (
173
+ <Badge variant="outline" className="text-[10px]">🔥 {routine.streak}일</Badge>
174
+ )}
175
+ <Badge variant={routine.is_active ? 'default' : 'secondary'} className="text-[10px]">
176
+ {routine.is_active ? '활성' : '중단'}
177
+ </Badge>
178
+ </div>
179
+ </CardContent>
180
+ </Card>
181
+ ))}
182
+ </div>
183
+ )}
184
+ </section>
185
+
186
+ {/* Notes */}
187
+ <section>
188
+ <div className="flex items-center gap-2 mb-3">
189
+ <StickyNote className="w-4 h-4 text-muted-foreground" />
190
+ <h2 className="text-sm font-medium">노트</h2>
191
+ <Badge variant="outline" className="text-[10px]">{notes.length}</Badge>
192
+ </div>
193
+ {notes.length === 0 ? (
194
+ <p className="text-xs text-muted-foreground pl-6">연결된 노트가 없습니다</p>
195
+ ) : (
196
+ <div className="space-y-2">
197
+ {notes.map((note: { id: string; title: string; type: string; priority: number }) => (
198
+ <Card key={note.id}>
199
+ <CardContent className="py-3 px-4">
200
+ <div className="flex items-center gap-3">
201
+ <StickyNote className="w-4 h-4 text-muted-foreground flex-shrink-0" />
202
+ <span className="text-sm font-medium truncate flex-1">{note.title}</span>
203
+ <Badge variant="outline" className="text-[10px]">P{note.priority}</Badge>
204
+ <Badge variant="secondary" className="text-[10px]">{note.type}</Badge>
205
+ </div>
206
+ </CardContent>
207
+ </Card>
208
+ ))}
209
+ </div>
210
+ )}
211
+ </section>
212
+ </div>
213
+ </div>
214
+ )
215
+ }
@@ -0,0 +1,161 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAreas, useCreateArea, useDeleteArea } from '@/hooks/useAreas'
5
+ import { useGoals } from '@/hooks/useGoals'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Badge } from '@/components/ui/badge'
9
+ import { Input } from '@/components/ui/input'
10
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
11
+ import { Plus, Target, Layers, Trash2 } from 'lucide-react'
12
+ import Link from 'next/link'
13
+ import type { Area } from '@/types'
14
+
15
+ const AREA_ICONS = ['💪', '💼', '📈', '🧠', '🎨', '❤️', '🌱', '📚']
16
+
17
+ export default function AreasPage() {
18
+ const { data: areas = [], isLoading } = useAreas()
19
+ const { data: goals = [] } = useGoals()
20
+ const createArea = useCreateArea()
21
+ const deleteArea = useDeleteArea()
22
+
23
+ const [open, setOpen] = useState(false)
24
+ const [title, setTitle] = useState('')
25
+ const [icon, setIcon] = useState('💪')
26
+
27
+ const handleCreate = () => {
28
+ if (!title.trim() || createArea.isPending) return
29
+ createArea.mutate(
30
+ { title: title.trim(), icon, status: 'active', linked_goals: [] },
31
+ { onSuccess: () => { setOpen(false); setTitle(''); setIcon('💪') } }
32
+ )
33
+ }
34
+
35
+ const activeGoals = (areaId: string) =>
36
+ goals.filter(g => g.area_id === areaId && g.status === 'active')
37
+
38
+ return (
39
+ <div className="max-w-4xl mx-auto">
40
+ <div className="flex items-center justify-between mb-6">
41
+ <div>
42
+ <h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
43
+ <Layers className="w-6 h-6" />
44
+ 영역 (Areas)
45
+ </h1>
46
+ <p className="text-sm text-muted-foreground mt-1">
47
+ 인생의 핵심 영역을 관리합니다
48
+ </p>
49
+ </div>
50
+ <Dialog open={open} onOpenChange={setOpen}>
51
+ <DialogTrigger asChild>
52
+ <Button size="sm" className="gap-1">
53
+ <Plus className="w-4 h-4" /> 영역 추가
54
+ </Button>
55
+ </DialogTrigger>
56
+ <DialogContent>
57
+ <DialogHeader>
58
+ <DialogTitle>새 영역 추가</DialogTitle>
59
+ </DialogHeader>
60
+ <div className="space-y-4 mt-2">
61
+ <div>
62
+ <label className="text-sm text-muted-foreground">아이콘</label>
63
+ <div className="flex gap-2 mt-1">
64
+ {AREA_ICONS.map(i => (
65
+ <button
66
+ key={i}
67
+ onClick={() => setIcon(i)}
68
+ className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition-colors ${
69
+ icon === i ? 'bg-primary/20 ring-2 ring-primary' : 'bg-card hover:bg-muted'
70
+ }`}
71
+ >
72
+ {i}
73
+ </button>
74
+ ))}
75
+ </div>
76
+ </div>
77
+ <div>
78
+ <label className="text-sm text-muted-foreground">영역 이름</label>
79
+ <Input
80
+ value={title}
81
+ onChange={e => setTitle(e.target.value)}
82
+ placeholder="예: 건강/커리어/마케팅"
83
+ className="mt-1"
84
+ onKeyDown={e => e.key === 'Enter' && handleCreate()}
85
+ />
86
+ </div>
87
+ <Button onClick={handleCreate} disabled={!title.trim() || createArea.isPending} className="w-full">
88
+ 추가
89
+ </Button>
90
+ </div>
91
+ </DialogContent>
92
+ </Dialog>
93
+ </div>
94
+
95
+ {isLoading ? (
96
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
97
+ {[1, 2, 3].map(i => <div key={i} className="h-32 bg-card rounded-lg animate-pulse" />)}
98
+ </div>
99
+ ) : areas.length === 0 ? (
100
+ <Card>
101
+ <CardContent className="py-12 text-center text-muted-foreground">
102
+ 아직 영역이 없습니다. 인생의 핵심 영역을 추가해보세요.
103
+ </CardContent>
104
+ </Card>
105
+ ) : (
106
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
107
+ {areas.map(area => {
108
+ const linkedGoals = activeGoals(area.id)
109
+ return (
110
+ <Link key={area.id} href={`/areas/${area.id}`}>
111
+ <Card className="group hover:border-primary/30 transition-colors cursor-pointer">
112
+ <CardHeader className="pb-2">
113
+ <CardTitle className="flex items-center justify-between">
114
+ <div className="flex items-center gap-2">
115
+ <span className="text-xl">{area.icon}</span>
116
+ <span className="text-lg">{area.title}</span>
117
+ </div>
118
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
119
+ <Button
120
+ variant="ghost"
121
+ size="icon"
122
+ className="h-7 w-7 text-muted-foreground hover:text-gatsaeng-red"
123
+ onClick={(e) => { e.preventDefault(); deleteArea.mutate(area.id) }}
124
+ >
125
+ <Trash2 className="w-3.5 h-3.5" />
126
+ </Button>
127
+ </div>
128
+ </CardTitle>
129
+ </CardHeader>
130
+ <CardContent>
131
+ <div className="flex items-center gap-1 text-xs text-muted-foreground mb-3">
132
+ <Target className="w-3 h-3" />
133
+ 연결된 목표 {linkedGoals.length}개
134
+ </div>
135
+ {linkedGoals.length > 0 ? (
136
+ <div className="space-y-2">
137
+ {linkedGoals.map(g => (
138
+ <Link key={g.id} href={`/goals/${g.id}`} className="flex items-center gap-2 text-sm hover:text-primary transition-colors">
139
+ <div className="w-2 h-2 rounded-full" style={{ backgroundColor: g.color }} />
140
+ <span className="truncate">{g.title}</span>
141
+ {g.target_value && g.current_value !== undefined && (
142
+ <Badge variant="outline" className="ml-auto text-[10px]">
143
+ {Math.round(((g.current_value ?? 0) / g.target_value) * 100)}%
144
+ </Badge>
145
+ )}
146
+ </Link>
147
+ ))}
148
+ </div>
149
+ ) : (
150
+ <p className="text-xs text-muted-foreground">연결된 목표가 없습니다.</p>
151
+ )}
152
+ </CardContent>
153
+ </Card>
154
+ </Link>
155
+ )
156
+ })}
157
+ </div>
158
+ )}
159
+ </div>
160
+ )
161
+ }
@@ -0,0 +1,215 @@
1
+ 'use client'
2
+
3
+ import { use, useState, useCallback } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { Card, CardContent } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { Input } from '@/components/ui/input'
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
10
+ import { ArrowLeft, Trash2, Star } from 'lucide-react'
11
+ import { useBook, useUpdateBook, useDeleteBook } from '@/hooks/useBooks'
12
+ import { TiptapEditor } from '@/components/editor/TiptapEditor'
13
+ import { PinButton } from '@/components/shared/PinButton'
14
+ import { cn } from '@/lib/utils'
15
+ import type { BookStatus } from '@/types'
16
+
17
+ const STATUS_LABELS: Record<BookStatus, string> = {
18
+ reading: '읽는 중',
19
+ completed: '완독',
20
+ want_to_read: '읽고 싶은',
21
+ dropped: '중단',
22
+ }
23
+
24
+ const STATUS_COLORS: Record<BookStatus, string> = {
25
+ reading: 'border-gatsaeng-teal/30 text-gatsaeng-teal',
26
+ completed: 'border-primary/30 text-primary',
27
+ want_to_read: 'border-gatsaeng-amber/30 text-gatsaeng-amber',
28
+ dropped: 'border-muted-foreground/30 text-muted-foreground',
29
+ }
30
+
31
+ export default function BookDetailPage({ params }: { params: Promise<{ id: string }> }) {
32
+ const { id } = use(params)
33
+ const router = useRouter()
34
+ const { data: book, isLoading } = useBook(id)
35
+ const updateBook = useUpdateBook()
36
+ const deleteBook = useDeleteBook()
37
+ const [editingTitle, setEditingTitle] = useState(false)
38
+ const [titleValue, setTitleValue] = useState('')
39
+
40
+ const handleTitleSave = useCallback(() => {
41
+ if (!book || !titleValue.trim()) return
42
+ if (titleValue !== book.title) {
43
+ updateBook.mutate({ id, title: titleValue })
44
+ }
45
+ setEditingTitle(false)
46
+ }, [id, book, titleValue, updateBook])
47
+
48
+ const handleContentSave = useCallback((markdown: string) => {
49
+ updateBook.mutate({ id, content: markdown })
50
+ }, [id, updateBook])
51
+
52
+ const handleDelete = () => {
53
+ if (!confirm('이 책을 삭제하시겠습니까?')) return
54
+ deleteBook.mutate(id, {
55
+ onSuccess: () => router.push('/books'),
56
+ })
57
+ }
58
+
59
+ if (isLoading) {
60
+ return (
61
+ <div className="flex items-center justify-center h-64">
62
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
63
+ </div>
64
+ )
65
+ }
66
+
67
+ if (!book) {
68
+ return (
69
+ <div className="text-center py-20">
70
+ <p className="text-muted-foreground">책을 찾을 수 없습니다</p>
71
+ <Button variant="outline" className="mt-4" onClick={() => router.push('/books')}>
72
+ <ArrowLeft className="w-4 h-4 mr-2" /> 돌아가기
73
+ </Button>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ const progress = book.total_pages && book.current_page
79
+ ? Math.round((book.current_page / book.total_pages) * 100)
80
+ : null
81
+
82
+ return (
83
+ <div className="max-w-3xl mx-auto">
84
+ {/* Header */}
85
+ <div className="flex items-center gap-3 mb-6">
86
+ <Button variant="ghost" size="icon" onClick={() => router.push('/books')}>
87
+ <ArrowLeft className="w-5 h-5" />
88
+ </Button>
89
+ <div className="flex-1 min-w-0">
90
+ {editingTitle ? (
91
+ <Input
92
+ value={titleValue}
93
+ onChange={(e) => setTitleValue(e.target.value)}
94
+ onBlur={handleTitleSave}
95
+ onKeyDown={(e) => {
96
+ if (e.key === 'Enter') handleTitleSave()
97
+ if (e.key === 'Escape') setEditingTitle(false)
98
+ }}
99
+ autoFocus
100
+ className="text-2xl font-bold h-auto py-1"
101
+ />
102
+ ) : (
103
+ <h1
104
+ className="text-2xl font-bold text-foreground cursor-pointer hover:text-primary/80 transition-colors truncate"
105
+ onDoubleClick={() => {
106
+ setTitleValue(book.title)
107
+ setEditingTitle(true)
108
+ }}
109
+ title="더블클릭하여 편집"
110
+ >
111
+ {book.title}
112
+ </h1>
113
+ )}
114
+ <p className="text-sm text-muted-foreground">{book.author}</p>
115
+ </div>
116
+ <PinButton type="book" id={id} title={book.title} size={20} />
117
+ <Button variant="ghost" size="icon" onClick={handleDelete} className="text-muted-foreground hover:text-gatsaeng-red">
118
+ <Trash2 className="w-4 h-4" />
119
+ </Button>
120
+ </div>
121
+
122
+ {/* Properties */}
123
+ <Card className="mb-6">
124
+ <CardContent className="py-4">
125
+ <div className="grid grid-cols-2 gap-4">
126
+ <div>
127
+ <label className="text-xs text-muted-foreground mb-1 block">상태</label>
128
+ <Select
129
+ value={book.status}
130
+ onValueChange={(v) => updateBook.mutate({ id, status: v as BookStatus })}
131
+ >
132
+ <SelectTrigger className="h-8 text-sm">
133
+ <SelectValue />
134
+ </SelectTrigger>
135
+ <SelectContent>
136
+ {Object.entries(STATUS_LABELS).map(([k, v]) => (
137
+ <SelectItem key={k} value={k}>{v}</SelectItem>
138
+ ))}
139
+ </SelectContent>
140
+ </Select>
141
+ </div>
142
+ <div>
143
+ <label className="text-xs text-muted-foreground mb-1 block">평점</label>
144
+ <div className="flex items-center gap-1">
145
+ {[1, 2, 3, 4, 5].map(n => (
146
+ <button
147
+ key={n}
148
+ onClick={() => updateBook.mutate({ id, rating: n })}
149
+ className="transition-colors"
150
+ >
151
+ <Star
152
+ className={cn(
153
+ 'w-5 h-5',
154
+ n <= (book.rating ?? 0)
155
+ ? 'text-gatsaeng-amber fill-gatsaeng-amber'
156
+ : 'text-muted-foreground/30'
157
+ )}
158
+ />
159
+ </button>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ <div>
164
+ <label className="text-xs text-muted-foreground mb-1 block">현재 페이지</label>
165
+ <Input
166
+ type="number"
167
+ value={book.current_page ?? ''}
168
+ onChange={(e) => updateBook.mutate({ id, current_page: Number(e.target.value) || 0 })}
169
+ className="h-8 text-sm"
170
+ placeholder="0"
171
+ />
172
+ </div>
173
+ <div>
174
+ <label className="text-xs text-muted-foreground mb-1 block">총 페이지</label>
175
+ <Input
176
+ type="number"
177
+ value={book.total_pages ?? ''}
178
+ onChange={(e) => updateBook.mutate({ id, total_pages: Number(e.target.value) || 0 })}
179
+ className="h-8 text-sm"
180
+ placeholder="0"
181
+ />
182
+ </div>
183
+ </div>
184
+
185
+ {/* Progress bar */}
186
+ {progress !== null && (
187
+ <div className="mt-4">
188
+ <div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
189
+ <span>진행률</span>
190
+ <span>{progress}%</span>
191
+ </div>
192
+ <div className="h-2 bg-muted rounded-full overflow-hidden">
193
+ <div
194
+ className="h-full bg-gatsaeng-teal rounded-full transition-all"
195
+ style={{ width: `${Math.min(progress, 100)}%` }}
196
+ />
197
+ </div>
198
+ </div>
199
+ )}
200
+ </CardContent>
201
+ </Card>
202
+
203
+ {/* Editor */}
204
+ <Card>
205
+ <CardContent className="p-0">
206
+ <TiptapEditor
207
+ content={book._content ?? ''}
208
+ onSave={handleContentSave}
209
+ placeholder="독서 노트를 작성하세요..."
210
+ />
211
+ </CardContent>
212
+ </Card>
213
+ </div>
214
+ )
215
+ }