diff --git a/.gitignore b/.gitignore index 0367b47..2457088 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts .idea +.gemini diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 600650a..d269a6c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,35 +1,35 @@ -// app/layout.tsx import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import Link from "next/link"; export const metadata: Metadata = { - title: { - default: "QuietSprint", - template: "%s · QuietSprint", - }, - description: "One goal. Start now.", + title: "Focustella", + description: "Space-themed focus timer", }; export default function RootLayout({ children, -}: Readonly<{ children: React.ReactNode }>) { +}: Readonly<{ + children: React.ReactNode; +}>) { return ( - - - {children} + + + {/* Layout Container */} +
+
+ + FOCUSTELLA + + +
+
+ {children} +
+
); diff --git a/src/app/page.tsx b/src/app/page.tsx index e35b542..ab4005f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,266 +1,65 @@ -// app/page.tsx -"use client"; +'use client'; +import Link from "next/link"; +import { ROUTES } from "@/lib/constants"; +import { useEffect, useState } from "react"; +import { getCurrentVoyage } from "@/lib/store"; import { useRouter } from "next/navigation"; -import { useCallback, useMemo, useState } from "react"; +import LobbyBackground from "@/components/LobbyBackground"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; - -type Mode = "freeflow" | "sprint" | "deepwork"; - -const PRIMARY = "#2F6FED"; -const PRIMARY_HOVER = "#295FD1"; - -function modeLabel(mode: Mode) { - switch (mode) { - case "freeflow": - return "프리플로우"; - case "sprint": - return "스프린트"; - case "deepwork": - return "딥워크"; - } -} - -function modeMeta(mode: Mode) { - if (mode === "freeflow") return "무제한"; - if (mode === "sprint") return "25분"; - return "90분"; -} - -function startLabel(mode: Mode) { - if (mode === "freeflow") return "집중 시작"; - return `집중 시작 (${modeMeta(mode)})`; -} - -export default function HomePage() { +export default function Home() { const router = useRouter(); + const [isMount, setIsMount] = useState(false); - const [open, setOpen] = useState(false); - const [mode, setMode] = useState(null); - const [goal, setGoal] = useState(""); + useEffect(() => { + setIsMount(true); + const current = getCurrentVoyage(); + if (current && current.status === 'in_progress') { + router.replace('/flight'); + } + }, [router]); - const meta = useMemo(() => (mode ? modeMeta(mode) : ""), [mode]); - - const go = useCallback( - (m: Mode, g?: string) => { - const params = new URLSearchParams(); - params.set("mode", m); - - if (g && g.trim()) - localStorage.setItem("hushroom:session-goal", g.trim()); - else localStorage.removeItem("hushroom:session-goal"); - - // nextAction은 사용하지 않음 - localStorage.removeItem("hushroom:session-nextAction"); - - localStorage.setItem("hushroom:last-mode", m); - router.push(`/session?${params.toString()}`); - }, - [router], - ); - - const openDialog = (m: Mode) => { - setMode(m); - setGoal(""); - setOpen(true); - }; - - const start = () => { - if (!mode) return; - setOpen(false); - go(mode, goal); - }; + if (!isMount) return null; return ( -
-
-
- QuietSprint -
-
- 딱 한 가지 목표. 바로 시작. -
-
+
+ {/* Background Layer */} + -
- {/* ✅ 파란 CTA = 프리플로우 */} -
-
자유 세션
-
- 시간 제한 없이, 원할 때 종료 (60분마다 가볍게 노크) -
+ {/* Content Layer */} +
+
+

어느 별자리로 출항할까요?

+

몰입하기 좋은 궤도입니다.

- - -
- -
몰입 블록
- + ))}
- {/* ✅ 버튼 위에 설명 (버튼 안에 설명 X) */} -
-
- 시간 고정 세션 -
-
- 한 번 실행되고 끝나면 요약으로 이동 -
+
+ 정거장에서 3명이 대기 중
- - {/* ✅ row(2열) */} -
- openDialog("sprint")} - /> - openDialog("deepwork")} - /> -
-
- - -
- ); -} - -function ModeTile({ - title, - meta, - onClick, -}: { - title: string; - meta: string; - onClick: () => void; -}) { - return ( - - - - ); -} - -function SessionGoalDialog({ - open, - onOpenChange, - mode, - meta, - goal, - setGoal, - onStart, -}: { - open: boolean; - onOpenChange: (v: boolean) => void; - mode: Mode | null; - meta: string; - goal: string; - setGoal: (v: string) => void; - onStart: () => void; -}) { - const title = mode ? `${modeLabel(mode)} · ${meta}` : "세션"; - - return ( - - -
{ - e.preventDefault(); - onStart(); - }} - > - - 세션 목표 설정 -
{title}
-
- -
- setGoal(e.target.value)} - placeholder="지금 할 한 가지를 한 줄로 적어주세요 (선택)" - className="text-lg focus-visible:ring-2" - autoFocus - onKeyDown={(e) => { - if (e.key === "Enter" && (e.nativeEvent as any).isComposing) { - e.preventDefault(); - } - }} - /> -
- 짧게 적을수록 좋아요. 끝이 보이게. -
-
- - - - - -
-
-
+ + ); } diff --git a/src/components/LobbyBackground.tsx b/src/components/LobbyBackground.tsx new file mode 100644 index 0000000..e665728 --- /dev/null +++ b/src/components/LobbyBackground.tsx @@ -0,0 +1,59 @@ +export default function LobbyBackground() { + return ( +
+ {/* Orion (Approximate) - Bottom Left */} +
+ + {/* Stars */} + {/* Betelgeuse */} + {/* Rigel */} + {/* Saiph */} + {/* Bellatrix */} + + {/* Belt */} + + + + + {/* Lines */} + + + + + +
+ + {/* Lyra (Approximate) - Top Right */} +
+ + {/* Vega */} + + + + + + + + + + + +
+ + {/* Ursa Major (Big Dipper) - Top Left */} +
+ + + + + + + + + + + +
+
+ ); +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..0a9677e --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,25 @@ +import { Route } from "@/types"; + +export const ROUTES: Route[] = [ + { + id: 'orion', + name: '오리온', + durationMinutes: 180, + tag: '딥워크', + description: '집필, 코딩 등 긴 호흡이 필요한 작업' + }, + { + id: 'lyra', + name: '거문고', + durationMinutes: 60, + tag: '정리/기획', + description: '기획안 작성, 문서 정리' + }, + { + id: 'cygnus', + name: '백조', + durationMinutes: 30, + tag: '리뷰/회고', + description: '하루 회고, 코드 리뷰' + }, +]; diff --git a/src/lib/store.ts b/src/lib/store.ts new file mode 100644 index 0000000..6a6229b --- /dev/null +++ b/src/lib/store.ts @@ -0,0 +1,43 @@ +import { Voyage, UserPreferences } from "@/types"; + +const KEYS = { + HISTORY: 'focustella_history_v1', + CURRENT: 'focustella_current_v1', + PREFS: 'focustella_prefs_v1', +}; + +export const getHistory = (): Voyage[] => { + if (typeof window === 'undefined') return []; + const item = localStorage.getItem(KEYS.HISTORY); + return item ? JSON.parse(item) : []; +}; + +export const saveToHistory = (voyage: Voyage) => { + const history = getHistory(); + // Add to beginning + localStorage.setItem(KEYS.HISTORY, JSON.stringify([voyage, ...history])); +}; + +export const getCurrentVoyage = (): Voyage | null => { + if (typeof window === 'undefined') return null; + const item = localStorage.getItem(KEYS.CURRENT); + return item ? JSON.parse(item) : null; +}; + +export const saveCurrentVoyage = (voyage: Voyage | null) => { + if (voyage === null) { + localStorage.removeItem(KEYS.CURRENT); + } else { + localStorage.setItem(KEYS.CURRENT, JSON.stringify(voyage)); + } +}; + +export const getPreferences = (): UserPreferences => { + if (typeof window === 'undefined') return { hideSeconds: false }; + const item = localStorage.getItem(KEYS.PREFS); + return item ? JSON.parse(item) : { hideSeconds: false }; +}; + +export const savePreferences = (prefs: UserPreferences) => { + localStorage.setItem(KEYS.PREFS, JSON.stringify(prefs)); +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..f19e094 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,28 @@ +export interface Route { + id: string; + name: string; + durationMinutes: number; + tag: string; + description: string; +} + +export type VoyageStatus = 'completed' | 'partial' | 'reoriented' | 'aborted' | 'in_progress'; + +export interface Voyage { + id: string; + routeId: string; + routeName: string; + startedAt: number; + endedAt?: number; + durationMinutes: number; + status: VoyageStatus; + missionText: string; + notes?: string; + debriefProgress?: string; + nextAction?: string; + blockerTag?: string; +} + +export interface UserPreferences { + hideSeconds: boolean; +}