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,475 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { Card, CardContent } from '@/components/ui/card'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Label } from '@/components/ui/label'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
10
|
+
import { BookOpen, ChevronRight, Star, Calendar, Sun, CalendarDays, ArrowLeft, Pencil, Trash2 } from 'lucide-react'
|
|
11
|
+
import { cn } from '@/lib/utils'
|
|
12
|
+
import type { Review } from '@/types'
|
|
13
|
+
|
|
14
|
+
const WEEKLY_PROMPTS = [
|
|
15
|
+
{ key: 'accomplished', label: '이번 주 가장 잘한 것', placeholder: '구체적인 성취를 적어주세요', icon: '🏆' },
|
|
16
|
+
{ key: 'struggled', label: '어디서 막혔는가', placeholder: '어떤 상황에서 어려움을 느꼈나요?', icon: '🧱' },
|
|
17
|
+
{ key: 'learnings', label: '배운 점', placeholder: '이번 주 가장 큰 깨달음은?', icon: '💡' },
|
|
18
|
+
{ key: 'next_week_focus', label: '다음 주 포커스', placeholder: '다음 주에 집중할 한 가지는?', icon: '🎯' },
|
|
19
|
+
{ key: 'energy_pattern', label: '에너지 패턴', placeholder: '언제 에너지가 높았고, 언제 낮았나요?', icon: '⚡' },
|
|
20
|
+
{ key: 'habit_insight', label: '루틴 인사이트', placeholder: '습관 유지에 도움이 된 것 / 방해한 것은?', icon: '🔄' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const DAILY_PROMPTS = [
|
|
24
|
+
{ key: 'accomplished', label: '오늘 가장 잘한 것', placeholder: '오늘의 작은 성취를 적어주세요', icon: '🏆' },
|
|
25
|
+
{ key: 'struggled', label: '어려웠던 점', placeholder: '오늘 어떤 부분이 어려웠나요?', icon: '🧱' },
|
|
26
|
+
{ key: 'learnings', label: '오늘의 깨달음', placeholder: '오늘 배운 것은?', icon: '💡' },
|
|
27
|
+
{ key: 'next_week_focus', label: '내일 포커스', placeholder: '내일 가장 중요한 한 가지는?', icon: '🎯' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const MONTHLY_PROMPTS = [
|
|
31
|
+
{ key: 'accomplished', label: '이번 달 주요 성취', placeholder: '이번 달 가장 의미 있는 성취는?', icon: '🏆' },
|
|
32
|
+
{ key: 'struggled', label: '반복된 어려움', placeholder: '이번 달 반복적으로 막힌 부분은?', icon: '🧱' },
|
|
33
|
+
{ key: 'learnings', label: '핵심 교훈', placeholder: '이번 달의 핵심 교훈은?', icon: '💡' },
|
|
34
|
+
{ key: 'next_week_focus', label: '다음 달 방향', placeholder: '다음 달 전략과 집중 방향은?', icon: '🎯' },
|
|
35
|
+
{ key: 'energy_pattern', label: '에너지/생산성 패턴', placeholder: '이번 달 에너지와 생산성 패턴 분석', icon: '⚡' },
|
|
36
|
+
{ key: 'habit_insight', label: '습관/루틴 점검', placeholder: '유지할 습관과 바꿀 습관은?', icon: '🔄' },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
type ReviewTab = 'daily' | 'weekly' | 'monthly'
|
|
40
|
+
const PROMPTS_MAP: Record<ReviewTab, typeof WEEKLY_PROMPTS> = {
|
|
41
|
+
daily: DAILY_PROMPTS,
|
|
42
|
+
weekly: WEEKLY_PROMPTS,
|
|
43
|
+
monthly: MONTHLY_PROMPTS,
|
|
44
|
+
}
|
|
45
|
+
const TAB_LABELS: Record<ReviewTab, string> = { daily: '일간 리뷰', weekly: '주간 리뷰', monthly: '월간 리뷰' }
|
|
46
|
+
|
|
47
|
+
function getWeekStart(date: Date = new Date()) {
|
|
48
|
+
const d = new Date(date)
|
|
49
|
+
const day = d.getDay()
|
|
50
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
|
|
51
|
+
d.setDate(diff)
|
|
52
|
+
return d.toISOString().slice(0, 10)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function ReviewPage() {
|
|
56
|
+
const queryClient = useQueryClient()
|
|
57
|
+
const [mode, setMode] = useState<'list' | 'write' | 'view'>('list')
|
|
58
|
+
const [reviewTab, setReviewTab] = useState<ReviewTab>('weekly')
|
|
59
|
+
const [formData, setFormData] = useState<Record<string, string>>({})
|
|
60
|
+
const [mood, setMood] = useState(3)
|
|
61
|
+
const [score, setScore] = useState(7)
|
|
62
|
+
const [editingReview, setEditingReview] = useState<Review | null>(null)
|
|
63
|
+
const [viewingReview, setViewingReview] = useState<Review | null>(null)
|
|
64
|
+
|
|
65
|
+
const { data: reviews } = useQuery({
|
|
66
|
+
queryKey: ['reviews'],
|
|
67
|
+
queryFn: async (): Promise<Review[]> => {
|
|
68
|
+
const res = await fetch('/api/reviews')
|
|
69
|
+
return res.json()
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const createReview = useMutation({
|
|
74
|
+
mutationFn: async (data: Record<string, unknown>) => {
|
|
75
|
+
const res = await fetch('/api/reviews', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify(data),
|
|
79
|
+
})
|
|
80
|
+
return res.json()
|
|
81
|
+
},
|
|
82
|
+
onSuccess: () => {
|
|
83
|
+
queryClient.invalidateQueries({ queryKey: ['reviews'] })
|
|
84
|
+
setMode('list')
|
|
85
|
+
setFormData({})
|
|
86
|
+
setEditingReview(null)
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const updateReview = useMutation({
|
|
91
|
+
mutationFn: async ({ id, ...data }: Record<string, unknown> & { id: string }) => {
|
|
92
|
+
const res = await fetch(`/api/reviews/${id}`, {
|
|
93
|
+
method: 'PUT',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify(data),
|
|
96
|
+
})
|
|
97
|
+
return res.json()
|
|
98
|
+
},
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
queryClient.invalidateQueries({ queryKey: ['reviews'] })
|
|
101
|
+
setMode('list')
|
|
102
|
+
setFormData({})
|
|
103
|
+
setEditingReview(null)
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const deleteReview = useMutation({
|
|
108
|
+
mutationFn: async (id: string) => {
|
|
109
|
+
await fetch(`/api/reviews/${id}`, { method: 'DELETE' })
|
|
110
|
+
},
|
|
111
|
+
onSuccess: () => {
|
|
112
|
+
queryClient.invalidateQueries({ queryKey: ['reviews'] })
|
|
113
|
+
setMode('list')
|
|
114
|
+
setViewingReview(null)
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const handleSubmit = () => {
|
|
119
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
120
|
+
const base: Record<string, unknown> = { mood, score, ...formData }
|
|
121
|
+
|
|
122
|
+
if (editingReview?.id) {
|
|
123
|
+
// Update existing
|
|
124
|
+
const tab = (editingReview.type as ReviewTab) || 'weekly'
|
|
125
|
+
if (tab === 'daily') {
|
|
126
|
+
base.type = 'daily'
|
|
127
|
+
base.date = editingReview.date || today
|
|
128
|
+
} else if (tab === 'monthly') {
|
|
129
|
+
base.type = 'monthly'
|
|
130
|
+
base.week_start = editingReview.week_start || today.slice(0, 7)
|
|
131
|
+
} else {
|
|
132
|
+
base.type = 'weekly'
|
|
133
|
+
base.week_start = editingReview.week_start || getWeekStart()
|
|
134
|
+
}
|
|
135
|
+
updateReview.mutate({ id: editingReview.id, ...base })
|
|
136
|
+
} else {
|
|
137
|
+
// Create new
|
|
138
|
+
if (reviewTab === 'daily') {
|
|
139
|
+
base.type = 'daily'
|
|
140
|
+
base.date = today
|
|
141
|
+
} else if (reviewTab === 'monthly') {
|
|
142
|
+
base.type = 'monthly'
|
|
143
|
+
base.week_start = today.slice(0, 7)
|
|
144
|
+
} else {
|
|
145
|
+
base.type = 'weekly'
|
|
146
|
+
base.week_start = getWeekStart()
|
|
147
|
+
}
|
|
148
|
+
createReview.mutate(base)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const handleEdit = (review: Review) => {
|
|
153
|
+
setEditingReview(review)
|
|
154
|
+
const tab = (review.type as ReviewTab) || 'weekly'
|
|
155
|
+
setReviewTab(tab)
|
|
156
|
+
setFormData({
|
|
157
|
+
accomplished: review.accomplished ?? '',
|
|
158
|
+
struggled: review.struggled ?? '',
|
|
159
|
+
learnings: review.learnings ?? '',
|
|
160
|
+
next_week_focus: review.next_week_focus ?? '',
|
|
161
|
+
energy_pattern: review.energy_pattern ?? '',
|
|
162
|
+
habit_insight: review.habit_insight ?? '',
|
|
163
|
+
})
|
|
164
|
+
setMood(review.mood ?? 3)
|
|
165
|
+
setScore(review.score ?? 7)
|
|
166
|
+
setMode('write')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleDelete = (id: string) => {
|
|
170
|
+
if (!confirm('이 회고를 삭제하시겠습니까?')) return
|
|
171
|
+
deleteReview.mutate(id)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleView = (review: Review) => {
|
|
175
|
+
setViewingReview(review)
|
|
176
|
+
setMode('view')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const weekStart = getWeekStart()
|
|
180
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
181
|
+
const allReviews = reviews ?? []
|
|
182
|
+
const filteredReviews = allReviews.filter(r => {
|
|
183
|
+
if (reviewTab === 'daily') return r.type === 'daily' || r.date
|
|
184
|
+
if (reviewTab === 'monthly') return r.type === 'monthly'
|
|
185
|
+
return !r.type || r.type === 'weekly'
|
|
186
|
+
})
|
|
187
|
+
const thisWeekReview = reviewTab === 'weekly'
|
|
188
|
+
? allReviews.find(r => r.week_start === weekStart && (!r.type || r.type === 'weekly'))
|
|
189
|
+
: reviewTab === 'daily'
|
|
190
|
+
? allReviews.find(r => r.date === today && r.type === 'daily')
|
|
191
|
+
: allReviews.find(r => r.type === 'monthly' && r.week_start === today.slice(0, 7))
|
|
192
|
+
const sortedReviews = [...filteredReviews].sort((a, b) => (b.week_start ?? b.date ?? '').localeCompare(a.week_start ?? a.date ?? ''))
|
|
193
|
+
|
|
194
|
+
// View mode
|
|
195
|
+
if (mode === 'view' && viewingReview) {
|
|
196
|
+
const vTab = (viewingReview.type as ReviewTab) || 'weekly'
|
|
197
|
+
const prompts = PROMPTS_MAP[vTab]
|
|
198
|
+
return (
|
|
199
|
+
<div className="max-w-3xl mx-auto">
|
|
200
|
+
<div className="flex items-center justify-between mb-6">
|
|
201
|
+
<div>
|
|
202
|
+
<button onClick={() => { setMode('list'); setViewingReview(null) }} className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors mb-2">
|
|
203
|
+
<ArrowLeft className="w-3.5 h-3.5" /> 목록으로
|
|
204
|
+
</button>
|
|
205
|
+
<h1 className="text-2xl font-bold text-foreground">
|
|
206
|
+
{TAB_LABELS[vTab]}
|
|
207
|
+
</h1>
|
|
208
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
209
|
+
{viewingReview.date || viewingReview.week_start}
|
|
210
|
+
{viewingReview.mood && ` · 기분 ${viewingReview.mood}/5`}
|
|
211
|
+
{viewingReview.score && ` · 점수 ${viewingReview.score}/10`}
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
<Button variant="outline" size="sm" onClick={() => handleEdit(viewingReview)}>
|
|
216
|
+
<Pencil className="w-3.5 h-3.5 mr-1" /> 수정
|
|
217
|
+
</Button>
|
|
218
|
+
<Button variant="outline" size="sm" className="text-gatsaeng-red hover:text-gatsaeng-red" onClick={() => viewingReview.id && handleDelete(viewingReview.id)}>
|
|
219
|
+
<Trash2 className="w-3.5 h-3.5 mr-1" /> 삭제
|
|
220
|
+
</Button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="space-y-4">
|
|
225
|
+
{prompts.map(prompt => {
|
|
226
|
+
const value = (viewingReview as unknown as Record<string, unknown>)[prompt.key] as string | undefined
|
|
227
|
+
if (!value) return null
|
|
228
|
+
return (
|
|
229
|
+
<Card key={prompt.key}>
|
|
230
|
+
<CardContent className="py-4">
|
|
231
|
+
<Label className="text-sm flex items-center gap-2 mb-2">
|
|
232
|
+
<span>{prompt.icon}</span>
|
|
233
|
+
{prompt.label}
|
|
234
|
+
</Label>
|
|
235
|
+
<p className="text-sm text-foreground whitespace-pre-wrap">{value}</p>
|
|
236
|
+
</CardContent>
|
|
237
|
+
</Card>
|
|
238
|
+
)
|
|
239
|
+
})}
|
|
240
|
+
|
|
241
|
+
{(viewingReview.mood || viewingReview.score) && (
|
|
242
|
+
<Card>
|
|
243
|
+
<CardContent className="py-4">
|
|
244
|
+
<div className="flex items-center gap-6">
|
|
245
|
+
{viewingReview.mood && (
|
|
246
|
+
<div>
|
|
247
|
+
<Label className="text-xs text-muted-foreground block mb-1">기분</Label>
|
|
248
|
+
<div className="flex items-center gap-0.5">
|
|
249
|
+
{Array.from({ length: viewingReview.mood }).map((_, i) => (
|
|
250
|
+
<Star key={i} className="w-4 h-4 fill-gatsaeng-amber text-gatsaeng-amber" />
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
{viewingReview.score && (
|
|
256
|
+
<div>
|
|
257
|
+
<Label className="text-xs text-muted-foreground block mb-1">점수</Label>
|
|
258
|
+
<span className="text-lg font-bold text-primary">{viewingReview.score}/10</span>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</CardContent>
|
|
263
|
+
</Card>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Write/Edit mode
|
|
271
|
+
if (mode === 'write') {
|
|
272
|
+
return (
|
|
273
|
+
<div className="max-w-3xl mx-auto">
|
|
274
|
+
<div className="flex items-center justify-between mb-6">
|
|
275
|
+
<div>
|
|
276
|
+
<h1 className="text-2xl font-bold text-foreground">
|
|
277
|
+
{editingReview ? `${TAB_LABELS[(editingReview.type as ReviewTab) || 'weekly']} 수정` : `${TAB_LABELS[reviewTab]} 작성`}
|
|
278
|
+
</h1>
|
|
279
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
280
|
+
{editingReview
|
|
281
|
+
? (editingReview.date || editingReview.week_start)
|
|
282
|
+
: reviewTab === 'daily' ? today : reviewTab === 'monthly' ? today.slice(0, 7) : `${weekStart} 주`}
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
<Button variant="outline" onClick={() => { setMode('list'); setEditingReview(null); setFormData({}) }}>목록으로</Button>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div className="space-y-4">
|
|
289
|
+
{PROMPTS_MAP[editingReview ? ((editingReview.type as ReviewTab) || 'weekly') : reviewTab].map(prompt => (
|
|
290
|
+
<Card key={prompt.key}>
|
|
291
|
+
<CardContent className="py-4">
|
|
292
|
+
<Label className="text-sm flex items-center gap-2 mb-2">
|
|
293
|
+
<span>{prompt.icon}</span>
|
|
294
|
+
{prompt.label}
|
|
295
|
+
</Label>
|
|
296
|
+
<textarea
|
|
297
|
+
className="w-full bg-transparent border border-border rounded-md px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground resize-none focus:outline-none focus:ring-1 focus:ring-ring"
|
|
298
|
+
rows={3}
|
|
299
|
+
placeholder={prompt.placeholder}
|
|
300
|
+
value={formData[prompt.key] ?? ''}
|
|
301
|
+
onChange={e => setFormData(prev => ({ ...prev, [prompt.key]: e.target.value }))}
|
|
302
|
+
/>
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
))}
|
|
306
|
+
|
|
307
|
+
{/* Mood & Score */}
|
|
308
|
+
<Card>
|
|
309
|
+
<CardContent className="py-4">
|
|
310
|
+
<div className="grid grid-cols-2 gap-6">
|
|
311
|
+
<div>
|
|
312
|
+
<Label className="text-sm mb-3 block">기분 (1-5)</Label>
|
|
313
|
+
<div className="flex gap-1">
|
|
314
|
+
{[1, 2, 3, 4, 5].map(v => (
|
|
315
|
+
<button
|
|
316
|
+
key={v}
|
|
317
|
+
onClick={() => setMood(v)}
|
|
318
|
+
className={`w-9 h-9 rounded-md flex items-center justify-center text-sm font-medium transition-colors ${
|
|
319
|
+
mood >= v
|
|
320
|
+
? 'bg-gatsaeng-amber text-black'
|
|
321
|
+
: 'bg-secondary text-muted-foreground hover:bg-secondary/80'
|
|
322
|
+
}`}
|
|
323
|
+
>
|
|
324
|
+
{v}
|
|
325
|
+
</button>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div>
|
|
330
|
+
<Label className="text-sm mb-3 block">점수 (1-10)</Label>
|
|
331
|
+
<div className="flex gap-1 flex-wrap">
|
|
332
|
+
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(v => (
|
|
333
|
+
<button
|
|
334
|
+
key={v}
|
|
335
|
+
onClick={() => setScore(v)}
|
|
336
|
+
className={`w-8 h-8 rounded-md flex items-center justify-center text-xs font-medium transition-colors ${
|
|
337
|
+
score >= v
|
|
338
|
+
? 'bg-primary text-primary-foreground'
|
|
339
|
+
: 'bg-secondary text-muted-foreground hover:bg-secondary/80'
|
|
340
|
+
}`}
|
|
341
|
+
>
|
|
342
|
+
{v}
|
|
343
|
+
</button>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</CardContent>
|
|
349
|
+
</Card>
|
|
350
|
+
|
|
351
|
+
<Button
|
|
352
|
+
onClick={handleSubmit}
|
|
353
|
+
disabled={createReview.isPending || updateReview.isPending}
|
|
354
|
+
className="w-full bg-gatsaeng-purple hover:bg-gatsaeng-purple/80 text-white"
|
|
355
|
+
>
|
|
356
|
+
<BookOpen className="w-4 h-4 mr-2" />
|
|
357
|
+
{editingReview ? '회고 수정' : '회고 저장'}
|
|
358
|
+
</Button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<div className="max-w-3xl mx-auto">
|
|
366
|
+
<div className="flex items-center justify-between mb-6">
|
|
367
|
+
<div>
|
|
368
|
+
<h1 className="text-2xl font-bold text-foreground">계획 & 회고</h1>
|
|
369
|
+
<p className="text-sm text-muted-foreground mt-1">Deliberate Practice 기반 구조화된 회고</p>
|
|
370
|
+
</div>
|
|
371
|
+
{!thisWeekReview && (
|
|
372
|
+
<Button
|
|
373
|
+
onClick={() => { setEditingReview(null); setFormData({}); setMode('write') }}
|
|
374
|
+
className="bg-gatsaeng-purple hover:bg-gatsaeng-purple/80 text-white"
|
|
375
|
+
>
|
|
376
|
+
<BookOpen className="w-4 h-4 mr-2" /> {TAB_LABELS[reviewTab]}
|
|
377
|
+
</Button>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<Tabs value={reviewTab} onValueChange={(v) => setReviewTab(v as ReviewTab)} className="mb-6">
|
|
382
|
+
<TabsList>
|
|
383
|
+
<TabsTrigger value="daily" className="gap-1.5">
|
|
384
|
+
<Sun className="w-3.5 h-3.5" /> 일간
|
|
385
|
+
</TabsTrigger>
|
|
386
|
+
<TabsTrigger value="weekly" className="gap-1.5">
|
|
387
|
+
<Calendar className="w-3.5 h-3.5" /> 주간
|
|
388
|
+
</TabsTrigger>
|
|
389
|
+
<TabsTrigger value="monthly" className="gap-1.5">
|
|
390
|
+
<CalendarDays className="w-3.5 h-3.5" /> 월간
|
|
391
|
+
</TabsTrigger>
|
|
392
|
+
</TabsList>
|
|
393
|
+
</Tabs>
|
|
394
|
+
|
|
395
|
+
{sortedReviews.length === 0 ? (
|
|
396
|
+
<Card>
|
|
397
|
+
<CardContent className="py-12 text-center">
|
|
398
|
+
<BookOpen className="w-8 h-8 mx-auto mb-3 text-muted-foreground" />
|
|
399
|
+
<p className="text-muted-foreground mb-4">아직 회고가 없습니다</p>
|
|
400
|
+
<Button onClick={() => { setEditingReview(null); setFormData({}); setMode('write') }} className="bg-gatsaeng-purple hover:bg-gatsaeng-purple/80 text-white">
|
|
401
|
+
첫 회고 작성하기
|
|
402
|
+
</Button>
|
|
403
|
+
</CardContent>
|
|
404
|
+
</Card>
|
|
405
|
+
) : (
|
|
406
|
+
<div className="space-y-3">
|
|
407
|
+
{sortedReviews.map(review => (
|
|
408
|
+
<Card
|
|
409
|
+
key={review.id}
|
|
410
|
+
className={cn(
|
|
411
|
+
'hover:border-primary/30 transition-colors cursor-pointer group',
|
|
412
|
+
review.week_start === weekStart && 'border-gatsaeng-purple/30'
|
|
413
|
+
)}
|
|
414
|
+
onClick={() => handleView(review)}
|
|
415
|
+
>
|
|
416
|
+
<CardContent className="py-4">
|
|
417
|
+
<div className="flex items-center justify-between">
|
|
418
|
+
<div className="flex items-center gap-3">
|
|
419
|
+
<div className="w-10 h-10 rounded-lg bg-gatsaeng-purple/10 flex items-center justify-center">
|
|
420
|
+
<Calendar className="w-5 h-5 text-gatsaeng-purple" />
|
|
421
|
+
</div>
|
|
422
|
+
<div>
|
|
423
|
+
<div className="text-sm font-medium">{review.date || review.week_start}</div>
|
|
424
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
425
|
+
{review.mood && (
|
|
426
|
+
<div className="flex items-center gap-0.5">
|
|
427
|
+
{Array.from({ length: review.mood }).map((_, i) => (
|
|
428
|
+
<Star key={i} className="w-3 h-3 fill-gatsaeng-amber text-gatsaeng-amber" />
|
|
429
|
+
))}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
{review.score && (
|
|
433
|
+
<Badge variant="outline" className="text-[10px]">
|
|
434
|
+
{review.score}/10
|
|
435
|
+
</Badge>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
<div className="flex items-center gap-2">
|
|
441
|
+
{review.week_start === weekStart && (
|
|
442
|
+
<Badge className="text-[10px] bg-gatsaeng-purple/10 text-gatsaeng-purple border-gatsaeng-purple/30">
|
|
443
|
+
이번 주
|
|
444
|
+
</Badge>
|
|
445
|
+
)}
|
|
446
|
+
<button
|
|
447
|
+
onClick={(e) => { e.stopPropagation(); handleEdit(review) }}
|
|
448
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-primary/10 rounded"
|
|
449
|
+
>
|
|
450
|
+
<Pencil className="w-3.5 h-3.5 text-muted-foreground" />
|
|
451
|
+
</button>
|
|
452
|
+
<button
|
|
453
|
+
onClick={(e) => { e.stopPropagation(); review.id && handleDelete(review.id) }}
|
|
454
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/10 rounded"
|
|
455
|
+
>
|
|
456
|
+
<Trash2 className="w-3.5 h-3.5 text-gatsaeng-red" />
|
|
457
|
+
</button>
|
|
458
|
+
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
{/* Preview of content */}
|
|
463
|
+
{review.accomplished && (
|
|
464
|
+
<p className="text-xs text-muted-foreground mt-2 line-clamp-1">
|
|
465
|
+
🏆 {review.accomplished}
|
|
466
|
+
</p>
|
|
467
|
+
)}
|
|
468
|
+
</CardContent>
|
|
469
|
+
</Card>
|
|
470
|
+
))}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
)
|
|
475
|
+
}
|