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,122 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import {
6
+ Home,
7
+ CheckSquare,
8
+ StickyNote,
9
+ Target,
10
+ Menu,
11
+ Layers,
12
+ FolderKanban,
13
+ RotateCcw,
14
+ BookMarked,
15
+ CalendarDays,
16
+ BookOpen,
17
+ Timer,
18
+ X,
19
+ Mic,
20
+ } from 'lucide-react'
21
+ import { cn } from '@/lib/utils'
22
+ import { useState } from 'react'
23
+
24
+ const NAV_ITEMS = [
25
+ { href: '/', label: '홈', icon: Home },
26
+ { href: '/tasks', label: '할일', icon: CheckSquare },
27
+ { href: '/notes', label: '노트', icon: StickyNote },
28
+ { href: '/goals', label: '목표', icon: Target },
29
+ ]
30
+
31
+ // No duplicate '/' — Dashboard is already in NAV_ITEMS as '홈'
32
+ const MORE_ITEMS = [
33
+ { href: '/areas', label: '영역', icon: Layers },
34
+ { href: '/projects', label: '프로젝트', icon: FolderKanban },
35
+ { href: '/routines', label: '루틴', icon: RotateCcw },
36
+ { href: '/books', label: '독서', icon: BookMarked },
37
+ { href: '/calendar', label: '캘린더', icon: CalendarDays },
38
+ { href: '/focus', label: '포커스', icon: Timer },
39
+ { href: '/review', label: '계획 & 회고', icon: BookOpen },
40
+ { href: '/voice', label: 'Eve 보이스', icon: Mic },
41
+ ]
42
+
43
+ function isNavActive(href: string, pathname: string) {
44
+ return href === '/' ? pathname === '/' : pathname.startsWith(href)
45
+ }
46
+
47
+ export function MobileBottomNav() {
48
+ const pathname = usePathname()
49
+ const [showMore, setShowMore] = useState(false)
50
+
51
+ return (
52
+ <>
53
+ {/* More menu overlay */}
54
+ {showMore && (
55
+ <div className="fixed inset-0 z-40 md:hidden">
56
+ <div className="absolute inset-0 bg-black/60" onClick={() => setShowMore(false)} />
57
+ <div className="absolute bottom-16 left-0 right-0 bg-background border-t border-border rounded-t-xl p-4 animate-in slide-in-from-bottom-4">
58
+ <div className="flex items-center justify-between mb-3">
59
+ <span className="text-sm font-medium">메뉴</span>
60
+ <button onClick={() => setShowMore(false)} className="p-1">
61
+ <X className="w-4 h-4" />
62
+ </button>
63
+ </div>
64
+ <div className="grid grid-cols-4 gap-3">
65
+ {MORE_ITEMS.map(item => {
66
+ const Icon = item.icon
67
+ const isActive = isNavActive(item.href, pathname)
68
+ return (
69
+ <Link
70
+ key={item.href}
71
+ href={item.href}
72
+ onClick={() => setShowMore(false)}
73
+ className={cn(
74
+ 'flex flex-col items-center gap-1 py-2 rounded-lg text-xs transition-colors',
75
+ isActive ? 'text-primary bg-primary/10' : 'text-muted-foreground'
76
+ )}
77
+ >
78
+ <Icon className="w-5 h-5" />
79
+ {item.label}
80
+ </Link>
81
+ )
82
+ })}
83
+ </div>
84
+ </div>
85
+ </div>
86
+ )}
87
+
88
+ {/* Bottom nav bar */}
89
+ <nav className="fixed bottom-0 left-0 right-0 z-30 md:hidden bg-background border-t border-border">
90
+ <div className="flex items-center justify-around h-14 px-2">
91
+ {NAV_ITEMS.map(item => {
92
+ const Icon = item.icon
93
+ const isActive = isNavActive(item.href, pathname)
94
+ return (
95
+ <Link
96
+ key={item.href}
97
+ href={item.href}
98
+ className={cn(
99
+ 'flex flex-col items-center gap-0.5 py-1 px-3 rounded-lg text-[10px] transition-colors',
100
+ isActive ? 'text-primary' : 'text-muted-foreground'
101
+ )}
102
+ >
103
+ <Icon className={cn('w-5 h-5', isActive && 'stroke-[2.5]')} />
104
+ {item.label}
105
+ </Link>
106
+ )
107
+ })}
108
+ <button
109
+ onClick={() => setShowMore(v => !v)}
110
+ className={cn(
111
+ 'flex flex-col items-center gap-0.5 py-1 px-3 rounded-lg text-[10px] transition-colors',
112
+ showMore ? 'text-primary' : 'text-muted-foreground'
113
+ )}
114
+ >
115
+ <Menu className={cn('w-5 h-5', showMore && 'stroke-[2.5]')} />
116
+ 메뉴
117
+ </button>
118
+ </div>
119
+ </nav>
120
+ </>
121
+ )
122
+ }
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Menu } from 'lucide-react'
6
+ import { Sidebar } from './Sidebar'
7
+ import { useState } from 'react'
8
+
9
+ export function MobileSidebar() {
10
+ const [open, setOpen] = useState(false)
11
+
12
+ return (
13
+ <Sheet open={open} onOpenChange={setOpen}>
14
+ <SheetTrigger asChild>
15
+ <Button variant="ghost" size="icon" className="md:hidden h-8 w-8">
16
+ <Menu className="w-5 h-5" />
17
+ </Button>
18
+ </SheetTrigger>
19
+ <SheetContent side="left" className="p-0 w-56">
20
+ <div onClick={(e) => {
21
+ // Close only when navigating (Link click), not for buttons/inputs
22
+ if ((e.target as HTMLElement).closest('a')) setOpen(false)
23
+ }}>
24
+ <Sidebar />
25
+ </div>
26
+ </SheetContent>
27
+ </Sheet>
28
+ )
29
+ }
@@ -0,0 +1,142 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname, useRouter } from 'next/navigation'
5
+ import { useTheme } from 'next-themes'
6
+ import { cn } from '@/lib/utils'
7
+ import { ENTITY_ROUTES } from '@/lib/routes'
8
+ import { useFavoritesStore } from '@/stores/favoritesStore'
9
+ import {
10
+ LayoutDashboard,
11
+ Target,
12
+ FolderKanban,
13
+ RotateCcw,
14
+ BookOpen,
15
+ BookMarked,
16
+ CalendarDays,
17
+ Layers,
18
+ Sun,
19
+ Moon,
20
+ Monitor,
21
+ CheckSquare,
22
+ StickyNote,
23
+ LogOut,
24
+ Star,
25
+ Mic,
26
+ } from 'lucide-react'
27
+
28
+ const SIDEBAR_ITEMS = [
29
+ { href: '/', label: '대시보드', icon: LayoutDashboard },
30
+ { href: '/areas', label: '영역', icon: Layers },
31
+ { href: '/goals', label: '목표', icon: Target },
32
+ { href: '/projects', label: '프로젝트', icon: FolderKanban },
33
+ { href: '/tasks', label: '할일', icon: CheckSquare },
34
+ { href: '/routines', label: '루틴', icon: RotateCcw },
35
+ { href: '/notes', label: '노트', icon: StickyNote },
36
+ { href: '/books', label: '독서', icon: BookMarked },
37
+ { href: '/calendar', label: '캘린더', icon: CalendarDays },
38
+ { href: '/review', label: '계획 & 회고', icon: BookOpen },
39
+ { href: '/voice', label: 'Eve 보이스', icon: Mic },
40
+ ]
41
+
42
+ const THEME_OPTIONS = [
43
+ { value: 'light', icon: Sun, label: 'Light' },
44
+ { value: 'dark', icon: Moon, label: 'Dark' },
45
+ { value: 'system', icon: Monitor, label: 'Auto' },
46
+ ] as const
47
+
48
+ export function Sidebar() {
49
+ const pathname = usePathname()
50
+ const router = useRouter()
51
+ const { theme, setTheme } = useTheme()
52
+ const favorites = useFavoritesStore(s => s.favorites)
53
+
54
+ const handleLogout = async () => {
55
+ await fetch('/api/auth/logout', { method: 'POST' })
56
+ router.push('/login')
57
+ }
58
+
59
+ return (
60
+ <aside className="w-56 h-full border-r border-border bg-background flex flex-col">
61
+ <div className="p-4 flex-1 overflow-y-auto">
62
+ <div className="font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground mb-3">
63
+ Navigation
64
+ </div>
65
+ <nav className="space-y-1">
66
+ {SIDEBAR_ITEMS.map(item => {
67
+ const Icon = item.icon
68
+ const isActive = pathname === item.href
69
+ return (
70
+ <Link
71
+ key={item.href}
72
+ href={item.href}
73
+ className={cn(
74
+ 'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
75
+ isActive
76
+ ? 'bg-card text-primary'
77
+ : 'text-muted-foreground hover:text-foreground hover:bg-card'
78
+ )}
79
+ >
80
+ <Icon className="w-4 h-4" />
81
+ {item.label}
82
+ </Link>
83
+ )
84
+ })}
85
+ </nav>
86
+
87
+ {favorites.length > 0 && (
88
+ <>
89
+ <div className="font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground mt-6 mb-3">
90
+ Favorites
91
+ </div>
92
+ <div className="space-y-1">
93
+ {favorites.map(fav => (
94
+ <Link
95
+ key={`${fav.type}-${fav.id}`}
96
+ href={`${ENTITY_ROUTES[fav.type] ?? ''}/${fav.id}`}
97
+ className="flex items-center gap-3 px-3 py-1.5 rounded-md text-sm text-muted-foreground hover:text-foreground hover:bg-card transition-colors"
98
+ >
99
+ <Star className="w-3.5 h-3.5 text-gatsaeng-amber fill-gatsaeng-amber" />
100
+ <span className="truncate">{fav.title}</span>
101
+ </Link>
102
+ ))}
103
+ </div>
104
+ </>
105
+ )}
106
+ </div>
107
+
108
+ <div className="p-4 border-t border-border space-y-3">
109
+ <div className="flex items-center gap-1 rounded-md bg-muted p-1">
110
+ {THEME_OPTIONS.map(opt => {
111
+ const Icon = opt.icon
112
+ return (
113
+ <button
114
+ key={opt.value}
115
+ onClick={() => setTheme(opt.value)}
116
+ className={cn(
117
+ 'flex-1 flex items-center justify-center gap-1 rounded-sm px-2 py-1 text-xs transition-colors',
118
+ theme === opt.value
119
+ ? 'bg-background text-foreground shadow-sm'
120
+ : 'text-muted-foreground hover:text-foreground'
121
+ )}
122
+ title={opt.label}
123
+ >
124
+ <Icon className="w-3.5 h-3.5" />
125
+ </button>
126
+ )
127
+ })}
128
+ </div>
129
+ <button
130
+ onClick={handleLogout}
131
+ className="flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-xs text-muted-foreground hover:text-gatsaeng-red hover:bg-gatsaeng-red/10 transition-colors"
132
+ >
133
+ <LogOut className="w-3.5 h-3.5" />
134
+ 로그아웃
135
+ </button>
136
+ <div className="text-xs text-muted-foreground">
137
+ 갓생 OS v0.1
138
+ </div>
139
+ </div>
140
+ </aside>
141
+ )
142
+ }
@@ -0,0 +1,229 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { Card, CardContent } from '@/components/ui/card'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Label } from '@/components/ui/label'
8
+ import { ChevronRight, ChevronLeft, Sparkles, Target, Brain, Clock, Rocket } from 'lucide-react'
9
+
10
+ interface OnboardingData {
11
+ display_name: string
12
+ identity: string
13
+ core_value: string
14
+ peak_hours: string
15
+ first_goal: string
16
+ }
17
+
18
+ const STEPS = [
19
+ { icon: Sparkles, title: '환영합니다!', subtitle: '갓생 OS에 오신 것을 환영합니다' },
20
+ { icon: Brain, title: '정체성', subtitle: '어떤 사람이 되고 싶으세요?' },
21
+ { icon: Target, title: '핵심 가치', subtitle: '당신의 핵심 가치는?' },
22
+ { icon: Clock, title: '피크 타임', subtitle: '가장 에너지가 높은 시간대는?' },
23
+ { icon: Rocket, title: '첫 목표', subtitle: '시작할 첫 번째 목표를 정해보세요' },
24
+ ]
25
+
26
+ const CORE_VALUES = ['성장', '자유', '관계', '건강', '재미', '안정', '창의']
27
+
28
+ const PEAK_HOURS = [
29
+ { label: '새벽형 (5-8시)', value: 'early' },
30
+ { label: '오전형 (9-12시)', value: 'morning' },
31
+ { label: '오후형 (13-17시)', value: 'afternoon' },
32
+ { label: '야간형 (22-2시)', value: 'night' },
33
+ ]
34
+
35
+ interface OnboardingFlowProps {
36
+ onComplete: (data: OnboardingData) => void
37
+ }
38
+
39
+ export function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
40
+ const [step, setStep] = useState(0)
41
+ const [data, setData] = useState<OnboardingData>({
42
+ display_name: '',
43
+ identity: '',
44
+ core_value: '',
45
+ peak_hours: '',
46
+ first_goal: '',
47
+ })
48
+
49
+ const update = (key: keyof OnboardingData, value: string) => {
50
+ setData(prev => ({ ...prev, [key]: value }))
51
+ }
52
+
53
+ const canProceed = () => {
54
+ switch (step) {
55
+ case 0: return data.display_name.trim().length > 0
56
+ case 1: return data.identity.trim().length > 0
57
+ case 2: return data.core_value.length > 0
58
+ case 3: return data.peak_hours.length > 0
59
+ case 4: return data.first_goal.trim().length > 0
60
+ default: return false
61
+ }
62
+ }
63
+
64
+ const StepIcon = STEPS[step].icon
65
+
66
+ return (
67
+ <div className="min-h-screen bg-background flex items-center justify-center p-4">
68
+ <div className="w-full max-w-lg">
69
+ {/* Progress */}
70
+ <div className="flex gap-1.5 mb-8">
71
+ {STEPS.map((_, i) => (
72
+ <div
73
+ key={i}
74
+ className={`h-1 flex-1 rounded-full transition-colors ${
75
+ i <= step ? 'bg-gatsaeng-amber' : 'bg-muted'
76
+ }`}
77
+ />
78
+ ))}
79
+ </div>
80
+
81
+ <Card className="border-border/50">
82
+ <CardContent className="py-8 px-6">
83
+ <div className="flex items-center gap-3 mb-6">
84
+ <div className="w-10 h-10 rounded-lg bg-gatsaeng-amber/10 flex items-center justify-center">
85
+ <StepIcon className="w-5 h-5 text-gatsaeng-amber" />
86
+ </div>
87
+ <div>
88
+ <h2 className="text-lg font-bold text-foreground">{STEPS[step].title}</h2>
89
+ <p className="text-xs text-muted-foreground">{STEPS[step].subtitle}</p>
90
+ </div>
91
+ </div>
92
+
93
+ {step === 0 && (
94
+ <div className="space-y-4">
95
+ <div>
96
+ <Label>이름 또는 닉네임</Label>
97
+ <Input
98
+ value={data.display_name}
99
+ onChange={e => update('display_name', e.target.value)}
100
+ placeholder="Drake"
101
+ className="mt-1"
102
+ autoFocus
103
+ />
104
+ </div>
105
+ <p className="text-xs text-muted-foreground">
106
+ 뇌과학 기반 습관 시스템으로 갓생을 시작합니다. 5단계 설정을 완료하면 대시보드가 열립니다.
107
+ </p>
108
+ </div>
109
+ )}
110
+
111
+ {step === 1 && (
112
+ <div className="space-y-4">
113
+ <div>
114
+ <Label>나는 _____ 사람이다</Label>
115
+ <Input
116
+ value={data.identity}
117
+ onChange={e => update('identity', e.target.value)}
118
+ placeholder="매일 성장하는"
119
+ className="mt-1"
120
+ autoFocus
121
+ />
122
+ </div>
123
+ <p className="text-xs text-muted-foreground">
124
+ Identity-based habits: 행동이 아닌 정체성에서 시작하면 습관이 오래 갑니다. (James Clear)
125
+ </p>
126
+ </div>
127
+ )}
128
+
129
+ {step === 2 && (
130
+ <div className="space-y-4">
131
+ <Label>당신의 핵심 가치를 선택하세요</Label>
132
+ <div className="grid grid-cols-4 gap-2">
133
+ {CORE_VALUES.map(value => (
134
+ <button
135
+ key={value}
136
+ onClick={() => update('core_value', value)}
137
+ className={`px-3 py-2.5 rounded-md text-sm font-medium transition-colors ${
138
+ data.core_value === value
139
+ ? 'bg-gatsaeng-amber text-black'
140
+ : 'bg-secondary text-muted-foreground hover:text-foreground'
141
+ }`}
142
+ >
143
+ {value}
144
+ </button>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ )}
149
+
150
+ {step === 3 && (
151
+ <div className="space-y-4">
152
+ <Label>가장 집중이 잘 되는 시간대는?</Label>
153
+ <div className="grid grid-cols-2 gap-2">
154
+ {PEAK_HOURS.map(ph => (
155
+ <button
156
+ key={ph.value}
157
+ onClick={() => update('peak_hours', ph.value)}
158
+ className={`px-3 py-3 rounded-md text-sm font-medium transition-colors ${
159
+ data.peak_hours === ph.value
160
+ ? 'bg-gatsaeng-purple text-white'
161
+ : 'bg-secondary text-muted-foreground hover:text-foreground'
162
+ }`}
163
+ >
164
+ {ph.label}
165
+ </button>
166
+ ))}
167
+ </div>
168
+ <p className="text-xs text-muted-foreground">
169
+ Ultradian rhythm: 에너지 패턴을 파악하면 최적의 작업 배치가 가능합니다.
170
+ </p>
171
+ </div>
172
+ )}
173
+
174
+ {step === 4 && (
175
+ <div className="space-y-4">
176
+ <div>
177
+ <Label>첫 목표를 입력하세요</Label>
178
+ <Input
179
+ value={data.first_goal}
180
+ onChange={e => update('first_goal', e.target.value)}
181
+ placeholder="영어 비즈니스 레벨 달성"
182
+ className="mt-1"
183
+ autoFocus
184
+ />
185
+ </div>
186
+ <p className="text-xs text-muted-foreground">
187
+ 대시보드에서 목표 진행률과 Why Statement를 확인할 수 있습니다.
188
+ </p>
189
+ </div>
190
+ )}
191
+
192
+ {/* Navigation */}
193
+ <div className="flex items-center justify-between mt-8">
194
+ {step > 0 ? (
195
+ <Button variant="ghost" size="sm" onClick={() => setStep(s => s - 1)}>
196
+ <ChevronLeft className="w-4 h-4 mr-1" /> 이전
197
+ </Button>
198
+ ) : <div />}
199
+
200
+ {step < STEPS.length - 1 ? (
201
+ <Button
202
+ size="sm"
203
+ disabled={!canProceed()}
204
+ onClick={() => setStep(s => s + 1)}
205
+ className="bg-gatsaeng-amber hover:bg-gatsaeng-amber/80 text-black"
206
+ >
207
+ 다음 <ChevronRight className="w-4 h-4 ml-1" />
208
+ </Button>
209
+ ) : (
210
+ <Button
211
+ size="sm"
212
+ disabled={!canProceed()}
213
+ onClick={() => onComplete(data)}
214
+ className="bg-gatsaeng-teal hover:bg-gatsaeng-teal/80 text-black"
215
+ >
216
+ <Rocket className="w-4 h-4 mr-1" /> 갓생 시작!
217
+ </Button>
218
+ )}
219
+ </div>
220
+ </CardContent>
221
+ </Card>
222
+
223
+ <p className="text-center text-[10px] text-muted-foreground mt-4">
224
+ Step {step + 1} of {STEPS.length}
225
+ </p>
226
+ </div>
227
+ </div>
228
+ )
229
+ }
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
4
+ import { OnboardingFlow } from './OnboardingFlow'
5
+ import type { Profile } from '@/types'
6
+
7
+ const PEAK_HOURS_MAP: Record<string, number[]> = {
8
+ early: [5, 6, 7, 8],
9
+ morning: [9, 10, 11, 12],
10
+ afternoon: [13, 14, 15, 16, 17],
11
+ night: [22, 23, 0, 1, 2],
12
+ }
13
+
14
+ export function OnboardingGate({ children }: { children: React.ReactNode }) {
15
+ const queryClient = useQueryClient()
16
+
17
+ const { data: profile, isLoading } = useQuery({
18
+ queryKey: ['profile'],
19
+ queryFn: async (): Promise<Profile | null> => {
20
+ const res = await fetch('/api/profile')
21
+ if (!res.ok) return null
22
+ return res.json()
23
+ },
24
+ })
25
+
26
+ const setupProfile = useMutation({
27
+ mutationFn: async (data: {
28
+ display_name: string
29
+ identity: string
30
+ core_value: string
31
+ peak_hours: string
32
+ first_goal: string
33
+ }) => {
34
+ // Update profile
35
+ await fetch('/api/profile', {
36
+ method: 'PUT',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({
39
+ display_name: data.display_name,
40
+ peak_hours: PEAK_HOURS_MAP[data.peak_hours] || [9, 10, 11],
41
+ }),
42
+ })
43
+
44
+ // Create first goal
45
+ if (data.first_goal) {
46
+ await fetch('/api/goals', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({
50
+ title: data.first_goal,
51
+ type: 'quarterly',
52
+ core_value: data.core_value,
53
+ identity_statement: `나는 ${data.identity} 사람이다`,
54
+ }),
55
+ })
56
+ }
57
+ },
58
+ onSuccess: () => {
59
+ queryClient.invalidateQueries({ queryKey: ['profile'] })
60
+ queryClient.invalidateQueries({ queryKey: ['goals'] })
61
+ },
62
+ })
63
+
64
+ if (isLoading) {
65
+ return (
66
+ <div className="min-h-screen bg-background flex items-center justify-center">
67
+ <div className="w-8 h-8 border-2 border-gatsaeng-amber border-t-transparent rounded-full animate-spin" />
68
+ </div>
69
+ )
70
+ }
71
+
72
+ // Show onboarding if profile has default name (never customized)
73
+ if (profile && profile.display_name === 'User') {
74
+ return <OnboardingFlow onComplete={(data) => setupProfile.mutate(data)} />
75
+ }
76
+
77
+ return <>{children}</>
78
+ }