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,152 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
7
+ import type { Task } from '@/types'
8
+
9
+ interface CalendarViewProps {
10
+ tasks: Task[]
11
+ onClickTask?: (task: Task) => void
12
+ }
13
+
14
+ function getDaysInMonth(year: number, month: number) {
15
+ return new Date(year, month + 1, 0).getDate()
16
+ }
17
+
18
+ function getFirstDayOfMonth(year: number, month: number) {
19
+ return new Date(year, month, 1).getDay()
20
+ }
21
+
22
+ const MONTH_NAMES = [
23
+ '1월', '2월', '3월', '4월', '5월', '6월',
24
+ '7월', '8월', '9월', '10월', '11월', '12월',
25
+ ]
26
+
27
+ const DAY_NAMES = ['일', '월', '화', '수', '목', '금', '토']
28
+
29
+ const STATUS_DOT: Record<string, string> = {
30
+ backlog: 'bg-muted-foreground',
31
+ todo: 'bg-primary',
32
+ doing: 'bg-gatsaeng-amber',
33
+ done: 'bg-gatsaeng-teal',
34
+ }
35
+
36
+ export function CalendarView({ tasks, onClickTask }: CalendarViewProps) {
37
+ const today = new Date()
38
+ const [year, setYear] = useState(today.getFullYear())
39
+ const [month, setMonth] = useState(today.getMonth())
40
+
41
+ const prev = () => {
42
+ if (month === 0) { setYear(y => y - 1); setMonth(11) }
43
+ else setMonth(m => m - 1)
44
+ }
45
+ const next = () => {
46
+ if (month === 11) { setYear(y => y + 1); setMonth(0) }
47
+ else setMonth(m => m + 1)
48
+ }
49
+
50
+ const daysInMonth = getDaysInMonth(year, month)
51
+ const firstDay = getFirstDayOfMonth(year, month)
52
+
53
+ const tasksByDate = useMemo(() => {
54
+ const map: Record<string, Task[]> = {}
55
+ tasks.forEach(task => {
56
+ if (task.due_date) {
57
+ const dateStr = task.due_date.slice(0, 10)
58
+ if (!map[dateStr]) map[dateStr] = []
59
+ map[dateStr].push(task)
60
+ }
61
+ })
62
+ return map
63
+ }, [tasks])
64
+
65
+ const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
66
+
67
+ return (
68
+ <div>
69
+ <div className="flex items-center justify-between mb-4">
70
+ <Button variant="ghost" size="icon" onClick={prev} className="h-8 w-8">
71
+ <ChevronLeft className="w-4 h-4" />
72
+ </Button>
73
+ <span className="text-sm font-medium">
74
+ {year}년 {MONTH_NAMES[month]}
75
+ </span>
76
+ <Button variant="ghost" size="icon" onClick={next} className="h-8 w-8">
77
+ <ChevronRight className="w-4 h-4" />
78
+ </Button>
79
+ </div>
80
+
81
+ <div className="grid grid-cols-7 gap-px bg-border rounded-lg overflow-hidden">
82
+ {DAY_NAMES.map(day => (
83
+ <div key={day} className="bg-secondary/30 px-2 py-1.5 text-center text-xs font-medium text-muted-foreground">
84
+ {day}
85
+ </div>
86
+ ))}
87
+
88
+ {Array.from({ length: firstDay }).map((_, i) => (
89
+ <div key={`empty-${i}`} className="bg-card/50 min-h-[80px]" />
90
+ ))}
91
+
92
+ {Array.from({ length: daysInMonth }).map((_, i) => {
93
+ const day = i + 1
94
+ const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
95
+ const dayTasks = tasksByDate[dateStr] || []
96
+ const isToday = dateStr === todayStr
97
+
98
+ return (
99
+ <div
100
+ key={day}
101
+ className={`bg-card min-h-[80px] p-1.5 ${
102
+ isToday ? 'ring-1 ring-primary ring-inset' : ''
103
+ }`}
104
+ >
105
+ <div className={`text-xs mb-1 ${
106
+ isToday ? 'text-primary font-bold' : 'text-muted-foreground'
107
+ }`}>
108
+ {day}
109
+ </div>
110
+ <div className="space-y-0.5">
111
+ {dayTasks.slice(0, 3).map(task => (
112
+ <button
113
+ key={task.id}
114
+ onClick={() => onClickTask?.(task)}
115
+ className="w-full text-left flex items-center gap-1 px-1 py-0.5 rounded text-[10px] hover:bg-secondary/50 transition-colors truncate"
116
+ >
117
+ <div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${STATUS_DOT[task.status]}`} />
118
+ <span className="truncate">{task.title}</span>
119
+ </button>
120
+ ))}
121
+ {dayTasks.length > 3 && (
122
+ <div className="text-[10px] text-muted-foreground px-1">
123
+ +{dayTasks.length - 3}
124
+ </div>
125
+ )}
126
+ </div>
127
+ </div>
128
+ )
129
+ })}
130
+ </div>
131
+
132
+ {/* Tasks without due dates */}
133
+ {tasks.filter(t => !t.due_date).length > 0 && (
134
+ <div className="mt-4">
135
+ <div className="text-xs text-muted-foreground mb-2">마감일 미설정</div>
136
+ <div className="flex flex-wrap gap-1">
137
+ {tasks.filter(t => !t.due_date).map(task => (
138
+ <Badge
139
+ key={task.id}
140
+ variant="outline"
141
+ className="text-[10px] cursor-pointer hover:bg-secondary/50"
142
+ onClick={() => onClickTask?.(task)}
143
+ >
144
+ {task.title}
145
+ </Badge>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ )}
150
+ </div>
151
+ )
152
+ }
@@ -0,0 +1,180 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import {
5
+ DndContext,
6
+ DragOverlay,
7
+ closestCorners,
8
+ KeyboardSensor,
9
+ PointerSensor,
10
+ useSensor,
11
+ useSensors,
12
+ type DragStartEvent,
13
+ type DragEndEvent,
14
+ type DragOverEvent,
15
+ } from '@dnd-kit/core'
16
+ import {
17
+ SortableContext,
18
+ verticalListSortingStrategy,
19
+ useSortable,
20
+ } from '@dnd-kit/sortable'
21
+ import { useDroppable } from '@dnd-kit/core'
22
+ import { CSS } from '@dnd-kit/utilities'
23
+ import { Badge } from '@/components/ui/badge'
24
+ import { TaskCard } from './TaskCard'
25
+ import type { Task, TaskStatus } from '@/types'
26
+
27
+ const COLUMNS: { key: TaskStatus; title: string; color: string }[] = [
28
+ { key: 'backlog', title: 'Backlog', color: '#8b949e' },
29
+ { key: 'todo', title: 'To Do', color: '#58a6ff' },
30
+ { key: 'doing', title: 'Doing', color: '#f5a623' },
31
+ { key: 'done', title: 'Done', color: '#00d4aa' },
32
+ ]
33
+
34
+ interface KanbanViewProps {
35
+ tasks: Task[]
36
+ onUpdateTask: (data: { id: string; status: TaskStatus; position: number }) => void
37
+ }
38
+
39
+ function SortableTaskCard({ task }: { task: Task }) {
40
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
41
+ id: task.id,
42
+ data: { task },
43
+ })
44
+
45
+ const style = {
46
+ transform: CSS.Transform.toString(transform),
47
+ transition,
48
+ }
49
+
50
+ return (
51
+ <TaskCard
52
+ ref={setNodeRef}
53
+ task={task}
54
+ isDragging={isDragging}
55
+ dragHandleProps={{ ...attributes, ...listeners }}
56
+ style={style}
57
+ />
58
+ )
59
+ }
60
+
61
+ function KanbanColumn({ column, tasks }: { column: typeof COLUMNS[number]; tasks: Task[] }) {
62
+ const { setNodeRef, isOver } = useDroppable({ id: column.key })
63
+
64
+ return (
65
+ <div
66
+ ref={setNodeRef}
67
+ className={`flex-1 min-w-[240px] rounded-lg p-2 transition-colors ${
68
+ isOver ? 'bg-primary/5 ring-1 ring-primary/20' : ''
69
+ }`}
70
+ >
71
+ <div className="flex items-center gap-2 mb-3 px-1">
72
+ <div className="w-2 h-2 rounded-full" style={{ backgroundColor: column.color }} />
73
+ <span className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
74
+ {column.title}
75
+ </span>
76
+ <Badge variant="outline" className="text-xs ml-auto">{tasks.length}</Badge>
77
+ </div>
78
+ <SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
79
+ <div className="space-y-2 min-h-[100px]">
80
+ {tasks.map(task => (
81
+ <SortableTaskCard key={task.id} task={task} />
82
+ ))}
83
+ </div>
84
+ </SortableContext>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ export function KanbanView({ tasks, onUpdateTask }: KanbanViewProps) {
90
+ const [activeTask, setActiveTask] = useState<Task | null>(null)
91
+
92
+ const sensors = useSensors(
93
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
94
+ useSensor(KeyboardSensor)
95
+ )
96
+
97
+ const tasksByColumn = COLUMNS.reduce(
98
+ (acc, col) => {
99
+ acc[col.key] = tasks.filter(t => t.status === col.key).sort((a, b) => a.position - b.position)
100
+ return acc
101
+ },
102
+ {} as Record<TaskStatus, Task[]>
103
+ )
104
+
105
+ function handleDragStart(event: DragStartEvent) {
106
+ const task = tasks.find(t => t.id === event.active.id)
107
+ if (task) setActiveTask(task)
108
+ }
109
+
110
+ function findColumn(id: string): TaskStatus | undefined {
111
+ // Check if the id is a column key
112
+ if (COLUMNS.some(c => c.key === id)) return id as TaskStatus
113
+ // Otherwise find which column contains this task
114
+ const task = tasks.find(t => t.id === id)
115
+ return task?.status
116
+ }
117
+
118
+ function handleDragOver(event: DragOverEvent) {
119
+ const { active, over } = event
120
+ if (!over) return
121
+
122
+ const activeColumn = findColumn(active.id as string)
123
+ const overColumn = findColumn(over.id as string)
124
+
125
+ if (!activeColumn || !overColumn || activeColumn === overColumn) return
126
+
127
+ // Moving to a new column
128
+ const task = tasks.find(t => t.id === active.id)
129
+ if (task) {
130
+ const targetTasks = tasksByColumn[overColumn]
131
+ onUpdateTask({
132
+ id: task.id,
133
+ status: overColumn,
134
+ position: targetTasks.length,
135
+ })
136
+ }
137
+ }
138
+
139
+ function handleDragEnd(event: DragEndEvent) {
140
+ setActiveTask(null)
141
+ const { active, over } = event
142
+ if (!over) return
143
+
144
+ const overColumn = findColumn(over.id as string)
145
+ if (!overColumn) return
146
+
147
+ const task = tasks.find(t => t.id === active.id)
148
+ if (!task) return
149
+
150
+ // Calculate new position
151
+ const columnTasks = tasksByColumn[overColumn].filter(t => t.id !== task.id)
152
+ const overIndex = columnTasks.findIndex(t => t.id === over.id)
153
+ const newPosition = overIndex >= 0 ? overIndex : columnTasks.length
154
+
155
+ onUpdateTask({
156
+ id: task.id,
157
+ status: overColumn,
158
+ position: newPosition,
159
+ })
160
+ }
161
+
162
+ return (
163
+ <DndContext
164
+ sensors={sensors}
165
+ collisionDetection={closestCorners}
166
+ onDragStart={handleDragStart}
167
+ onDragOver={handleDragOver}
168
+ onDragEnd={handleDragEnd}
169
+ >
170
+ <div className="flex gap-4 overflow-x-auto pb-4">
171
+ {COLUMNS.map(col => (
172
+ <KanbanColumn key={col.key} column={col} tasks={tasksByColumn[col.key]} />
173
+ ))}
174
+ </div>
175
+ <DragOverlay>
176
+ {activeTask && <TaskCard task={activeTask} isDragging />}
177
+ </DragOverlay>
178
+ </DndContext>
179
+ )
180
+ }
@@ -0,0 +1,82 @@
1
+ 'use client'
2
+
3
+ import { Checkbox } from '@/components/ui/checkbox'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import type { Task, TaskStatus } from '@/types'
6
+
7
+ const STATUS_ICONS: Record<string, string> = {
8
+ backlog: '○',
9
+ todo: '◐',
10
+ doing: '◑',
11
+ done: '●',
12
+ }
13
+
14
+ interface ListViewProps {
15
+ tasks: Task[]
16
+ onUpdateTask: (data: { id: string; status: TaskStatus }) => void
17
+ }
18
+
19
+ export function ListView({ tasks, onUpdateTask }: ListViewProps) {
20
+ const grouped = {
21
+ doing: tasks.filter(t => t.status === 'doing'),
22
+ todo: tasks.filter(t => t.status === 'todo'),
23
+ backlog: tasks.filter(t => t.status === 'backlog'),
24
+ done: tasks.filter(t => t.status === 'done'),
25
+ }
26
+
27
+ const sections = [
28
+ { key: 'doing', title: '진행중', tasks: grouped.doing },
29
+ { key: 'todo', title: '할 일', tasks: grouped.todo },
30
+ { key: 'backlog', title: '백로그', tasks: grouped.backlog },
31
+ { key: 'done', title: '완료', tasks: grouped.done },
32
+ ]
33
+
34
+ return (
35
+ <div className="space-y-6">
36
+ {sections.map(section => (
37
+ section.tasks.length > 0 && (
38
+ <div key={section.key}>
39
+ <div className="flex items-center gap-2 mb-2">
40
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
41
+ {section.title}
42
+ </span>
43
+ <Badge variant="outline" className="text-xs">{section.tasks.length}</Badge>
44
+ </div>
45
+ <div className="space-y-1">
46
+ {section.tasks.map(task => (
47
+ <div
48
+ key={task.id}
49
+ className="flex items-center gap-3 py-2 px-3 rounded-md hover:bg-secondary/30 transition-colors group"
50
+ >
51
+ <Checkbox
52
+ checked={task.status === 'done'}
53
+ onCheckedChange={(checked) => {
54
+ onUpdateTask({
55
+ id: task.id,
56
+ status: checked ? 'done' : 'todo',
57
+ })
58
+ }}
59
+ />
60
+ <span className={`text-sm flex-1 ${task.status === 'done' ? 'line-through text-muted-foreground' : ''}`}>
61
+ {task.title}
62
+ </span>
63
+ <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
64
+ <Badge variant="outline" className="text-[10px]">{task.priority}</Badge>
65
+ {task.due_date && (
66
+ <span className="text-[10px] text-muted-foreground">{task.due_date.slice(0, 10)}</span>
67
+ )}
68
+ </div>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ )
74
+ ))}
75
+ {tasks.length === 0 && (
76
+ <div className="text-center text-muted-foreground py-8 text-sm">
77
+ 아직 태스크가 없습니다
78
+ </div>
79
+ )}
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,206 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
7
+ import { ArrowUpDown, Search } from 'lucide-react'
8
+ import type { Task, TaskStatus, TaskPriority } from '@/types'
9
+
10
+ const STATUS_COLORS: Record<string, string> = {
11
+ backlog: 'text-muted-foreground',
12
+ todo: 'text-primary',
13
+ doing: 'text-gatsaeng-amber',
14
+ done: 'text-gatsaeng-teal',
15
+ }
16
+
17
+ const PRIORITY_ORDER: Record<string, number> = {
18
+ urgent: 0,
19
+ high: 1,
20
+ medium: 2,
21
+ low: 3,
22
+ }
23
+
24
+ type SortKey = 'title' | 'status' | 'priority' | 'due_date'
25
+ type SortDir = 'asc' | 'desc'
26
+
27
+ interface TableViewProps {
28
+ tasks: Task[]
29
+ onClickTask?: (task: Task) => void
30
+ onUpdateTask: (data: { id: string; status: TaskStatus }) => void
31
+ }
32
+
33
+ export function TableView({ tasks, onClickTask, onUpdateTask }: TableViewProps) {
34
+ const [search, setSearch] = useState('')
35
+ const [statusFilter, setStatusFilter] = useState<string>('all')
36
+ const [priorityFilter, setPriorityFilter] = useState<string>('all')
37
+ const [sortKey, setSortKey] = useState<SortKey>('priority')
38
+ const [sortDir, setSortDir] = useState<SortDir>('asc')
39
+
40
+ const toggleSort = (key: SortKey) => {
41
+ if (sortKey === key) {
42
+ setSortDir(d => d === 'asc' ? 'desc' : 'asc')
43
+ } else {
44
+ setSortKey(key)
45
+ setSortDir('asc')
46
+ }
47
+ }
48
+
49
+ const filtered = useMemo(() => {
50
+ let result = tasks
51
+
52
+ if (search) {
53
+ const q = search.toLowerCase()
54
+ result = result.filter(t => t.title.toLowerCase().includes(q) || t.tag?.toLowerCase().includes(q))
55
+ }
56
+ if (statusFilter !== 'all') {
57
+ result = result.filter(t => t.status === statusFilter)
58
+ }
59
+ if (priorityFilter !== 'all') {
60
+ result = result.filter(t => t.priority === priorityFilter)
61
+ }
62
+
63
+ result = [...result].sort((a, b) => {
64
+ const dir = sortDir === 'asc' ? 1 : -1
65
+ switch (sortKey) {
66
+ case 'title':
67
+ return dir * a.title.localeCompare(b.title)
68
+ case 'status': {
69
+ const order = ['backlog', 'todo', 'doing', 'done']
70
+ return dir * (order.indexOf(a.status) - order.indexOf(b.status))
71
+ }
72
+ case 'priority':
73
+ return dir * (PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority])
74
+ case 'due_date':
75
+ return dir * ((a.due_date ?? '').localeCompare(b.due_date ?? ''))
76
+ default:
77
+ return 0
78
+ }
79
+ })
80
+
81
+ return result
82
+ }, [tasks, search, statusFilter, priorityFilter, sortKey, sortDir])
83
+
84
+ const SortHeader = ({ label, sortKeyName }: { label: string; sortKeyName: SortKey }) => (
85
+ <button
86
+ onClick={() => toggleSort(sortKeyName)}
87
+ className="flex items-center gap-1 text-xs font-medium text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
88
+ >
89
+ {label}
90
+ <ArrowUpDown className={`w-3 h-3 ${sortKey === sortKeyName ? 'text-primary' : ''}`} />
91
+ </button>
92
+ )
93
+
94
+ return (
95
+ <div>
96
+ <div className="flex gap-2 mb-4">
97
+ <div className="relative flex-1 max-w-xs">
98
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
99
+ <Input
100
+ value={search}
101
+ onChange={e => setSearch(e.target.value)}
102
+ placeholder="검색..."
103
+ className="pl-8 h-8 text-sm"
104
+ />
105
+ </div>
106
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
107
+ <SelectTrigger className="w-28 h-8 text-xs"><SelectValue /></SelectTrigger>
108
+ <SelectContent>
109
+ <SelectItem value="all">전체 상태</SelectItem>
110
+ <SelectItem value="backlog">Backlog</SelectItem>
111
+ <SelectItem value="todo">To Do</SelectItem>
112
+ <SelectItem value="doing">Doing</SelectItem>
113
+ <SelectItem value="done">Done</SelectItem>
114
+ </SelectContent>
115
+ </Select>
116
+ <Select value={priorityFilter} onValueChange={setPriorityFilter}>
117
+ <SelectTrigger className="w-28 h-8 text-xs"><SelectValue /></SelectTrigger>
118
+ <SelectContent>
119
+ <SelectItem value="all">전체 우선</SelectItem>
120
+ <SelectItem value="urgent">긴급</SelectItem>
121
+ <SelectItem value="high">높음</SelectItem>
122
+ <SelectItem value="medium">보통</SelectItem>
123
+ <SelectItem value="low">낮음</SelectItem>
124
+ </SelectContent>
125
+ </Select>
126
+ </div>
127
+
128
+ <div className="border border-border rounded-lg overflow-hidden">
129
+ <table className="w-full">
130
+ <thead>
131
+ <tr className="border-b border-border bg-secondary/30">
132
+ <th className="text-left px-4 py-2.5">
133
+ <SortHeader label="제목" sortKeyName="title" />
134
+ </th>
135
+ <th className="text-left px-4 py-2.5 w-24">
136
+ <SortHeader label="상태" sortKeyName="status" />
137
+ </th>
138
+ <th className="text-left px-4 py-2.5 w-24">
139
+ <SortHeader label="우선순위" sortKeyName="priority" />
140
+ </th>
141
+ <th className="text-left px-4 py-2.5 w-20">
142
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">에너지</span>
143
+ </th>
144
+ <th className="text-left px-4 py-2.5 w-28">
145
+ <SortHeader label="마감일" sortKeyName="due_date" />
146
+ </th>
147
+ </tr>
148
+ </thead>
149
+ <tbody>
150
+ {filtered.length === 0 ? (
151
+ <tr>
152
+ <td colSpan={5} className="px-4 py-8 text-center text-muted-foreground text-sm">
153
+ {search || statusFilter !== 'all' || priorityFilter !== 'all'
154
+ ? '필터에 맞는 태스크가 없습니다'
155
+ : '아직 태스크가 없습니다'}
156
+ </td>
157
+ </tr>
158
+ ) : (
159
+ filtered.map(task => (
160
+ <tr
161
+ key={task.id}
162
+ className="border-b border-border/50 last:border-0 hover:bg-secondary/20 transition-colors cursor-pointer"
163
+ onClick={() => onClickTask?.(task)}
164
+ >
165
+ <td className="px-4 py-2.5">
166
+ <div className="text-sm">{task.title}</div>
167
+ {task.tag && (
168
+ <Badge variant="outline" className="text-[10px] mt-1">{task.tag}</Badge>
169
+ )}
170
+ </td>
171
+ <td className="px-4 py-2.5">
172
+ <Select
173
+ value={task.status}
174
+ onValueChange={(v) => {
175
+ onUpdateTask({ id: task.id, status: v as TaskStatus })
176
+ }}
177
+ >
178
+ <SelectTrigger className={`h-7 text-xs w-20 border-0 bg-transparent ${STATUS_COLORS[task.status]}`}>
179
+ <SelectValue />
180
+ </SelectTrigger>
181
+ <SelectContent>
182
+ <SelectItem value="backlog">Backlog</SelectItem>
183
+ <SelectItem value="todo">To Do</SelectItem>
184
+ <SelectItem value="doing">Doing</SelectItem>
185
+ <SelectItem value="done">Done</SelectItem>
186
+ </SelectContent>
187
+ </Select>
188
+ </td>
189
+ <td className="px-4 py-2.5">
190
+ <Badge variant="outline" className="text-[10px]">{task.priority}</Badge>
191
+ </td>
192
+ <td className="px-4 py-2.5 text-xs text-muted-foreground">
193
+ {task.energy_required ?? '—'}
194
+ </td>
195
+ <td className="px-4 py-2.5 text-xs text-muted-foreground">
196
+ {task.due_date?.slice(0, 10) ?? '—'}
197
+ </td>
198
+ </tr>
199
+ ))
200
+ )}
201
+ </tbody>
202
+ </table>
203
+ </div>
204
+ </div>
205
+ )
206
+ }