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.
- package/README.md +77 -0
- package/bin/arete.js +156 -0
- package/bin/create.js +111 -0
- package/lib/install-openclaw.js +50 -0
- package/lib/scaffold.js +213 -0
- package/lib/setup-wizard.js +88 -0
- package/lib/updater.js +130 -0
- package/package.json +34 -0
- package/packages/gatsaeng-os/README.md +36 -0
- package/packages/gatsaeng-os/components.json +23 -0
- package/packages/gatsaeng-os/eslint.config.mjs +18 -0
- package/packages/gatsaeng-os/next.config.ts +7 -0
- package/packages/gatsaeng-os/package.json +59 -0
- package/packages/gatsaeng-os/postcss.config.mjs +7 -0
- package/packages/gatsaeng-os/public/file.svg +1 -0
- package/packages/gatsaeng-os/public/globe.svg +1 -0
- package/packages/gatsaeng-os/public/next.svg +1 -0
- package/packages/gatsaeng-os/public/vercel.svg +1 -0
- package/packages/gatsaeng-os/public/window.svg +1 -0
- package/packages/gatsaeng-os/python/api_server.py +248 -0
- package/packages/gatsaeng-os/python/briefing.py +145 -0
- package/packages/gatsaeng-os/python/config.py +55 -0
- package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
- package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
- package/packages/gatsaeng-os/python/proactive.py +158 -0
- package/packages/gatsaeng-os/python/requirements.txt +11 -0
- package/packages/gatsaeng-os/python/run.py +28 -0
- package/packages/gatsaeng-os/python/scoring.py +44 -0
- package/packages/gatsaeng-os/python/streak.py +70 -0
- package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
- package/packages/gatsaeng-os/python/timing_engine.py +117 -0
- package/packages/gatsaeng-os/python/vault_io.py +423 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
- package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
- package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
- package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
- package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
- package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
- package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
- package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
- package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
- package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
- package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
- package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
- package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
- package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
- package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
- package/packages/gatsaeng-os/src/app/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
- package/packages/gatsaeng-os/src/app/globals.css +208 -0
- package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
- package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
- package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
- package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
- package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
- package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
- package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
- package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
- package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
- package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
- package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
- package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
- package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
- package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
- package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
- package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
- package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
- package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
- package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
- package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
- package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
- package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
- package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
- package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
- package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
- package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
- package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
- package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
- package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
- package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
- package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
- package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
- package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
- package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
- package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
- package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
- package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
- package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
- package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
- package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
- package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
- package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
- package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
- package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
- package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
- package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
- package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
- package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
- package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
- package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
- package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
- package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
- package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
- package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
- package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
- package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
- package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
- package/packages/gatsaeng-os/src/lib/date.ts +7 -0
- package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
- package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
- package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
- package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
- package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
- package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
- package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
- package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
- package/packages/gatsaeng-os/src/middleware.ts +34 -0
- package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
- package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
- package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
- package/packages/gatsaeng-os/src/types/index.ts +320 -0
- package/packages/gatsaeng-os/tsconfig.json +34 -0
- package/templates/scripts/forge_qa.sh.tmpl +237 -0
- package/templates/scripts/forge_ship.sh.tmpl +183 -0
- package/templates/scripts/session_indexer.py.tmpl +420 -0
- package/templates/scripts/tracer.py.tmpl +266 -0
- package/templates/workspace/AGENTS.md.tmpl +190 -0
- package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
- package/templates/workspace/MEMORY.md.tmpl +35 -0
- package/templates/workspace/SOUL.md.tmpl +258 -0
- package/templates/workspace/TOOLS.md.tmpl +28 -0
- 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
|
+
}
|