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,426 @@
1
+ 'use client'
2
+
3
+ import { use, useState } from 'react'
4
+ import { useQuery } from '@tanstack/react-query'
5
+ import { useUpdateGoal, useDeleteGoal } from '@/hooks/useGoals'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Input } from '@/components/ui/input'
10
+ import { Label } from '@/components/ui/label'
11
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
12
+ import { ArrowLeft, Target, Plus, Minus, Brain, UserCircle, MapPin, Calendar, Flag, Trash2, Pencil, Check, X } from 'lucide-react'
13
+ import Link from 'next/link'
14
+ import { useRouter } from 'next/navigation'
15
+ import { useMilestonesWithDDay, useCreateMilestone, useUpdateMilestone, useDeleteMilestone } from '@/hooks/useMilestones'
16
+ import type { Goal, MilestoneWithDDay } from '@/types'
17
+ import { LIMITS } from '@/types'
18
+
19
+ const TYPE_LABELS: Record<string, string> = {
20
+ annual: '연간',
21
+ quarterly: '분기',
22
+ monthly: '월간',
23
+ }
24
+
25
+ export default function GoalDetailPage({ params }: { params: Promise<{ id: string }> }) {
26
+ const { id } = use(params)
27
+ const router = useRouter()
28
+ const [addAmount, setAddAmount] = useState(1)
29
+ const { data: milestones = [] } = useMilestonesWithDDay(id)
30
+ const createMilestone = useCreateMilestone()
31
+ const updateMilestone = useUpdateMilestone()
32
+ const deleteMilestone = useDeleteMilestone()
33
+ const [msDialogOpen, setMsDialogOpen] = useState(false)
34
+ const [editingMs, setEditingMs] = useState<MilestoneWithDDay | null>(null)
35
+ const [msProgressEdit, setMsProgressEdit] = useState<{ id: string; value: number } | null>(null)
36
+
37
+ const { data: goal, refetch } = useQuery({
38
+ queryKey: ['goal', id],
39
+ queryFn: async (): Promise<Goal> => {
40
+ const res = await fetch(`/api/goals/${id}`)
41
+ return res.json()
42
+ },
43
+ })
44
+
45
+ const updateGoal = useUpdateGoal()
46
+ const deleteGoal = useDeleteGoal()
47
+
48
+ const handleDelete = () => {
49
+ if (!confirm('이 목표를 삭제하시겠습니까?')) return
50
+ deleteGoal.mutate(id, {
51
+ onSuccess: () => router.push('/goals'),
52
+ })
53
+ }
54
+
55
+ const handleUpdateProgress = (delta: number) => {
56
+ if (!goal) return
57
+ const newValue = Math.max(0, (goal.current_value ?? 0) + delta)
58
+ updateGoal.mutate(
59
+ { id, current_value: newValue },
60
+ { onSuccess: () => refetch() }
61
+ )
62
+ }
63
+
64
+ const handleMilestoneSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
65
+ e.preventDefault()
66
+ const form = new FormData(e.currentTarget)
67
+ const payload = {
68
+ title: form.get('ms_title') as string,
69
+ target_value: Number(form.get('ms_target')) || 1,
70
+ unit: (form.get('ms_unit') as string) || '',
71
+ due_date: form.get('ms_due_date') as string,
72
+ }
73
+
74
+ if (editingMs) {
75
+ await updateMilestone.mutateAsync({ id: editingMs.id, ...payload })
76
+ } else {
77
+ await createMilestone.mutateAsync({ goal_id: id, ...payload })
78
+ }
79
+ setMsDialogOpen(false)
80
+ setEditingMs(null)
81
+ }
82
+
83
+ const handleMsProgressSave = (msId: string, value: number) => {
84
+ updateMilestone.mutate(
85
+ { id: msId, current_value: Math.max(0, value) },
86
+ { onSuccess: () => setMsProgressEdit(null) }
87
+ )
88
+ }
89
+
90
+ const handleMsDelete = (msId: string) => {
91
+ if (!confirm('이 마일스톤을 삭제하시겠습니까?')) return
92
+ deleteMilestone.mutate(msId)
93
+ }
94
+
95
+ if (!goal) {
96
+ return <div className="max-w-3xl mx-auto animate-pulse"><div className="h-48 bg-card rounded-lg" /></div>
97
+ }
98
+
99
+ const progress = goal.target_value
100
+ ? Math.min(100, Math.round(((goal.current_value ?? 0) / goal.target_value) * 100))
101
+ : 0
102
+
103
+ const daysLeft = goal.due_date
104
+ ? Math.ceil((new Date(goal.due_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
105
+ : null
106
+
107
+ return (
108
+ <div className="max-w-3xl mx-auto">
109
+ <Link href="/goals" className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors mb-4">
110
+ <ArrowLeft className="w-3.5 h-3.5" />
111
+ 목표 목록
112
+ </Link>
113
+
114
+ {/* Header */}
115
+ <div className="flex items-start justify-between mb-6">
116
+ <div className="flex items-center gap-3">
117
+ <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ backgroundColor: goal.color + '20' }}>
118
+ <Target className="w-5 h-5" style={{ color: goal.color }} />
119
+ </div>
120
+ <div>
121
+ <h1 className="text-2xl font-bold text-foreground">{goal.title}</h1>
122
+ <div className="flex items-center gap-2 mt-1">
123
+ <Badge variant="outline">{TYPE_LABELS[goal.type]}</Badge>
124
+ <Badge variant="outline" className={goal.status === 'active' ? 'border-gatsaeng-teal text-gatsaeng-teal' : ''}>{goal.status}</Badge>
125
+ {goal.core_value && <Badge variant="outline">{goal.core_value}</Badge>}
126
+ </div>
127
+ </div>
128
+ </div>
129
+ <div className="flex items-center gap-3">
130
+ <Button
131
+ variant="ghost"
132
+ size="icon"
133
+ className="text-muted-foreground hover:text-gatsaeng-red"
134
+ onClick={handleDelete}
135
+ >
136
+ <Trash2 className="w-4 h-4" />
137
+ </Button>
138
+ {daysLeft !== null && (
139
+ <div className="text-right">
140
+ <div className={`text-2xl font-bold ${daysLeft <= 7 ? 'text-gatsaeng-red' : daysLeft <= 30 ? 'text-gatsaeng-amber' : 'text-foreground'}`}>
141
+ D{daysLeft > 0 ? `-${daysLeft}` : daysLeft === 0 ? '-Day' : `+${Math.abs(daysLeft)}`}
142
+ </div>
143
+ <div className="text-xs text-muted-foreground">{goal.due_date?.slice(0, 10)}</div>
144
+ </div>
145
+ )}
146
+ </div>
147
+ </div>
148
+
149
+ {/* Progress Ring + Controls */}
150
+ {goal.target_value && (
151
+ <Card className="mb-6">
152
+ <CardContent className="py-6">
153
+ <div className="flex items-center gap-8">
154
+ {/* SVG Ring */}
155
+ <div className="relative w-32 h-32 flex-shrink-0">
156
+ <svg className="w-32 h-32 -rotate-90" viewBox="0 0 128 128">
157
+ <circle cx="64" cy="64" r="56" fill="none" stroke="currentColor" className="text-muted" strokeWidth="8" />
158
+ <circle
159
+ cx="64" cy="64" r="56" fill="none"
160
+ stroke={goal.color}
161
+ strokeWidth="8"
162
+ strokeLinecap="round"
163
+ strokeDasharray={`${2 * Math.PI * 56}`}
164
+ strokeDashoffset={`${2 * Math.PI * 56 * (1 - progress / 100)}`}
165
+ className="transition-all duration-500"
166
+ />
167
+ </svg>
168
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
169
+ <span className="text-2xl font-bold" style={{ color: goal.color }}>{progress}%</span>
170
+ <span className="text-[10px] text-muted-foreground">{goal.current_value ?? 0}/{goal.target_value}</span>
171
+ </div>
172
+ </div>
173
+
174
+ {/* Controls */}
175
+ <div className="flex-1">
176
+ <div className="text-sm text-muted-foreground mb-3">
177
+ 진행 기록 ({goal.unit ?? '단위'})
178
+ </div>
179
+ <div className="flex items-center gap-2">
180
+ <Button
181
+ variant="outline"
182
+ size="icon"
183
+ className="h-10 w-10"
184
+ onClick={() => handleUpdateProgress(-addAmount)}
185
+ disabled={(goal.current_value ?? 0) === 0}
186
+ >
187
+ <Minus className="w-4 h-4" />
188
+ </Button>
189
+ <Input
190
+ type="number"
191
+ value={addAmount}
192
+ onChange={e => setAddAmount(Math.max(1, Number(e.target.value)))}
193
+ className="w-20 h-10 text-center"
194
+ min={1}
195
+ />
196
+ <Button
197
+ size="icon"
198
+ className="h-10 w-10 bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black"
199
+ onClick={() => handleUpdateProgress(addAmount)}
200
+ >
201
+ <Plus className="w-4 h-4" />
202
+ </Button>
203
+ </div>
204
+ <div className="text-xs text-muted-foreground mt-2">
205
+ {goal.target_value - (goal.current_value ?? 0) > 0
206
+ ? `목표까지 ${goal.target_value - (goal.current_value ?? 0)} ${goal.unit ?? ''} 남음`
207
+ : '목표 달성!'}
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </CardContent>
212
+ </Card>
213
+ )}
214
+
215
+ {/* Milestones */}
216
+ <Card className="mb-6">
217
+ <CardHeader className="pb-2">
218
+ <div className="flex items-center justify-between">
219
+ <CardTitle className="text-sm flex items-center gap-2">
220
+ <Flag className="w-4 h-4 text-gatsaeng-amber" />
221
+ 마일스톤 ({milestones.length}/{LIMITS.MAX_MILESTONES_PER_GOAL})
222
+ </CardTitle>
223
+ <Dialog open={msDialogOpen} onOpenChange={(v) => { setMsDialogOpen(v); if (!v) setEditingMs(null) }}>
224
+ <DialogTrigger asChild>
225
+ <Button
226
+ variant="ghost"
227
+ size="sm"
228
+ className="h-7 text-xs"
229
+ disabled={milestones.length >= LIMITS.MAX_MILESTONES_PER_GOAL}
230
+ onClick={() => setEditingMs(null)}
231
+ >
232
+ <Plus className="w-3.5 h-3.5 mr-1" />
233
+ 추가
234
+ </Button>
235
+ </DialogTrigger>
236
+ <DialogContent>
237
+ <DialogHeader>
238
+ <DialogTitle>{editingMs ? '마일스톤 수정' : '마일스톤 추가'}</DialogTitle>
239
+ </DialogHeader>
240
+ <form onSubmit={handleMilestoneSubmit} className="space-y-4">
241
+ <div>
242
+ <Label>제목</Label>
243
+ <Input name="ms_title" required placeholder="1차 중간점검" defaultValue={editingMs?.title ?? ''} />
244
+ </div>
245
+ <div className="grid grid-cols-2 gap-4">
246
+ <div>
247
+ <Label>목표치</Label>
248
+ <Input name="ms_target" type="number" required min={1} placeholder="50" defaultValue={editingMs?.target_value ?? ''} />
249
+ </div>
250
+ <div>
251
+ <Label>단위</Label>
252
+ <Input name="ms_unit" placeholder={goal.unit || '회, 권, 개'} defaultValue={editingMs?.unit ?? goal.unit ?? ''} />
253
+ </div>
254
+ </div>
255
+ <div>
256
+ <Label>마감일</Label>
257
+ <Input name="ms_due_date" type="date" required defaultValue={editingMs?.due_date?.slice(0, 10) ?? ''} />
258
+ </div>
259
+ <Button
260
+ type="submit"
261
+ className="w-full bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black"
262
+ disabled={createMilestone.isPending || updateMilestone.isPending}
263
+ >
264
+ {editingMs ? '수정' : '추가'}
265
+ </Button>
266
+ </form>
267
+ </DialogContent>
268
+ </Dialog>
269
+ </div>
270
+ </CardHeader>
271
+ <CardContent>
272
+ {milestones.length === 0 ? (
273
+ <p className="text-sm text-muted-foreground text-center py-3">
274
+ 아직 마일스톤이 없습니다. 목표를 단계별로 나눠보세요.
275
+ </p>
276
+ ) : (
277
+ <div className="space-y-3">
278
+ {milestones.map(m => {
279
+ const msProgress = m.target_value > 0
280
+ ? Math.min(100, Math.round((m.current_value / m.target_value) * 100))
281
+ : 0
282
+ const isEditing = msProgressEdit?.id === m.id
283
+ return (
284
+ <div key={m.id} className="group flex items-center gap-3">
285
+ <div className={`text-sm font-bold font-mono min-w-[50px] text-right ${
286
+ m.d_day <= 0 ? 'text-gatsaeng-red' : m.d_day <= 7 ? 'text-gatsaeng-red' : m.d_day <= 30 ? 'text-gatsaeng-amber' : 'text-foreground'
287
+ }`}>
288
+ {m.d_day === 0 ? 'D-Day' : m.d_day > 0 ? `D-${m.d_day}` : `D+${Math.abs(m.d_day)}`}
289
+ </div>
290
+ <div className="flex-1 min-w-0">
291
+ <div className="text-sm text-foreground truncate">{m.title}</div>
292
+ {isEditing ? (
293
+ <div className="flex items-center gap-1 mt-1">
294
+ <Input
295
+ type="number"
296
+ className="h-6 w-16 text-xs"
297
+ value={msProgressEdit.value}
298
+ onChange={e => setMsProgressEdit({ id: m.id, value: Number(e.target.value) })}
299
+ min={0}
300
+ max={m.target_value}
301
+ autoFocus
302
+ onKeyDown={e => {
303
+ if (e.key === 'Enter') handleMsProgressSave(m.id, msProgressEdit.value)
304
+ if (e.key === 'Escape') setMsProgressEdit(null)
305
+ }}
306
+ />
307
+ <span className="text-[10px] text-muted-foreground">/ {m.target_value} {m.unit}</span>
308
+ <Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => handleMsProgressSave(m.id, msProgressEdit.value)}>
309
+ <Check className="w-3 h-3 text-gatsaeng-teal" />
310
+ </Button>
311
+ <Button variant="ghost" size="icon" className="h-5 w-5" onClick={() => setMsProgressEdit(null)}>
312
+ <X className="w-3 h-3" />
313
+ </Button>
314
+ </div>
315
+ ) : (
316
+ <div
317
+ className="text-[10px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
318
+ onClick={() => setMsProgressEdit({ id: m.id, value: m.current_value })}
319
+ >
320
+ {m.current_value}/{m.target_value} {m.unit} · {m.due_date.slice(0, 10)}
321
+ </div>
322
+ )}
323
+ </div>
324
+ <div className="w-16 h-1.5 bg-muted rounded-full overflow-hidden">
325
+ <div
326
+ className="h-full rounded-full transition-all"
327
+ style={{ width: `${msProgress}%`, backgroundColor: goal.color }}
328
+ />
329
+ </div>
330
+ <span className="text-xs text-muted-foreground min-w-[30px] text-right">{msProgress}%</span>
331
+ {/* Edit/Delete buttons - visible on hover */}
332
+ <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
333
+ <Button
334
+ variant="ghost"
335
+ size="icon"
336
+ className="h-6 w-6"
337
+ onClick={() => { setEditingMs(m); setMsDialogOpen(true) }}
338
+ >
339
+ <Pencil className="w-3 h-3" />
340
+ </Button>
341
+ <Button
342
+ variant="ghost"
343
+ size="icon"
344
+ className="h-6 w-6 hover:text-gatsaeng-red"
345
+ onClick={() => handleMsDelete(m.id)}
346
+ >
347
+ <Trash2 className="w-3 h-3" />
348
+ </Button>
349
+ </div>
350
+ </div>
351
+ )
352
+ })}
353
+ </div>
354
+ )}
355
+ </CardContent>
356
+ </Card>
357
+
358
+ {/* AI Diagnosis */}
359
+ {(goal.ai_diagnosis || goal.ai_direction) && (
360
+ <Card className="mb-6 border-l-4 border-l-primary">
361
+ <CardHeader className="pb-1">
362
+ <CardTitle className="text-sm flex items-center gap-2 text-primary">
363
+ <Brain className="w-4 h-4" />
364
+ AI 진단
365
+ </CardTitle>
366
+ </CardHeader>
367
+ <CardContent className="space-y-2">
368
+ {goal.ai_diagnosis && <p className="text-sm text-foreground">{goal.ai_diagnosis}</p>}
369
+ {goal.ai_direction && <p className="text-sm text-muted-foreground">{goal.ai_direction}</p>}
370
+ {goal.ai_next_review && (
371
+ <div className="flex items-center gap-1 text-[10px] text-muted-foreground">
372
+ <Calendar className="w-3 h-3" />
373
+ 다음 리뷰: {goal.ai_next_review}
374
+ </div>
375
+ )}
376
+ </CardContent>
377
+ </Card>
378
+ )}
379
+
380
+ {/* Neuroscience Cards */}
381
+ <div className="grid grid-cols-1 gap-4">
382
+ {goal.why_statement && (
383
+ <Card className="border-l-4" style={{ borderLeftColor: '#7c5cbf' }}>
384
+ <CardHeader className="pb-1">
385
+ <CardTitle className="text-sm flex items-center gap-2 text-gatsaeng-purple">
386
+ <Brain className="w-4 h-4" />
387
+ Why Statement
388
+ </CardTitle>
389
+ </CardHeader>
390
+ <CardContent>
391
+ <p className="text-sm text-foreground">{goal.why_statement}</p>
392
+ </CardContent>
393
+ </Card>
394
+ )}
395
+
396
+ {goal.identity_statement && (
397
+ <Card className="border-l-4" style={{ borderLeftColor: '#58a6ff' }}>
398
+ <CardHeader className="pb-1">
399
+ <CardTitle className="text-sm flex items-center gap-2 text-primary">
400
+ <UserCircle className="w-4 h-4" />
401
+ Identity Statement
402
+ </CardTitle>
403
+ </CardHeader>
404
+ <CardContent>
405
+ <p className="text-sm text-foreground italic">&ldquo;{goal.identity_statement}&rdquo;</p>
406
+ </CardContent>
407
+ </Card>
408
+ )}
409
+
410
+ {goal.when_where_how && (
411
+ <Card className="border-l-4" style={{ borderLeftColor: '#00d4aa' }}>
412
+ <CardHeader className="pb-1">
413
+ <CardTitle className="text-sm flex items-center gap-2 text-gatsaeng-teal">
414
+ <MapPin className="w-4 h-4" />
415
+ Implementation Intention
416
+ </CardTitle>
417
+ </CardHeader>
418
+ <CardContent>
419
+ <p className="text-sm text-foreground">{goal.when_where_how}</p>
420
+ </CardContent>
421
+ </Card>
422
+ )}
423
+ </div>
424
+ </div>
425
+ )
426
+ }
@@ -0,0 +1,178 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useGoals, useCreateGoal } from '@/hooks/useGoals'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { Input } from '@/components/ui/input'
8
+ import { Label } from '@/components/ui/label'
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
10
+ import { Badge } from '@/components/ui/badge'
11
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
12
+ import { Progress } from '@/components/ui/progress'
13
+ import { DDayBadge } from '@/components/tasks/DDayBadge'
14
+ import { Plus, Target } from 'lucide-react'
15
+ import Link from 'next/link'
16
+
17
+ const CORE_VALUES = ['성장', '자유', '관계', '건강', '재미', '안정', '창의']
18
+
19
+ export default function GoalsPage() {
20
+ const { data: goals, isLoading } = useGoals()
21
+ const createGoal = useCreateGoal()
22
+ const [open, setOpen] = useState(false)
23
+
24
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
25
+ e.preventDefault()
26
+ const form = new FormData(e.currentTarget)
27
+ await createGoal.mutateAsync({
28
+ title: form.get('title') as string,
29
+ type: form.get('type') as string,
30
+ target_value: Number(form.get('target_value')) || undefined,
31
+ unit: (form.get('unit') as string) || undefined,
32
+ why_statement: (form.get('why_statement') as string) || undefined,
33
+ identity_statement: (form.get('identity_statement') as string) || undefined,
34
+ when_where_how: (form.get('when_where_how') as string) || undefined,
35
+ core_value: (form.get('core_value') as string) || undefined,
36
+ due_date: (form.get('due_date') as string) || undefined,
37
+ })
38
+ setOpen(false)
39
+ }
40
+
41
+ return (
42
+ <div className="max-w-4xl mx-auto">
43
+ <div className="flex items-center justify-between mb-6">
44
+ <div>
45
+ <h1 className="text-2xl font-bold text-foreground">목표</h1>
46
+ <p className="text-sm text-muted-foreground mt-1">Why로 시작하는 목표 관리</p>
47
+ </div>
48
+ <Dialog open={open} onOpenChange={setOpen}>
49
+ <DialogTrigger asChild>
50
+ <Button className="bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black">
51
+ <Plus className="w-4 h-4 mr-2" /> 목표 추가
52
+ </Button>
53
+ </DialogTrigger>
54
+ <DialogContent className="max-w-lg">
55
+ <DialogHeader>
56
+ <DialogTitle>목표 만들기</DialogTitle>
57
+ </DialogHeader>
58
+ <form onSubmit={handleSubmit} className="space-y-4">
59
+ <div>
60
+ <Label>목표 제목</Label>
61
+ <Input name="title" required placeholder="영어 비즈니스 레벨 달성" />
62
+ </div>
63
+ <div className="grid grid-cols-2 gap-4">
64
+ <div>
65
+ <Label>타입</Label>
66
+ <Select name="type" defaultValue="quarterly">
67
+ <SelectTrigger><SelectValue /></SelectTrigger>
68
+ <SelectContent>
69
+ <SelectItem value="annual">연간</SelectItem>
70
+ <SelectItem value="quarterly">분기</SelectItem>
71
+ <SelectItem value="monthly">월간</SelectItem>
72
+ </SelectContent>
73
+ </Select>
74
+ </div>
75
+ <div>
76
+ <Label>마감일</Label>
77
+ <Input name="due_date" type="date" />
78
+ </div>
79
+ </div>
80
+ <div className="grid grid-cols-2 gap-4">
81
+ <div>
82
+ <Label>목표치</Label>
83
+ <Input name="target_value" type="number" placeholder="100" />
84
+ </div>
85
+ <div>
86
+ <Label>단위</Label>
87
+ <Input name="unit" placeholder="시간, 권, 개 등" />
88
+ </div>
89
+ </div>
90
+ <div>
91
+ <Label>왜 이 목표인가요? (Why Statement)</Label>
92
+ <Input name="why_statement" placeholder="자유롭게 협업하고 커리어 선택지를 넓히기 위해" />
93
+ </div>
94
+ <div>
95
+ <Label>어떤 사람이 되고 싶은가요? (Identity)</Label>
96
+ <Input name="identity_statement" placeholder="나는 영어로 생각하는 사람이다" />
97
+ </div>
98
+ <div>
99
+ <Label>언제, 어디서, 어떻게? (Implementation Intention)</Label>
100
+ <Input name="when_where_how" placeholder="매일 아침 9시, 카페에서, 단어 20개 + 섀도잉 15분" />
101
+ </div>
102
+ <div>
103
+ <Label>핵심 가치</Label>
104
+ <Select name="core_value">
105
+ <SelectTrigger><SelectValue placeholder="선택" /></SelectTrigger>
106
+ <SelectContent>
107
+ {CORE_VALUES.map(v => (
108
+ <SelectItem key={v} value={v}>{v}</SelectItem>
109
+ ))}
110
+ </SelectContent>
111
+ </Select>
112
+ </div>
113
+ <Button type="submit" className="w-full bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black">
114
+ 목표 생성
115
+ </Button>
116
+ </form>
117
+ </DialogContent>
118
+ </Dialog>
119
+ </div>
120
+
121
+ {isLoading ? (
122
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
123
+ {[1, 2, 3].map(i => <div key={i} className="h-40 bg-card rounded-lg animate-pulse" />)}
124
+ </div>
125
+ ) : (
126
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
127
+ {(goals ?? []).map(goal => {
128
+ const progress = goal.target_value
129
+ ? Math.round(((goal.current_value ?? 0) / goal.target_value) * 100)
130
+ : 0
131
+ return (
132
+ <Link key={goal.id} href={`/goals/${goal.id}`}>
133
+ <Card className="hover:border-primary/50 transition-colors cursor-pointer">
134
+ <CardHeader className="pb-2">
135
+ <div className="flex items-center justify-between">
136
+ <CardTitle className="text-base flex items-center gap-2">
137
+ <Target className="w-4 h-4" style={{ color: goal.color }} />
138
+ <span className="truncate">{goal.title}</span>
139
+ </CardTitle>
140
+ <div className="flex items-center gap-2 flex-shrink-0">
141
+ {goal.due_date && <DDayBadge dueDate={goal.due_date} />}
142
+ <Badge variant="outline">{goal.type}</Badge>
143
+ </div>
144
+ </div>
145
+ </CardHeader>
146
+ <CardContent>
147
+ {goal.target_value && (
148
+ <div className="mb-3">
149
+ <div className="flex justify-between text-xs text-muted-foreground mb-1">
150
+ <span>{goal.current_value ?? 0} / {goal.target_value} {goal.unit}</span>
151
+ <span>{progress}%</span>
152
+ </div>
153
+ <div className="h-2 bg-muted rounded-full overflow-hidden">
154
+ <div
155
+ className="h-full rounded-full transition-all"
156
+ style={{ width: `${progress}%`, backgroundColor: goal.color }}
157
+ />
158
+ </div>
159
+ </div>
160
+ )}
161
+ {goal.why_statement && (
162
+ <p className="text-xs text-muted-foreground">
163
+ Why: {goal.why_statement}
164
+ </p>
165
+ )}
166
+ {goal.core_value && (
167
+ <Badge className="mt-2 text-xs" variant="outline">{goal.core_value}</Badge>
168
+ )}
169
+ </CardContent>
170
+ </Card>
171
+ </Link>
172
+ )
173
+ })}
174
+ </div>
175
+ )}
176
+ </div>
177
+ )
178
+ }
@@ -0,0 +1,29 @@
1
+ import { Header } from '@/components/layout/Header'
2
+ import { Sidebar } from '@/components/layout/Sidebar'
3
+ import { MobileBottomNav } from '@/components/layout/MobileBottomNav'
4
+ import { OnboardingGate } from '@/components/onboarding/OnboardingGate'
5
+ import { GlobalSearch } from '@/components/search/GlobalSearch'
6
+
7
+ export default function DashboardLayout({
8
+ children,
9
+ }: {
10
+ children: React.ReactNode
11
+ }) {
12
+ return (
13
+ <OnboardingGate>
14
+ <div className="h-screen flex flex-col overflow-hidden">
15
+ <Header />
16
+ <div className="flex-1 flex overflow-hidden">
17
+ <div className="hidden md:block">
18
+ <Sidebar />
19
+ </div>
20
+ <main className="flex-1 overflow-y-auto p-4 md:p-6 pb-20 md:pb-6">
21
+ {children}
22
+ </main>
23
+ </div>
24
+ <MobileBottomNav />
25
+ <GlobalSearch />
26
+ </div>
27
+ </OnboardingGate>
28
+ )
29
+ }