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,436 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useRoutines, useToggleRoutine } from '@/hooks/useRoutines'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Card, CardContent } 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 { Checkbox } from '@/components/ui/checkbox'
11
+ import { Badge } from '@/components/ui/badge'
12
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
13
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
14
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
15
+ import { Plus, Zap, Flame, ArrowDown, Clock, MapPin, Bell, Trophy, Trash2, Pause, Play, Pencil } from 'lucide-react'
16
+ import { cn } from '@/lib/utils'
17
+ import type { RoutineWithStatus, TriggerType, EnergyLevel } from '@/types'
18
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
19
+
20
+ const TRIGGER_ICONS: Record<string, React.ReactNode> = {
21
+ time: <Clock className="w-3 h-3" />,
22
+ event: <Bell className="w-3 h-3" />,
23
+ location: <MapPin className="w-3 h-3" />,
24
+ }
25
+
26
+ export default function RoutinesPage() {
27
+ const { chains, routines, isLoading } = useRoutines()
28
+ const toggleMutation = useToggleRoutine()
29
+ const queryClient = useQueryClient()
30
+ const [open, setOpen] = useState(false)
31
+ const [editOpen, setEditOpen] = useState(false)
32
+ const [editingRoutine, setEditingRoutine] = useState<RoutineWithStatus | null>(null)
33
+ const [showReward, setShowReward] = useState<string | null>(null)
34
+ const [routineTab, setRoutineTab] = useState('active')
35
+
36
+ const createRoutine = useMutation({
37
+ mutationFn: async (data: Record<string, unknown>) => {
38
+ const res = await fetch('/api/routines', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify(data),
42
+ })
43
+ return res.json()
44
+ },
45
+ onSuccess: () => {
46
+ queryClient.invalidateQueries({ queryKey: ['routines'] })
47
+ setOpen(false)
48
+ },
49
+ })
50
+
51
+ const deleteRoutine = useMutation({
52
+ mutationFn: async (id: string) => {
53
+ const res = await fetch(`/api/routines/${id}`, { method: 'DELETE' })
54
+ return res.json()
55
+ },
56
+ onSuccess: () => {
57
+ queryClient.invalidateQueries({ queryKey: ['routines'] })
58
+ },
59
+ })
60
+
61
+ const updateRoutine = useMutation({
62
+ mutationFn: async ({ id, ...data }: Record<string, unknown> & { id: string }) => {
63
+ const res = await fetch(`/api/routines/${id}`, {
64
+ method: 'PUT',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(data),
67
+ })
68
+ return res.json()
69
+ },
70
+ onSuccess: () => {
71
+ queryClient.invalidateQueries({ queryKey: ['routines'] })
72
+ setEditOpen(false)
73
+ setEditingRoutine(null)
74
+ },
75
+ })
76
+
77
+ const handleEditSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
78
+ e.preventDefault()
79
+ if (!editingRoutine) return
80
+ const form = new FormData(e.currentTarget)
81
+ await updateRoutine.mutateAsync({
82
+ id: editingRoutine.id,
83
+ title: form.get('title') as string,
84
+ trigger_type: form.get('trigger_type') as TriggerType,
85
+ trigger_cue: (form.get('trigger_cue') as string) || undefined,
86
+ energy_required: form.get('energy_required') as EnergyLevel,
87
+ reward_note: (form.get('reward_note') as string) || undefined,
88
+ })
89
+ }
90
+
91
+ const handleToggleActive = (routine: RoutineWithStatus) => {
92
+ updateRoutine.mutate({ id: routine.id, is_active: !routine.is_active })
93
+ }
94
+
95
+ const handleDelete = (id: string, title: string) => {
96
+ if (!confirm(`"${title}" 루틴을 삭제하시겠습니까?`)) return
97
+ deleteRoutine.mutate(id)
98
+ }
99
+
100
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
101
+ e.preventDefault()
102
+ const form = new FormData(e.currentTarget)
103
+ await createRoutine.mutateAsync({
104
+ title: form.get('title') as string,
105
+ trigger_type: form.get('trigger_type') as TriggerType,
106
+ trigger_cue: (form.get('trigger_cue') as string) || undefined,
107
+ energy_required: form.get('energy_required') as EnergyLevel,
108
+ reward_note: (form.get('reward_note') as string) || undefined,
109
+ after_routine_id: (form.get('after_routine_id') as string) || undefined,
110
+ })
111
+ }
112
+
113
+ const handleToggle = (routine: RoutineWithStatus) => {
114
+ toggleMutation.mutate({ routineId: routine.id, completed: routine.completed_today })
115
+ // Show reward note briefly when completing
116
+ if (!routine.completed_today && routine.reward_note) {
117
+ setShowReward(routine.id)
118
+ setTimeout(() => setShowReward(null), 3000)
119
+ }
120
+ }
121
+
122
+ // Filter by active/stopped
123
+ const activeRoutines = routines.filter(r => r.is_active !== false)
124
+ const stoppedRoutines = routines.filter(r => r.is_active === false)
125
+ const filteredChains = routineTab === 'active'
126
+ ? chains.map(c => c.filter(r => r.is_active !== false)).filter(c => c.length > 0)
127
+ : chains.map(c => c.filter(r => r.is_active === false)).filter(c => c.length > 0)
128
+
129
+ // Stats
130
+ const totalRoutines = activeRoutines.length
131
+ const completedToday = activeRoutines.filter(r => r.completed_today).length
132
+ const totalStreak = activeRoutines.reduce((max, r) => Math.max(max, r.streak), 0)
133
+
134
+ return (
135
+ <div className="max-w-3xl mx-auto">
136
+ <div className="flex items-center justify-between mb-6">
137
+ <div>
138
+ <h1 className="text-2xl font-bold text-foreground">루틴</h1>
139
+ <p className="text-sm text-muted-foreground mt-1">습관 스택킹으로 루틴 체인 만들기</p>
140
+ </div>
141
+ <Dialog open={open} onOpenChange={setOpen}>
142
+ <DialogTrigger asChild>
143
+ <Button className="bg-gatsaeng-teal hover:bg-gatsaeng-teal/80 text-black">
144
+ <Plus className="w-4 h-4 mr-2" /> 루틴 추가
145
+ </Button>
146
+ </DialogTrigger>
147
+ <DialogContent className="max-w-lg">
148
+ <DialogHeader>
149
+ <DialogTitle>루틴 만들기</DialogTitle>
150
+ </DialogHeader>
151
+ <form onSubmit={handleSubmit} className="space-y-4">
152
+ <div>
153
+ <Label>루틴 이름</Label>
154
+ <Input name="title" required placeholder="명상 10분" />
155
+ </div>
156
+ <div>
157
+ <Label>트리거 타입</Label>
158
+ <Select name="trigger_type" defaultValue="event">
159
+ <SelectTrigger><SelectValue /></SelectTrigger>
160
+ <SelectContent>
161
+ <SelectItem value="time">시간 기준</SelectItem>
162
+ <SelectItem value="event">이벤트 기준</SelectItem>
163
+ <SelectItem value="location">장소 기준</SelectItem>
164
+ </SelectContent>
165
+ </Select>
166
+ </div>
167
+ <div>
168
+ <Label>트리거 큐</Label>
169
+ <Input name="trigger_cue" placeholder="기상 직후 / 점심 식사 후 / 퇴근 후 집 도착 시" />
170
+ </div>
171
+ <div>
172
+ <Label>앞 루틴 (습관 스택킹)</Label>
173
+ <Select name="after_routine_id">
174
+ <SelectTrigger><SelectValue placeholder="없음" /></SelectTrigger>
175
+ <SelectContent>
176
+ <SelectItem value="">없음</SelectItem>
177
+ {routines.map(r => (
178
+ <SelectItem key={r.id} value={r.id}>{r.title}</SelectItem>
179
+ ))}
180
+ </SelectContent>
181
+ </Select>
182
+ </div>
183
+ <div>
184
+ <Label>에너지 소모</Label>
185
+ <Select name="energy_required" defaultValue="medium">
186
+ <SelectTrigger><SelectValue /></SelectTrigger>
187
+ <SelectContent>
188
+ <SelectItem value="low">낮음</SelectItem>
189
+ <SelectItem value="medium">중간</SelectItem>
190
+ <SelectItem value="high">높음</SelectItem>
191
+ </SelectContent>
192
+ </Select>
193
+ </div>
194
+ <div>
195
+ <Label>완료 후 나에게 하는 말</Label>
196
+ <Input name="reward_note" placeholder="오늘도 내 뇌를 위한 투자를 했다" />
197
+ </div>
198
+ <Button type="submit" className="w-full bg-gatsaeng-teal hover:bg-gatsaeng-teal/80 text-black">
199
+ 루틴 생성
200
+ </Button>
201
+ </form>
202
+ </DialogContent>
203
+ </Dialog>
204
+ </div>
205
+
206
+ <Tabs value={routineTab} onValueChange={setRoutineTab} className="mb-4">
207
+ <TabsList>
208
+ <TabsTrigger value="active">활성 ({activeRoutines.length})</TabsTrigger>
209
+ <TabsTrigger value="stopped">중단 ({stoppedRoutines.length})</TabsTrigger>
210
+ </TabsList>
211
+ </Tabs>
212
+
213
+ {/* Daily Stats Bar */}
214
+ {totalRoutines > 0 && (
215
+ <div className="flex items-center gap-4 mb-6 p-3 bg-card rounded-lg border border-border">
216
+ <div className="flex-1">
217
+ <div className="flex justify-between text-xs text-muted-foreground mb-1.5">
218
+ <span>오늘 진행률</span>
219
+ <span>{completedToday}/{totalRoutines}</span>
220
+ </div>
221
+ <div className="h-2 bg-muted rounded-full overflow-hidden">
222
+ <div
223
+ className="h-full bg-gatsaeng-teal rounded-full transition-all duration-500"
224
+ style={{ width: `${totalRoutines > 0 ? (completedToday / totalRoutines) * 100 : 0}%` }}
225
+ />
226
+ </div>
227
+ </div>
228
+ {totalStreak > 0 && (
229
+ <div className="flex items-center gap-1.5 text-gatsaeng-amber">
230
+ <Flame className="w-4 h-4" />
231
+ <span className="text-sm font-bold">{totalStreak}</span>
232
+ <span className="text-xs text-muted-foreground">최고 연속</span>
233
+ </div>
234
+ )}
235
+ </div>
236
+ )}
237
+
238
+ {isLoading ? (
239
+ <div className="space-y-3">
240
+ {[1, 2, 3].map(i => <div key={i} className="h-16 bg-card rounded-lg animate-pulse" />)}
241
+ </div>
242
+ ) : filteredChains.length === 0 ? (
243
+ <Card>
244
+ <CardContent className="py-12 text-center text-muted-foreground">
245
+ {routineTab === 'active' ? '활성 루틴이 없습니다. 루틴을 추가해보세요.' : '중단된 루틴이 없습니다.'}
246
+ </CardContent>
247
+ </Card>
248
+ ) : (
249
+ <div className="space-y-8">
250
+ {filteredChains.map((chain, chainIdx) => {
251
+ const chainComplete = chain.every(r => r.completed_today)
252
+ return (
253
+ <div key={chainIdx}>
254
+ {/* Chain Header */}
255
+ <div className="flex items-center gap-2 mb-3">
256
+ <div className={cn(
257
+ 'w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold',
258
+ chainComplete ? 'bg-gatsaeng-teal/20 text-gatsaeng-teal' : 'bg-secondary text-muted-foreground'
259
+ )}>
260
+ {chainComplete ? <Trophy className="w-3 h-3" /> : chainIdx + 1}
261
+ </div>
262
+ <span className="text-xs uppercase tracking-wider text-muted-foreground">
263
+ Chain {chainIdx + 1}
264
+ </span>
265
+ {chainComplete && (
266
+ <Badge className="text-[10px] bg-gatsaeng-teal/10 text-gatsaeng-teal border-gatsaeng-teal/30">
267
+ 완료!
268
+ </Badge>
269
+ )}
270
+ </div>
271
+
272
+ {/* Chain Items with Visual Connector */}
273
+ <div className="relative">
274
+ {chain.map((routine, i) => (
275
+ <div key={routine.id}>
276
+ {/* Arrow connector between items */}
277
+ {i > 0 && (
278
+ <div className="flex items-center justify-center py-1">
279
+ <ArrowDown className={cn(
280
+ 'w-3.5 h-3.5',
281
+ chain[i - 1].completed_today ? 'text-gatsaeng-teal' : 'text-muted-foreground/30'
282
+ )} />
283
+ </div>
284
+ )}
285
+
286
+ <Card className={cn(
287
+ 'transition-all duration-300 group',
288
+ routine.completed_today && 'border-gatsaeng-teal/30 bg-gatsaeng-teal/5',
289
+ !routine.completed_today && i > 0 && chain[i - 1].completed_today && 'ring-1 ring-gatsaeng-amber/50'
290
+ )}>
291
+ <CardContent className="py-3 px-4">
292
+ <div className="flex items-center gap-3">
293
+ <Checkbox
294
+ checked={routine.completed_today}
295
+ onCheckedChange={() => handleToggle(routine)}
296
+ className="data-[state=checked]:bg-gatsaeng-teal data-[state=checked]:border-gatsaeng-teal"
297
+ />
298
+ <div className="flex-1 min-w-0">
299
+ <div className={cn(
300
+ 'text-sm font-medium',
301
+ routine.completed_today && 'line-through text-muted-foreground'
302
+ )}>
303
+ {routine.title}
304
+ </div>
305
+ <div className="flex items-center gap-2 mt-0.5">
306
+ {routine.trigger_cue && (
307
+ <Tooltip>
308
+ <TooltipTrigger asChild>
309
+ <div className="flex items-center gap-1 text-[10px] text-muted-foreground">
310
+ {TRIGGER_ICONS[routine.trigger_type]}
311
+ <span className="truncate max-w-[200px]">{routine.trigger_cue}</span>
312
+ </div>
313
+ </TooltipTrigger>
314
+ <TooltipContent>{routine.trigger_cue}</TooltipContent>
315
+ </Tooltip>
316
+ )}
317
+ </div>
318
+ </div>
319
+ <div className="flex items-center gap-2 flex-shrink-0">
320
+ <Zap className={cn(
321
+ 'w-3 h-3',
322
+ routine.energy_required === 'high' ? 'text-gatsaeng-red' :
323
+ routine.energy_required === 'medium' ? 'text-gatsaeng-amber' :
324
+ 'text-gatsaeng-teal'
325
+ )} />
326
+ {routine.streak > 0 && (
327
+ <Tooltip>
328
+ <TooltipTrigger asChild>
329
+ <div className="flex items-center gap-1">
330
+ <Flame className={cn(
331
+ 'w-3.5 h-3.5',
332
+ routine.streak >= 7 ? 'text-gatsaeng-red' :
333
+ routine.streak >= 3 ? 'text-gatsaeng-amber' :
334
+ 'text-muted-foreground'
335
+ )} />
336
+ <span className="text-xs font-bold text-gatsaeng-amber">
337
+ {routine.streak}
338
+ </span>
339
+ </div>
340
+ </TooltipTrigger>
341
+ <TooltipContent>
342
+ {routine.streak}일 연속 (최고: {routine.longest_streak}일)
343
+ </TooltipContent>
344
+ </Tooltip>
345
+ )}
346
+ <button
347
+ onClick={(e) => { e.stopPropagation(); setEditingRoutine(routine); setEditOpen(true) }}
348
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-primary/10 rounded"
349
+ >
350
+ <Pencil className="w-3.5 h-3.5 text-muted-foreground" />
351
+ </button>
352
+ <button
353
+ onClick={(e) => { e.stopPropagation(); handleToggleActive(routine) }}
354
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gatsaeng-amber/10 rounded"
355
+ title={routine.is_active ? '중단' : '재개'}
356
+ >
357
+ {routine.is_active !== false ? <Pause className="w-3.5 h-3.5 text-gatsaeng-amber" /> : <Play className="w-3.5 h-3.5 text-gatsaeng-teal" />}
358
+ </button>
359
+ <button
360
+ onClick={(e) => { e.stopPropagation(); handleDelete(routine.id, routine.title) }}
361
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/10 rounded"
362
+ >
363
+ <Trash2 className="w-3.5 h-3.5 text-gatsaeng-red" />
364
+ </button>
365
+ </div>
366
+ </div>
367
+
368
+ {/* Reward Note Animation */}
369
+ {showReward === routine.id && routine.reward_note && (
370
+ <div className="mt-2 p-2 bg-gatsaeng-teal/10 rounded-md border border-gatsaeng-teal/20 text-xs text-gatsaeng-teal animate-in fade-in slide-in-from-top-1 duration-300">
371
+ &ldquo;{routine.reward_note}&rdquo;
372
+ </div>
373
+ )}
374
+ </CardContent>
375
+ </Card>
376
+ </div>
377
+ ))}
378
+ </div>
379
+ </div>
380
+ )
381
+ })}
382
+ </div>
383
+ )}
384
+
385
+ {/* Edit Dialog */}
386
+ <Dialog open={editOpen} onOpenChange={(v) => { setEditOpen(v); if (!v) setEditingRoutine(null) }}>
387
+ <DialogContent className="max-w-lg">
388
+ <DialogHeader>
389
+ <DialogTitle>루틴 수정</DialogTitle>
390
+ </DialogHeader>
391
+ {editingRoutine && (
392
+ <form onSubmit={handleEditSubmit} className="space-y-4">
393
+ <div>
394
+ <Label>루틴 이름</Label>
395
+ <Input name="title" required defaultValue={editingRoutine.title} />
396
+ </div>
397
+ <div>
398
+ <Label>트리거 타입</Label>
399
+ <Select name="trigger_type" defaultValue={editingRoutine.trigger_type}>
400
+ <SelectTrigger><SelectValue /></SelectTrigger>
401
+ <SelectContent>
402
+ <SelectItem value="time">시간 기준</SelectItem>
403
+ <SelectItem value="event">이벤트 기준</SelectItem>
404
+ <SelectItem value="location">장소 기준</SelectItem>
405
+ </SelectContent>
406
+ </Select>
407
+ </div>
408
+ <div>
409
+ <Label>트리거 큐</Label>
410
+ <Input name="trigger_cue" defaultValue={editingRoutine.trigger_cue ?? ''} placeholder="기상 직후 / 점심 식사 후" />
411
+ </div>
412
+ <div>
413
+ <Label>에너지 소모</Label>
414
+ <Select name="energy_required" defaultValue={editingRoutine.energy_required}>
415
+ <SelectTrigger><SelectValue /></SelectTrigger>
416
+ <SelectContent>
417
+ <SelectItem value="low">낮음</SelectItem>
418
+ <SelectItem value="medium">중간</SelectItem>
419
+ <SelectItem value="high">높음</SelectItem>
420
+ </SelectContent>
421
+ </Select>
422
+ </div>
423
+ <div>
424
+ <Label>완료 후 나에게 하는 말</Label>
425
+ <Input name="reward_note" defaultValue={editingRoutine.reward_note ?? ''} placeholder="오늘도 내 뇌를 위한 투자를 했다" />
426
+ </div>
427
+ <Button type="submit" disabled={updateRoutine.isPending} className="w-full bg-gatsaeng-teal hover:bg-gatsaeng-teal/80 text-black">
428
+ 수정 완료
429
+ </Button>
430
+ </form>
431
+ )}
432
+ </DialogContent>
433
+ </Dialog>
434
+ </div>
435
+ )
436
+ }
@@ -0,0 +1,210 @@
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, Square, CheckSquare } from 'lucide-react'
11
+ import { useTask, useUpdateTask, useDeleteTask } from '@/hooks/useProjects'
12
+ import { TiptapEditor } from '@/components/editor/TiptapEditor'
13
+ import { PinButton } from '@/components/shared/PinButton'
14
+ import { cn } from '@/lib/utils'
15
+ import type { TaskStatus, TaskPriority } from '@/types'
16
+
17
+ const STATUS_LABELS: Record<TaskStatus, string> = {
18
+ backlog: '백로그',
19
+ todo: '할 일',
20
+ doing: '진행 중',
21
+ done: '완료',
22
+ }
23
+
24
+ const PRIORITY_LABELS: Record<TaskPriority, string> = {
25
+ urgent: '긴급',
26
+ high: '높음',
27
+ medium: '보통',
28
+ low: '낮음',
29
+ }
30
+
31
+ const PRIORITY_COLORS: Record<TaskPriority, string> = {
32
+ urgent: 'border-gatsaeng-red/50 text-gatsaeng-red',
33
+ high: 'border-orange-500/50 text-orange-500',
34
+ medium: 'border-gatsaeng-amber/50 text-gatsaeng-amber',
35
+ low: 'border-muted-foreground/30 text-muted-foreground',
36
+ }
37
+
38
+ export default function TaskDetailPage({ params }: { params: Promise<{ id: string }> }) {
39
+ const { id } = use(params)
40
+ const router = useRouter()
41
+ const { data: task, isLoading } = useTask(id)
42
+ const updateTask = useUpdateTask()
43
+ const deleteTask = useDeleteTask()
44
+ const [editingTitle, setEditingTitle] = useState(false)
45
+ const [titleValue, setTitleValue] = useState('')
46
+
47
+ const handleTitleSave = useCallback(() => {
48
+ if (!task || !titleValue.trim()) return
49
+ if (titleValue !== task.title) {
50
+ updateTask.mutate({ id, title: titleValue })
51
+ }
52
+ setEditingTitle(false)
53
+ }, [id, task, titleValue, updateTask])
54
+
55
+ const handleContentSave = useCallback((markdown: string) => {
56
+ updateTask.mutate({ id, content: markdown })
57
+ }, [id, updateTask])
58
+
59
+ const handleToggleDone = () => {
60
+ if (!task) return
61
+ updateTask.mutate({
62
+ id,
63
+ status: task.status === 'done' ? 'todo' : 'done',
64
+ })
65
+ }
66
+
67
+ const handleDelete = () => {
68
+ if (!confirm('이 할일을 삭제하시겠습니까?')) return
69
+ deleteTask.mutate(id, {
70
+ onSuccess: () => router.push('/tasks'),
71
+ })
72
+ }
73
+
74
+ if (isLoading) {
75
+ return (
76
+ <div className="flex items-center justify-center h-64">
77
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
78
+ </div>
79
+ )
80
+ }
81
+
82
+ if (!task) {
83
+ return (
84
+ <div className="text-center py-20">
85
+ <p className="text-muted-foreground">할일을 찾을 수 없습니다</p>
86
+ <Button variant="outline" className="mt-4" onClick={() => router.push('/tasks')}>
87
+ <ArrowLeft className="w-4 h-4 mr-2" /> 돌아가기
88
+ </Button>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ const isDone = task.status === 'done'
94
+
95
+ return (
96
+ <div className="max-w-3xl mx-auto">
97
+ {/* Header */}
98
+ <div className="flex items-center gap-3 mb-6">
99
+ <Button variant="ghost" size="icon" onClick={() => router.push('/tasks')}>
100
+ <ArrowLeft className="w-5 h-5" />
101
+ </Button>
102
+ <button onClick={handleToggleDone} className="shrink-0">
103
+ {isDone ? (
104
+ <CheckSquare className="w-6 h-6 text-gatsaeng-teal" />
105
+ ) : (
106
+ <Square className="w-6 h-6 text-muted-foreground hover:text-primary transition-colors" />
107
+ )}
108
+ </button>
109
+ <div className="flex-1 min-w-0">
110
+ {editingTitle ? (
111
+ <Input
112
+ value={titleValue}
113
+ onChange={(e) => setTitleValue(e.target.value)}
114
+ onBlur={handleTitleSave}
115
+ onKeyDown={(e) => {
116
+ if (e.key === 'Enter') handleTitleSave()
117
+ if (e.key === 'Escape') setEditingTitle(false)
118
+ }}
119
+ autoFocus
120
+ className="text-2xl font-bold h-auto py-1"
121
+ />
122
+ ) : (
123
+ <h1
124
+ className={cn(
125
+ 'text-2xl font-bold cursor-pointer hover:text-primary/80 transition-colors truncate',
126
+ isDone ? 'line-through text-muted-foreground' : 'text-foreground'
127
+ )}
128
+ onDoubleClick={() => {
129
+ setTitleValue(task.title)
130
+ setEditingTitle(true)
131
+ }}
132
+ title="더블클릭하여 편집"
133
+ >
134
+ {task.title}
135
+ </h1>
136
+ )}
137
+ </div>
138
+ <PinButton type="task" id={id} title={task.title} size={20} />
139
+ <Button variant="ghost" size="icon" onClick={handleDelete} className="text-muted-foreground hover:text-gatsaeng-red">
140
+ <Trash2 className="w-4 h-4" />
141
+ </Button>
142
+ </div>
143
+
144
+ {/* Properties */}
145
+ <Card className="mb-6">
146
+ <CardContent className="py-4">
147
+ <div className="grid grid-cols-2 gap-4">
148
+ <div>
149
+ <label className="text-xs text-muted-foreground mb-1 block">상태</label>
150
+ <Select
151
+ value={task.status}
152
+ onValueChange={(v) => updateTask.mutate({ id, status: v as TaskStatus })}
153
+ >
154
+ <SelectTrigger className="h-8 text-sm">
155
+ <SelectValue />
156
+ </SelectTrigger>
157
+ <SelectContent>
158
+ {Object.entries(STATUS_LABELS).map(([k, v]) => (
159
+ <SelectItem key={k} value={k}>{v}</SelectItem>
160
+ ))}
161
+ </SelectContent>
162
+ </Select>
163
+ </div>
164
+ <div>
165
+ <label className="text-xs text-muted-foreground mb-1 block">우선순위</label>
166
+ <Select
167
+ value={task.priority}
168
+ onValueChange={(v) => updateTask.mutate({ id, priority: v as TaskPriority })}
169
+ >
170
+ <SelectTrigger className="h-8 text-sm">
171
+ <SelectValue />
172
+ </SelectTrigger>
173
+ <SelectContent>
174
+ {Object.entries(PRIORITY_LABELS).map(([k, v]) => (
175
+ <SelectItem key={k} value={k}>{v}</SelectItem>
176
+ ))}
177
+ </SelectContent>
178
+ </Select>
179
+ </div>
180
+ <div>
181
+ <label className="text-xs text-muted-foreground mb-1 block">마감일</label>
182
+ <Input
183
+ type="date"
184
+ value={task.due_date ?? ''}
185
+ onChange={(e) => updateTask.mutate({ id, due_date: e.target.value || undefined })}
186
+ className="h-8 text-sm"
187
+ />
188
+ </div>
189
+ <div className="flex items-end">
190
+ <Badge variant="outline" className={PRIORITY_COLORS[task.priority]}>
191
+ {PRIORITY_LABELS[task.priority]}
192
+ </Badge>
193
+ </div>
194
+ </div>
195
+ </CardContent>
196
+ </Card>
197
+
198
+ {/* Editor */}
199
+ <Card>
200
+ <CardContent className="p-0">
201
+ <TiptapEditor
202
+ content={task._content ?? ''}
203
+ onSave={handleContentSave}
204
+ placeholder="상세 내용이나 메모를 입력하세요..."
205
+ />
206
+ </CardContent>
207
+ </Card>
208
+ </div>
209
+ )
210
+ }