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,46 @@
1
+ 'use client'
2
+
3
+ import { Component, type ReactNode } from 'react'
4
+ import { Card, CardContent } from '@/components/ui/card'
5
+ import { Button } from '@/components/ui/button'
6
+ import { AlertTriangle } from 'lucide-react'
7
+
8
+ interface Props {
9
+ children: ReactNode
10
+ fallback?: ReactNode
11
+ }
12
+
13
+ interface State {
14
+ hasError: boolean
15
+ error?: Error
16
+ }
17
+
18
+ export class ErrorBoundary extends Component<Props, State> {
19
+ state: State = { hasError: false }
20
+
21
+ static getDerivedStateFromError(error: Error): State {
22
+ return { hasError: true, error }
23
+ }
24
+
25
+ render() {
26
+ if (this.state.hasError) {
27
+ return this.props.fallback ?? (
28
+ <Card className="border-gatsaeng-red/30">
29
+ <CardContent className="py-8 text-center">
30
+ <AlertTriangle className="w-8 h-8 text-gatsaeng-red mx-auto mb-3" />
31
+ <p className="text-sm text-foreground mb-1">오류가 발생했습니다</p>
32
+ <p className="text-xs text-muted-foreground mb-4">{this.state.error?.message}</p>
33
+ <Button
34
+ variant="outline"
35
+ size="sm"
36
+ onClick={() => this.setState({ hasError: false, error: undefined })}
37
+ >
38
+ 다시 시도
39
+ </Button>
40
+ </CardContent>
41
+ </Card>
42
+ )
43
+ }
44
+ return this.props.children
45
+ }
46
+ }
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { useDashboardStore } from '@/stores/dashboardStore'
5
+ import { RoutineChecklist } from './RoutineChecklist'
6
+ import { GoalRings } from './GoalRings'
7
+ import { FocusTimer } from './FocusTimer'
8
+ import { GatsaengScore } from './GatsaengScore'
9
+ import { ZeigarnikPanel } from './ZeigarnikPanel'
10
+ import { EnergyTracker } from './EnergyTracker'
11
+ import { DdayWidget } from './DdayWidget'
12
+ import { ProactiveBar } from './ProactiveBar'
13
+ import { TimingWidget } from './TimingWidget'
14
+ import type { WidgetId } from '@/types'
15
+
16
+ const WIDGETS: Partial<Record<WidgetId, React.ComponentType>> = {
17
+ routine: RoutineChecklist,
18
+ goals: GoalRings,
19
+ timer: FocusTimer,
20
+ zeigarnik: () => null, // rendered separately as top bar
21
+ energy: EnergyTracker,
22
+ dday: DdayWidget,
23
+ proactive: () => null, // rendered as top bar
24
+ heatmap: () => (
25
+ <div className="border border-border rounded-lg p-6 text-center text-muted-foreground text-sm">
26
+ Activity Heatmap — coming soon
27
+ </div>
28
+ ),
29
+ kanban: () => (
30
+ <div className="border border-border rounded-lg p-6 text-center text-muted-foreground text-sm">
31
+ Quick Kanban — coming soon
32
+ </div>
33
+ ),
34
+ }
35
+
36
+ export function DashboardGrid() {
37
+ const { activeWidgets } = useDashboardStore()
38
+ const { data: profile } = useQuery({
39
+ queryKey: ['profile'],
40
+ queryFn: async () => {
41
+ const res = await fetch('/api/profile')
42
+ return res.json()
43
+ },
44
+ staleTime: 1000 * 60 * 30,
45
+ })
46
+
47
+ const displayWidgets = activeWidgets.filter(w => w !== 'zeigarnik' && w !== 'proactive')
48
+
49
+ return (
50
+ <div className="space-y-6">
51
+ {/* Proactive alerts bar */}
52
+ {activeWidgets.includes('proactive') && <ProactiveBar />}
53
+
54
+ {/* Zeigarnik panel */}
55
+ {activeWidgets.includes('zeigarnik') && <ZeigarnikPanel />}
56
+
57
+ {/* Saju motto banner */}
58
+ {profile?.saju_motto && (
59
+ <div className="border border-gatsaeng-purple/30 bg-gatsaeng-purple/5 rounded-lg px-4 py-3">
60
+ <div className="text-[10px] uppercase tracking-wider text-gatsaeng-purple/60 mb-1">
61
+ 사주 모토
62
+ </div>
63
+ <p className="text-sm text-foreground font-medium">{profile.saju_motto}</p>
64
+ {profile.saju_identity && (
65
+ <p className="text-xs text-muted-foreground mt-1">{profile.saju_identity}</p>
66
+ )}
67
+ </div>
68
+ )}
69
+
70
+ {/* Score + Timing row */}
71
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
72
+ <GatsaengScore />
73
+ <TimingWidget />
74
+ </div>
75
+
76
+ {/* Widget grid */}
77
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
78
+ {displayWidgets.map(widgetId => {
79
+ const Widget = WIDGETS[widgetId]
80
+ if (!Widget) return null
81
+ return <Widget key={widgetId} />
82
+ })}
83
+ </div>
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,88 @@
1
+ 'use client'
2
+
3
+ import { useMilestonesWithDDay } from '@/hooks/useMilestones'
4
+ import { useGoals } from '@/hooks/useGoals'
5
+ import { WidgetWrapper } from './WidgetWrapper'
6
+ import { Calendar, Target } from 'lucide-react'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import Link from 'next/link'
9
+
10
+ function formatDDay(d: number) {
11
+ if (d === 0) return 'D-Day'
12
+ if (d > 0) return `D-${d}`
13
+ return `D+${Math.abs(d)}`
14
+ }
15
+
16
+ function ddayColor(d: number) {
17
+ if (d <= 0) return 'text-gatsaeng-red'
18
+ if (d <= 7) return 'text-gatsaeng-red'
19
+ if (d <= 30) return 'text-gatsaeng-amber'
20
+ return 'text-foreground'
21
+ }
22
+
23
+ export function DdayWidget() {
24
+ const { data: milestones = [], isLoading } = useMilestonesWithDDay()
25
+ const { data: goals = [] } = useGoals()
26
+
27
+ const active = milestones.filter(m => m.status === 'active').slice(0, 5)
28
+
29
+ const goalMap = new Map(goals.map(g => [g.id, g]))
30
+
31
+ return (
32
+ <WidgetWrapper title="D-day 트래커" icon={<Calendar className="w-4 h-4 text-gatsaeng-red" />}>
33
+ {isLoading ? (
34
+ <div className="space-y-3">
35
+ {[1, 2, 3].map(i => <div key={i} className="h-10 bg-muted/30 rounded animate-pulse" />)}
36
+ </div>
37
+ ) : active.length === 0 ? (
38
+ <p className="text-sm text-muted-foreground">활성 마일스톤이 없습니다.</p>
39
+ ) : (
40
+ <div className="space-y-3">
41
+ {active.map(m => {
42
+ const goal = goalMap.get(m.goal_id)
43
+ const progress = m.target_value > 0
44
+ ? Math.min(100, Math.round((m.current_value / m.target_value) * 100))
45
+ : 0
46
+ return (
47
+ <Link
48
+ key={m.id}
49
+ href={`/goals/${m.goal_id}`}
50
+ className="flex items-center gap-3 group"
51
+ >
52
+ <div className={`text-lg font-bold font-mono min-w-[60px] text-right ${ddayColor(m.d_day)}`}>
53
+ {formatDDay(m.d_day)}
54
+ </div>
55
+ <div className="flex-1 min-w-0">
56
+ <div className="text-sm font-medium text-foreground truncate group-hover:text-primary transition-colors">
57
+ {m.title}
58
+ </div>
59
+ <div className="flex items-center gap-2 mt-0.5">
60
+ {goal && (
61
+ <div className="flex items-center gap-1">
62
+ <Target className="w-3 h-3" style={{ color: goal.color }} />
63
+ <span className="text-[10px] text-muted-foreground truncate max-w-[100px]">{goal.title}</span>
64
+ </div>
65
+ )}
66
+ <Badge variant="outline" className="text-[10px] px-1 py-0">
67
+ {progress}%
68
+ </Badge>
69
+ </div>
70
+ </div>
71
+ {/* mini progress bar */}
72
+ <div className="w-12 h-1.5 bg-muted rounded-full overflow-hidden">
73
+ <div
74
+ className="h-full rounded-full transition-all"
75
+ style={{
76
+ width: `${progress}%`,
77
+ backgroundColor: goal?.color ?? '#58a6ff',
78
+ }}
79
+ />
80
+ </div>
81
+ </Link>
82
+ )
83
+ })}
84
+ </div>
85
+ )}
86
+ </WidgetWrapper>
87
+ )
88
+ }
@@ -0,0 +1,87 @@
1
+ 'use client'
2
+
3
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
4
+ import { WidgetWrapper } from './WidgetWrapper'
5
+ import { Zap } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+ import { apiFetch } from '@/lib/apiFetch'
8
+ import { getToday } from '@/lib/date'
9
+ import type { EnergyLog } from '@/types'
10
+
11
+ const ENERGY_LEVELS = [
12
+ { value: 1, label: '매우 낮음', color: 'bg-gatsaeng-red' },
13
+ { value: 2, label: '낮음', color: 'bg-orange-500' },
14
+ { value: 3, label: '보통', color: 'bg-yellow-500' },
15
+ { value: 4, label: '높음', color: 'bg-gatsaeng-teal' },
16
+ { value: 5, label: '최고', color: 'bg-green-500' },
17
+ ]
18
+
19
+ export function EnergyTracker() {
20
+ const today = getToday()
21
+ const currentHour = new Date().getHours()
22
+ const queryClient = useQueryClient()
23
+
24
+ const { data: energyLog } = useQuery({
25
+ queryKey: ['energy-log', today],
26
+ queryFn: () => apiFetch<EnergyLog>(`/api/logs/energy?date=${today}`),
27
+ })
28
+
29
+ const logMutation = useMutation({
30
+ mutationFn: (level: number) =>
31
+ apiFetch('/api/logs/energy', {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({ date: today, hour: currentHour, level }),
35
+ }),
36
+ onSuccess: () => {
37
+ queryClient.invalidateQueries({ queryKey: ['energy-log', today] })
38
+ },
39
+ })
40
+
41
+ const currentEntry = energyLog?.entries?.find(e => e.hour === currentHour)
42
+
43
+ return (
44
+ <WidgetWrapper title="에너지 체크" icon={<Zap className="w-4 h-4 text-gatsaeng-amber" />} widgetId="energy">
45
+ <div className="space-y-3">
46
+ <p className="text-xs text-muted-foreground">
47
+ 현재 시간: {currentHour}시 — 지금 에너지 레벨은?
48
+ </p>
49
+ <div className="flex gap-2">
50
+ {ENERGY_LEVELS.map(level => (
51
+ <button
52
+ key={level.value}
53
+ onClick={() => logMutation.mutate(level.value)}
54
+ className={cn(
55
+ 'w-8 h-8 rounded-full transition-all text-xs font-bold',
56
+ currentEntry?.level === level.value
57
+ ? `${level.color} text-white scale-110`
58
+ : 'bg-muted text-muted-foreground hover:scale-105'
59
+ )}
60
+ >
61
+ {level.value}
62
+ </button>
63
+ ))}
64
+ </div>
65
+ {energyLog?.entries && energyLog.entries.length > 0 && (
66
+ <div className="flex gap-1 mt-2">
67
+ {Array.from({ length: 24 }, (_, h) => {
68
+ const entry = energyLog.entries.find(e => e.hour === h)
69
+ return (
70
+ <div
71
+ key={h}
72
+ className={cn(
73
+ 'w-2 h-6 rounded-sm',
74
+ entry
75
+ ? ENERGY_LEVELS[entry.level - 1]?.color ?? 'bg-muted'
76
+ : 'bg-muted/30'
77
+ )}
78
+ title={`${h}시: ${entry ? `레벨 ${entry.level}` : '미기록'}`}
79
+ />
80
+ )
81
+ })}
82
+ </div>
83
+ )}
84
+ </div>
85
+ </WidgetWrapper>
86
+ )
87
+ }
@@ -0,0 +1,139 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useCallback } from 'react'
4
+ import { WidgetWrapper } from './WidgetWrapper'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Badge } from '@/components/ui/badge'
7
+ import { Input } from '@/components/ui/input'
8
+ import { Timer, Play, Pause, RotateCcw } from 'lucide-react'
9
+ import { useTimerStore } from '@/stores/timerStore'
10
+ import { cn } from '@/lib/utils'
11
+ import type { SessionType } from '@/types'
12
+
13
+ const SESSION_LABELS: Record<SessionType, string> = {
14
+ pomodoro_25: '25분',
15
+ focus_90: '90분 딥워크',
16
+ deep_work: '커스텀',
17
+ }
18
+
19
+ export function FocusTimer() {
20
+ const seconds = useTimerStore(s => s.seconds)
21
+ const isRunning = useTimerStore(s => s.isRunning)
22
+ const sessionType = useTimerStore(s => s.sessionType)
23
+ const completedSessions = useTimerStore(s => s.completedSessions)
24
+ const customMinutes = useTimerStore(s => s.customMinutes)
25
+ const startedAt = useTimerStore(s => s.startedAt)
26
+ const start = useTimerStore(s => s.start)
27
+ const pause = useTimerStore(s => s.pause)
28
+ const reset = useTimerStore(s => s.reset)
29
+ const tick = useTimerStore(s => s.tick)
30
+ const setSessionType = useTimerStore(s => s.setSessionType)
31
+ const setCustomMinutes = useTimerStore(s => s.setCustomMinutes)
32
+
33
+ const intervalRef = useRef<NodeJS.Timeout | null>(null)
34
+ const prevCompletedRef = useRef(completedSessions)
35
+
36
+ const saveFocusSession = useCallback(async (started: string, type: SessionType, custom: number) => {
37
+ const durationMap: Record<SessionType, number> = {
38
+ pomodoro_25: 25,
39
+ focus_90: 90,
40
+ deep_work: custom,
41
+ }
42
+ try {
43
+ await fetch('/api/logs/focus', {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({
47
+ session_type: type,
48
+ duration_minutes: durationMap[type],
49
+ completed: true,
50
+ started_at: started,
51
+ ended_at: new Date().toISOString(),
52
+ }),
53
+ })
54
+ } catch {
55
+ // silently fail — vault save is best-effort from dashboard widget
56
+ }
57
+ }, [])
58
+
59
+ useEffect(() => {
60
+ if (completedSessions > prevCompletedRef.current && startedAt) {
61
+ saveFocusSession(startedAt, sessionType, customMinutes)
62
+ }
63
+ prevCompletedRef.current = completedSessions
64
+ }, [completedSessions, startedAt, sessionType, customMinutes, saveFocusSession])
65
+
66
+ useEffect(() => {
67
+ if (isRunning) {
68
+ intervalRef.current = setInterval(tick, 1000)
69
+ } else if (intervalRef.current) {
70
+ clearInterval(intervalRef.current)
71
+ }
72
+ return () => {
73
+ if (intervalRef.current) clearInterval(intervalRef.current)
74
+ }
75
+ }, [isRunning, tick])
76
+
77
+ const minutes = Math.floor(seconds / 60)
78
+ const secs = seconds % 60
79
+
80
+ return (
81
+ <WidgetWrapper title="포커스 타이머" icon={<Timer className="w-4 h-4" />} widgetId="timer">
82
+ <div className="flex flex-col items-center gap-4">
83
+ <div className="flex gap-2">
84
+ {(Object.keys(SESSION_LABELS) as SessionType[]).map(type => (
85
+ <Badge
86
+ key={type}
87
+ variant={sessionType === type ? 'default' : 'outline'}
88
+ className={cn(
89
+ 'cursor-pointer text-xs',
90
+ sessionType === type && 'bg-gatsaeng-purple text-white'
91
+ )}
92
+ onClick={() => setSessionType(type)}
93
+ >
94
+ {SESSION_LABELS[type]}
95
+ </Badge>
96
+ ))}
97
+ </div>
98
+
99
+ {sessionType === 'deep_work' && !isRunning && seconds === customMinutes * 60 && (
100
+ <div className="flex items-center gap-2">
101
+ <Input
102
+ type="number"
103
+ value={customMinutes}
104
+ onChange={e => setCustomMinutes(Number(e.target.value))}
105
+ className="w-20 h-8 text-center text-sm"
106
+ min={1}
107
+ max={180}
108
+ />
109
+ <span className="text-xs text-muted-foreground">분</span>
110
+ </div>
111
+ )}
112
+
113
+ <div className="text-4xl font-mono font-bold text-gatsaeng-purple tabular-nums">
114
+ {String(minutes).padStart(2, '0')}:{String(secs).padStart(2, '0')}
115
+ </div>
116
+
117
+ <div className="flex gap-2">
118
+ <Button
119
+ size="sm"
120
+ variant={isRunning ? 'outline' : 'default'}
121
+ onClick={isRunning ? pause : start}
122
+ className={cn(!isRunning && 'bg-gatsaeng-purple hover:bg-gatsaeng-purple/80')}
123
+ >
124
+ {isRunning ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
125
+ </Button>
126
+ <Button size="sm" variant="outline" onClick={reset}>
127
+ <RotateCcw className="w-4 h-4" />
128
+ </Button>
129
+ </div>
130
+
131
+ {completedSessions > 0 && (
132
+ <span className="text-xs text-muted-foreground">
133
+ 완료 세션: {completedSessions}
134
+ </span>
135
+ )}
136
+ </div>
137
+ </WidgetWrapper>
138
+ )
139
+ }
@@ -0,0 +1,30 @@
1
+ 'use client'
2
+
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { WidgetWrapper } from './WidgetWrapper'
5
+ import { Zap } from 'lucide-react'
6
+ import type { Profile } from '@/types'
7
+
8
+ export function GatsaengScore() {
9
+ const { data: profile } = useQuery({
10
+ queryKey: ['profile'],
11
+ queryFn: async (): Promise<Profile> => {
12
+ const res = await fetch('/api/profile')
13
+ return res.json()
14
+ },
15
+ })
16
+
17
+ return (
18
+ <WidgetWrapper title="갓생 스코어" icon={<Zap className="w-4 h-4 text-gatsaeng-amber" />}>
19
+ <div className="flex items-center gap-4">
20
+ <div className="text-3xl font-bold font-mono text-gatsaeng-amber">
21
+ {profile?.total_score ?? 0}
22
+ </div>
23
+ <div className="text-sm text-muted-foreground">
24
+ <div>Lv. {profile?.level ?? 1}</div>
25
+ <div>연속 {profile?.current_streak ?? 0}일</div>
26
+ </div>
27
+ </div>
28
+ </WidgetWrapper>
29
+ )
30
+ }
@@ -0,0 +1,107 @@
1
+ 'use client'
2
+
3
+ import { WidgetWrapper } from './WidgetWrapper'
4
+ import { Target } from 'lucide-react'
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
6
+ import { useGoals } from '@/hooks/useGoals'
7
+
8
+ const DEFAULT_RING_COLOR = '#58a6ff'
9
+ const VALID_COLOR_RE = /^#[0-9a-fA-F]{3,8}$|^(rgb|hsl)/
10
+
11
+ function RingChart({ progress, color, size = 60 }: { progress: number; color: string; size?: number }) {
12
+ const radius = (size - 8) / 2
13
+ const circumference = 2 * Math.PI * radius
14
+ const offset = circumference - (progress / 100) * circumference
15
+ const safeColor = VALID_COLOR_RE.test(color) ? color : DEFAULT_RING_COLOR
16
+
17
+ return (
18
+ <svg width={size} height={size} className="transform -rotate-90">
19
+ <circle
20
+ cx={size / 2}
21
+ cy={size / 2}
22
+ r={radius}
23
+ fill="none"
24
+ stroke="currentColor"
25
+ strokeWidth={4}
26
+ className="text-muted/30"
27
+ />
28
+ <circle
29
+ cx={size / 2}
30
+ cy={size / 2}
31
+ r={radius}
32
+ fill="none"
33
+ stroke={safeColor}
34
+ strokeWidth={4}
35
+ strokeDasharray={circumference}
36
+ strokeDashoffset={offset}
37
+ strokeLinecap="round"
38
+ className="transition-all duration-500"
39
+ />
40
+ </svg>
41
+ )
42
+ }
43
+
44
+ export function GoalRings() {
45
+ const { data: goals, isLoading } = useGoals()
46
+
47
+ const activeGoals = (goals ?? []).filter(g => g.status === 'active')
48
+
49
+ if (isLoading) {
50
+ return (
51
+ <WidgetWrapper title="목표 진척률" icon={<Target className="w-4 h-4" />}>
52
+ <div className="flex gap-4">
53
+ {[1, 2, 3].map(i => (
54
+ <div key={i} className="w-16 h-16 rounded-full bg-muted animate-pulse" />
55
+ ))}
56
+ </div>
57
+ </WidgetWrapper>
58
+ )
59
+ }
60
+
61
+ return (
62
+ <WidgetWrapper title="목표 진척률" icon={<Target className="w-4 h-4" />}>
63
+ {activeGoals.length === 0 ? (
64
+ <p className="text-sm text-muted-foreground">목표를 추가해보세요</p>
65
+ ) : (
66
+ <div className="flex flex-wrap gap-4">
67
+ {activeGoals.map(goal => {
68
+ const progress = goal.target_value
69
+ ? Math.round(((goal.current_value ?? 0) / goal.target_value) * 100)
70
+ : 0
71
+
72
+ return (
73
+ <Tooltip key={goal.id}>
74
+ <TooltipTrigger asChild>
75
+ <div className="flex flex-col items-center gap-1 cursor-pointer">
76
+ <div className="relative">
77
+ <RingChart progress={progress} color={goal.color} />
78
+ <span className="absolute inset-0 flex items-center justify-center text-xs font-mono">
79
+ {progress}%
80
+ </span>
81
+ </div>
82
+ <span className="text-xs text-muted-foreground max-w-[70px] truncate">
83
+ {goal.title}
84
+ </span>
85
+ </div>
86
+ </TooltipTrigger>
87
+ <TooltipContent side="bottom" className="max-w-xs">
88
+ <p className="font-medium">{goal.title}</p>
89
+ {goal.why_statement && (
90
+ <p className="text-xs text-muted-foreground mt-1">
91
+ Why: {goal.why_statement}
92
+ </p>
93
+ )}
94
+ {goal.target_value && (
95
+ <p className="text-xs mt-1">
96
+ {goal.current_value ?? 0} / {goal.target_value} {goal.unit}
97
+ </p>
98
+ )}
99
+ </TooltipContent>
100
+ </Tooltip>
101
+ )
102
+ })}
103
+ </div>
104
+ )}
105
+ </WidgetWrapper>
106
+ )
107
+ }
@@ -0,0 +1,98 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useMilestonesWithDDay } from '@/hooks/useMilestones'
5
+ import { useRoutines } from '@/hooks/useRoutines'
6
+ import { AlertTriangle, Clock, Target, ChevronRight, X } from 'lucide-react'
7
+ import type { ProactiveAlert } from '@/types'
8
+
9
+ export function ProactiveBar() {
10
+ const { data: milestones = [] } = useMilestonesWithDDay()
11
+ const { routines } = useRoutines()
12
+ const [dismissed, setDismissed] = useState<Set<string>>(new Set())
13
+
14
+ // build alerts from current data
15
+ const alerts: ProactiveAlert[] = []
16
+ const now = new Date()
17
+ const hour = now.getHours()
18
+
19
+ // 1. milestone D-day alerts (≤ 7 days)
20
+ milestones
21
+ .filter(m => m.status === 'active' && m.d_day <= 7 && m.d_day >= 0)
22
+ .forEach(m => {
23
+ alerts.push({
24
+ id: `ms-${m.id}`,
25
+ type: 'milestone_dday',
26
+ title: m.d_day === 0 ? 'D-Day!' : `D-${m.d_day}`,
27
+ message: m.title,
28
+ created_at: now.toISOString(),
29
+ })
30
+ })
31
+
32
+ // 2. skipped routine alerts (only after 11am)
33
+ if (hour >= 11) {
34
+ const skipped = routines.filter(r => !r.completed_today)
35
+ if (skipped.length > 0) {
36
+ alerts.push({
37
+ id: 'skipped-routines',
38
+ type: 'skipped_routine',
39
+ title: `미완료 루틴 ${skipped.length}개`,
40
+ message: skipped.slice(0, 3).map(r => r.title).join(', '),
41
+ created_at: now.toISOString(),
42
+ })
43
+ }
44
+ }
45
+
46
+ // 3. overdue milestones
47
+ milestones
48
+ .filter(m => m.status === 'active' && m.d_day < 0)
49
+ .slice(0, 2)
50
+ .forEach(m => {
51
+ alerts.push({
52
+ id: `overdue-${m.id}`,
53
+ type: 'deadline',
54
+ title: `D+${Math.abs(m.d_day)} 초과`,
55
+ message: m.title,
56
+ created_at: now.toISOString(),
57
+ })
58
+ })
59
+
60
+ const visible = alerts.filter(a => !dismissed.has(a.id))
61
+
62
+ if (visible.length === 0) return null
63
+
64
+ const iconMap: Record<ProactiveAlert['type'], React.ReactNode> = {
65
+ skipped_routine: <Clock className="w-3.5 h-3.5" />,
66
+ deadline: <AlertTriangle className="w-3.5 h-3.5" />,
67
+ milestone_dday: <Target className="w-3.5 h-3.5" />,
68
+ reanalysis_due: <ChevronRight className="w-3.5 h-3.5" />,
69
+ }
70
+
71
+ const colorMap: Record<ProactiveAlert['type'], string> = {
72
+ skipped_routine: 'bg-gatsaeng-amber/10 border-gatsaeng-amber/30 text-gatsaeng-amber',
73
+ deadline: 'bg-gatsaeng-red/10 border-gatsaeng-red/30 text-gatsaeng-red',
74
+ milestone_dday: 'bg-gatsaeng-purple/10 border-gatsaeng-purple/30 text-gatsaeng-purple',
75
+ reanalysis_due: 'bg-primary/10 border-primary/30 text-primary',
76
+ }
77
+
78
+ return (
79
+ <div className="space-y-2 mb-4">
80
+ {visible.map(alert => (
81
+ <div
82
+ key={alert.id}
83
+ className={`flex items-center gap-3 px-4 py-2 rounded-lg border text-sm ${colorMap[alert.type]}`}
84
+ >
85
+ {iconMap[alert.type]}
86
+ <span className="font-medium">{alert.title}</span>
87
+ <span className="text-foreground/70">{alert.message}</span>
88
+ <button
89
+ onClick={() => setDismissed(prev => new Set(prev).add(alert.id))}
90
+ className="ml-auto opacity-50 hover:opacity-100 transition-opacity"
91
+ >
92
+ <X className="w-3.5 h-3.5" />
93
+ </button>
94
+ </div>
95
+ ))}
96
+ </div>
97
+ )
98
+ }