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,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
|
+
}
|