diff --git a/.gitignore b/.gitignore index 2457088..5ad6d27 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts .idea -.gemini +.cli diff --git a/src/app/flight/page.tsx b/src/app/flight/page.tsx index 22a2a36..75ef5ca 100644 --- a/src/app/flight/page.tsx +++ b/src/app/flight/page.tsx @@ -79,7 +79,7 @@ export default function FlightPage() { return (
- + {/* UI Element 1: Label */}
@@ -115,4 +115,4 @@ export default function FlightPage() {
); -} \ No newline at end of file +} diff --git a/src/app/globals.css b/src/app/globals.css index 672fe9a..f6535c1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -126,25 +126,119 @@ @layer utilities { @keyframes twinkle { - 0%, 100% { opacity: 0.8; transform: scale(1); } - 50% { opacity: 0.3; transform: scale(0.8); } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } } + + @keyframes star-core-pulse { + 0%, + 38%, + 100% { + opacity: var(--core-low, 0.25); + } + 52% { + opacity: var(--core-high, 0.55); + } + } + + @keyframes star-glint-pulse { + 0%, + 35%, + 100% { + opacity: var(--glint-base, 0.02); + } + 50% { + opacity: var(--glint-peak, 0.82); + } + 60% { + opacity: calc(var(--glint-peak, 0.82) * 0.12); + } + } + + @keyframes star-bloom-pulse { + 0%, + 35%, + 100% { + opacity: var(--bloom-base, 0.01); + } + 50% { + opacity: var(--bloom-peak, 0.18); + } + 60% { + opacity: calc(var(--bloom-peak, 0.18) * 0.2); + } + } + .animate-twinkle { animation: twinkle 4s ease-in-out infinite; } - .animate-twinkle-delay-1 { - animation: twinkle 5s ease-in-out infinite; - animation-delay: 1s; + + .star-core { + animation-name: star-core-pulse; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + opacity: var(--core-low, 0.25); + fill: currentColor; } - .animate-twinkle-delay-2 { - animation: twinkle 6s ease-in-out infinite; - animation-delay: 2s; + + .star-core-bloom { + animation-name: star-bloom-pulse; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + opacity: var(--bloom-base, 0.01); + fill: currentColor; + filter: blur(0.7px); + } + + .star-glint { + animation-name: star-glint-pulse; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + opacity: var(--glint-base, 0.02); + stroke-width: 0.62; + stroke-linecap: round; + } + + .star-glint-bloom { + animation-name: star-bloom-pulse; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + opacity: var(--bloom-base, 0.01); + stroke-width: 0.9; + stroke-linecap: round; + filter: blur(0.65px); } @media (prefers-reduced-motion: reduce) { - .animate-twinkle, .animate-twinkle-delay-1, .animate-twinkle-delay-2 { + .animate-twinkle { animation: none; + opacity: 0.8; } + + .star-core { + animation: none; + opacity: var(--core-reduced, 0.34); + } + + .star-core-bloom { + animation: none; + opacity: 0.08; + } + + .star-glint { + animation: none; + opacity: 0.08; + } + + .star-glint-bloom { + animation: none; + opacity: 0.04; + } } -} \ No newline at end of file +} diff --git a/src/app/page.tsx b/src/app/page.tsx index ab4005f..9e90222 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,63 +1,370 @@ -'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 { Route } from "@/types"; +import Link from "next/link"; import { useRouter } from "next/navigation"; -import LobbyBackground from "@/components/LobbyBackground"; +import { CSSProperties, useEffect } from "react"; + +type Star = { + cx: number; + cy: number; + r: number; + armScale?: number; +}; + +type Segment = { + x1: number; + y1: number; + x2: number; + y2: number; +}; + +type Constellation = { + key: "orion" | "auriga" | "ursaMajor"; + className: string; + viewBox: string; + colorClass: string; + stars: Star[]; + segments: Segment[]; +}; + +const CONSTELLATIONS: Constellation[] = [ + { + key: "orion", + className: "absolute bottom-10 left-5 w-72 h-72 opacity-30", + viewBox: "0 0 100 100", + colorClass: "text-indigo-200", + stars: [ + { cx: 25, cy: 15, r: 0.82 }, + { cx: 75, cy: 25, r: 0.96, armScale: 1.08 }, + { cx: 45, cy: 48, r: 0.58 }, + { cx: 50, cy: 50, r: 0.54 }, + { cx: 55, cy: 52, r: 0.6 }, + { cx: 30, cy: 85, r: 0.86 }, + { cx: 70, cy: 80, r: 0.98, armScale: 1.08 }, + ], + segments: [ + { x1: 25, y1: 15, x2: 45, y2: 48 }, + { x1: 75, y1: 25, x2: 55, y2: 52 }, + { x1: 45, y1: 48, x2: 30, y2: 85 }, + { x1: 55, y1: 52, x2: 70, y2: 80 }, + ], + }, + { + key: "auriga", + className: "absolute top-10 right-10 w-64 h-64 opacity-25", + viewBox: "0 0 100 100", + colorClass: "text-blue-200", + stars: [ + { cx: 50, cy: 15, r: 1.06, armScale: 1.1 }, + { cx: 20, cy: 35, r: 0.67 }, + { cx: 25, cy: 75, r: 0.65 }, + { cx: 75, cy: 75, r: 0.66 }, + { cx: 85, cy: 35, r: 0.76 }, + ], + segments: [ + { x1: 50, y1: 15, x2: 20, y2: 35 }, + { x1: 20, y1: 35, x2: 25, y2: 75 }, + { x1: 25, y1: 75, x2: 75, y2: 75 }, + { x1: 75, y1: 75, x2: 85, y2: 35 }, + { x1: 85, y1: 35, x2: 50, y2: 15 }, + ], + }, + { + key: "ursaMajor", + className: "absolute top-20 left-10 w-80 h-48 opacity-25", + viewBox: "0 0 100 60", + colorClass: "text-slate-200", + stars: [ + { cx: 10, cy: 20, r: 0.64 }, + { cx: 25, cy: 25, r: 0.67 }, + { cx: 40, cy: 35, r: 0.69 }, + { cx: 55, cy: 45, r: 0.99 }, + { cx: 75, cy: 45, r: 0.98 }, + { cx: 80, cy: 15, r: 0.93 }, + { cx: 60, cy: 10, r: 0.96 }, + ], + segments: [ + { x1: 10, y1: 20, x2: 25, y2: 25 }, + { x1: 25, y1: 25, x2: 40, y2: 35 }, + { x1: 40, y1: 35, x2: 55, y2: 45 }, + { x1: 55, y1: 45, x2: 75, y2: 45 }, + { x1: 75, y1: 45, x2: 80, y2: 15 }, + { x1: 80, y1: 15, x2: 60, y2: 10 }, + { x1: 60, y1: 10, x2: 55, y2: 45 }, + ], + }, +]; + +const STAR_STYLES = [ + { duration: 2.3, delay: 0.1 }, + { duration: 3.1, delay: 1.2 }, + { duration: 4.8, delay: 0.8 }, + { duration: 2.7, delay: 2.1 }, + { duration: 5.2, delay: 1.7 }, + { duration: 3.9, delay: 0.4 }, + { duration: 4.4, delay: 2.6 }, + { duration: 2.1, delay: 1.3 }, + { duration: 5.8, delay: 0.2 }, + { duration: 3.3, delay: 2.4 }, + { duration: 4.0, delay: 1.1 }, + { duration: 2.9, delay: 1.9 }, +] as const; + +function StarGlint({ starIndex, star }: { starIndex: number; star: Star }) { + const timing = STAR_STYLES[starIndex % STAR_STYLES.length]; + const strengthTier = + star.r >= 0.95 ? "bright" : star.r >= 0.72 ? "mid" : "faint"; + const glintPeak = + strengthTier === "bright" ? 0.7 : strengthTier === "mid" ? 0.61 : 0.52; + const bloomPeak = + strengthTier === "bright" ? 0.16 : strengthTier === "mid" ? 0.12 : 0.08; + const coreLow = + strengthTier === "bright" ? 0.9 : strengthTier === "mid" ? 0.73 : 0.6; + const coreHigh = + strengthTier === "bright" ? 1 : strengthTier === "mid" ? 0.9 : 0.82; + const coreReduced = + strengthTier === "bright" ? 0.9 : strengthTier === "mid" ? 0.76 : 0.66; + const coreStyle = { + animationDuration: `${timing.duration}s`, + animationDelay: `${timing.delay}s`, + "--core-low": `${coreLow}`, + "--core-high": `${coreHigh}`, + "--core-reduced": `${coreReduced}`, + } as CSSProperties; + const glintStyle = { + animationDuration: `${timing.duration}s`, + animationDelay: `${timing.delay + 0.12}s`, + "--glint-peak": `${glintPeak}`, + "--glint-base": "0.02", + "--bloom-peak": `${bloomPeak}`, + "--bloom-base": "0.01", + } as CSSProperties; + const glintLength = star.r * 4.4 * (star.armScale ?? 1); + const gradientXId = `glint-x-${starIndex}`; + const gradientYId = `glint-y-${starIndex}`; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function LobbyBackground() { + return ( +
+ {CONSTELLATIONS.map((constellation, constellationIndex) => ( +
+ + {constellation.segments.map((segment, segmentIndex) => ( + + ))} + + {constellation.stars.map((star, starIndex) => { + const globalStarIndex = + CONSTELLATIONS.slice(0, constellationIndex).reduce( + (sum, item) => sum + item.stars.length, + 0, + ) + starIndex; + + return ( + + ); + })} + +
+ ))} +
+ ); +} + +function RouteCard({ + route, + isCTA = false, +}: { + route: Route; + isCTA?: boolean; +}) { + return ( +
+
+
+

+ {route.name} +

+ + {route.tag} + +
+ + {route.durationMinutes === 0 ? "∞" : route.durationMinutes} + + {route.durationMinutes === 0 ? "" : "min"} + + +
+ + {!isCTA && ( +

+ {route.description} +

+ )} + + {isCTA && ( +

+ {route.description} +

+ )} + + + {isCTA ? "정거장 진입 (대기)" : "바로 출항"} + +
+ ); +} export default function Home() { const router = useRouter(); - const [isMount, setIsMount] = useState(false); useEffect(() => { - setIsMount(true); const current = getCurrentVoyage(); - if (current && current.status === 'in_progress') { - router.replace('/flight'); + if (current && current.status === "in_progress") { + router.replace("/flight"); } }, [router]); - if (!isMount) return null; + const stationRoute = ROUTES[0]; + const normalRoutes = ROUTES.slice(1); return (
- {/* Background Layer */} - {/* Content Layer */}
-

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

+

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

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

-
- {ROUTES.map((route) => ( -
-
-
-

{route.name}

- - {route.tag} - -
- {route.durationMinutes}min -
-

{route.description}

- - 바로 출항 - -
- ))} -
+
+
+ +
-
- 정거장에서 3명이 대기 중 +
+ {normalRoutes.map((route) => ( + + ))} +
diff --git a/src/components/FlightBackground.tsx b/src/components/FlightBackground.tsx index 982ae69..28f88c5 100644 --- a/src/components/FlightBackground.tsx +++ b/src/components/FlightBackground.tsx @@ -2,7 +2,31 @@ import { useEffect, useRef } from 'react'; -export default function FlightBackground() { +type FlightBackgroundProps = { + vanishYOffset?: number; + centerProtectRadius?: number; +}; + +type Star = { + wx: number; + wy: number; + z: number; + speed: number; + radius: number; + alpha: number; + tailLength: number; +}; + +const clamp = (value: number, min: number, max: number) => + Math.min(max, Math.max(min, value)); + +const randomInRange = (min: number, max: number) => + min + Math.random() * (max - min); + +export default function FlightBackground({ + vanishYOffset = -64, + centerProtectRadius = 190, +}: FlightBackgroundProps) { const canvasRef = useRef(null); useEffect(() => { @@ -14,9 +38,12 @@ export default function FlightBackground() { let width = window.innerWidth; let height = window.innerHeight; + let animationFrameId = 0; - // Check prefers-reduced-motion - const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + let prefersReduced = motionQuery.matches; + + const vanishXJitter = (Math.random() < 0.5 ? -1 : 1) * randomInRange(10, 25); const setSize = () => { width = window.innerWidth; @@ -24,103 +51,262 @@ export default function FlightBackground() { canvas.width = width; canvas.height = height; }; + + const getStarCount = () => { + const isMobile = width < 768; + const min = isMobile ? 12 : 18; + const max = isMobile ? 30 : 45; + const byArea = Math.round((width * height) / 42000); + return clamp(byArea, min, max); + }; + + const getVanishingPoint = () => ({ + x: width / 2 + vanishXJitter, + y: height / 2 + vanishYOffset, + }); + + const createSpeed = () => { + const tier = Math.random(); + + if (tier < 0.9) return randomInRange(0.003, 0.007); + if (tier < 0.99) return randomInRange(0.007, 0.011); + return randomInRange(0.011, 0.014); + }; + + const createVisuals = () => { + const highlight = Math.random() < 0.16; + const tailRoll = Math.random(); + const tailLength = + tailRoll < 0.82 + ? randomInRange(0, 2.5) + : tailRoll < 0.86 + ? randomInRange(2.5, 3.8) + : randomInRange(4, 10); + + return { + radius: highlight ? randomInRange(1.2, 1.8) : randomInRange(0.7, 1.2), + alpha: highlight ? randomInRange(0.55, 0.85) : randomInRange(0.25, 0.55), + tailLength, + }; + }; + + const createSpawnRadius = () => { + const roll = Math.random(); + const maxWideRadius = Math.min(Math.max(width, height) * 0.7, 360); + const ringOuter = Math.min(320, maxWideRadius); + + if (roll < 0.08) { + return randomInRange(0, 60); + } + + if (roll < 0.8) { + return randomInRange(80, Math.max(80, ringOuter)); + } + + return randomInRange(120, Math.max(120, maxWideRadius)); + }; + + const createStar = (isRespawn: boolean): Star => { + const radius = createSpawnRadius(); + const angle = randomInRange(0, Math.PI * 2); + const visuals = createVisuals(); + + return { + wx: Math.cos(angle) * radius, + wy: Math.sin(angle) * radius, + z: isRespawn ? randomInRange(0.9, 1.55) : randomInRange(0.55, 1.6), + speed: createSpeed(), + radius: visuals.radius, + alpha: visuals.alpha, + tailLength: visuals.tailLength, + }; + }; + setSize(); - window.addEventListener('resize', setSize); + let stars: Star[] = Array.from({ length: getStarCount() }, () => createStar(false)); - // Star configuration - const starCount = 150; - const stars: { x: number; y: number; z: number; speed: number }[] = []; - - // Target point (slightly above center) - // We will move stars away from this point. - // Instead of full 3D, let's do a 2D split flow. - // Stars spawn randomly. - // If x < center, move left-down. - // If x > center, move right-down. - - // Initial Spawn - for (let i = 0; i < starCount; i++) { - stars.push({ - x: Math.random() * width, - y: Math.random() * height, - z: Math.random() * 2 + 0.5, // depth/size - speed: Math.random() * 2 + 0.5 - }); - } + const screenDistanceFromCenter = (x: number, y: number) => { + const cx = width / 2; + const cy = height / 2; + return Math.hypot(x - cx, y - cy); + }; - let animationFrameId: number; + const applyCenterProtection = (x: number, y: number, alpha: number) => { + const distance = screenDistanceFromCenter(x, y); - const render = () => { - if (!ctx) return; - ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; // Trails + if (distance >= centerProtectRadius) return alpha; + + const ratio = clamp(distance / centerProtectRadius, 0, 1); + const attenuation = 0.35 + ratio * 0.65; + return alpha * attenuation; + }; + + const project = (star: Star, zValue: number) => { + const vp = getVanishingPoint(); + return { + x: vp.x + star.wx / zValue, + y: vp.y + star.wy / zValue, + }; + }; + + const recycleStar = (index: number) => { + stars[index] = createStar(true); + }; + + const drawStar = ( + star: Star, + fromX: number, + fromY: number, + toX: number, + toY: number, + ) => { + const visibleAlpha = applyCenterProtection(toX, toY, star.alpha); + const dx = toX - fromX; + const dy = toY - fromY; + const movementLength = Math.hypot(dx, dy); + + if (star.tailLength < 1 || movementLength < 0.001) { + ctx.globalAlpha = visibleAlpha; + ctx.fillStyle = '#f8fbff'; + ctx.beginPath(); + ctx.arc(toX, toY, star.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.globalAlpha = 1; + return; + } + + const dirX = dx / movementLength; + const dirY = dy / movementLength; + const tailX = toX - dirX * star.tailLength; + const tailY = toY - dirY * star.tailLength; + + const gradient = ctx.createLinearGradient(tailX, tailY, toX, toY); + gradient.addColorStop(0, 'rgba(248, 251, 255, 0)'); + gradient.addColorStop(1, `rgba(248, 251, 255, ${visibleAlpha})`); + + ctx.strokeStyle = gradient; + ctx.lineWidth = clamp(star.radius * 0.9, 0.65, 1.6); + ctx.beginPath(); + ctx.moveTo(tailX, tailY); + ctx.lineTo(toX, toY); + ctx.stroke(); + + ctx.globalAlpha = Math.min(1, visibleAlpha + 0.08); + ctx.fillStyle = '#f8fbff'; + ctx.beginPath(); + ctx.arc(toX, toY, clamp(star.radius * 0.72, 0.6, 1.45), 0, Math.PI * 2); + ctx.fill(); + ctx.globalAlpha = 1; + }; + + const drawCenterVeil = () => { + const vp = getVanishingPoint(); + const veil = ctx.createRadialGradient( + vp.x, + vp.y, + centerProtectRadius * 0.18, + vp.x, + vp.y, + centerProtectRadius * 1.35, + ); + veil.addColorStop(0, 'rgba(160, 185, 235, 0.08)'); + veil.addColorStop(0.55, 'rgba(90, 114, 170, 0.03)'); + veil.addColorStop(1, 'rgba(0, 0, 0, 0)'); + + ctx.fillStyle = veil; ctx.fillRect(0, 0, width, height); - // ctx.clearRect(0, 0, width, height); // Clear + }; - const targetX = width / 2; - // targetY is virtual, above screen. - // Movement vector: - // Left side: dx = -speed, dy = speed - // Right side: dx = speed, dy = speed + const drawVignette = () => { + const vignette = ctx.createRadialGradient( + width / 2, + height / 2, + Math.min(width, height) * 0.25, + width / 2, + height / 2, + Math.max(width, height) * 0.95, + ); + vignette.addColorStop(0, 'rgba(0, 0, 0, 0)'); + vignette.addColorStop(1, 'rgba(0, 0, 0, 0.82)'); + ctx.fillStyle = vignette; + ctx.fillRect(0, 0, width, height); + }; - ctx.fillStyle = '#ffffff'; + const drawFrame = (moveStars: boolean) => { + ctx.fillStyle = 'rgba(2, 5, 10, 0.3)'; + ctx.fillRect(0, 0, width, height); - stars.forEach((star) => { - // Reduced motion: very slow or static - const moveFactor = prefersReduced ? 0.1 : 1.0; + drawCenterVeil(); - // Determine direction based on side - const isLeft = star.x < targetX; - - // Simple perspective-ish flow - // The further from center X, the faster dx - // The further down, the faster dy - - // Let's stick to the PRD requirements: - // Left -> Bottom-Left - // Right -> Bottom-Right - - const dx = (isLeft ? -1 : 1) * star.speed * 0.5 * moveFactor; - const dy = star.speed * moveFactor; + stars.forEach((star, index) => { + const from = project(star, star.z); - star.x += dx; - star.y += dy; - - // Reset if out of bounds - if (star.y > height || star.x < 0 || star.x > width) { - star.y = -10; - star.x = Math.random() * width; - // Bias respawn near center-top for "flow" effect? - // Or just random top. Random top is safer for uniform coverage. + if (moveStars) { + star.z -= star.speed; } - const size = Math.max(0.5, star.z * (star.y / height) * 2); - const opacity = Math.min(1, star.y / 200); // Fade in from top + const to = project(star, star.z); - ctx.globalAlpha = opacity; - ctx.beginPath(); - ctx.arc(star.x, star.y, size, 0, Math.PI * 2); - ctx.fill(); - ctx.globalAlpha = 1.0; + const outOfBounds = + to.x < -50 || to.x > width + 50 || to.y < -50 || to.y > height + 50; + + if (star.z <= 0.22 || outOfBounds) { + recycleStar(index); + return; + } + + drawStar(star, from.x, from.y, to.x, to.y); }); - // Draw subtle cockpit frame or vignette - // Gradient vignette - const gradient = ctx.createRadialGradient(width/2, height/2, height/3, width/2, height/2, height); - gradient.addColorStop(0, 'transparent'); - gradient.addColorStop(1, 'rgba(0,0,0,0.8)'); - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, width, height); + drawVignette(); + }; + const render = () => { + drawFrame(true); animationFrameId = requestAnimationFrame(render); }; - render(); + const renderStatic = () => { + ctx.clearRect(0, 0, width, height); + drawFrame(false); + }; + + const handleResize = () => { + setSize(); + stars = Array.from({ length: getStarCount() }, () => createStar(false)); + + if (prefersReduced) { + renderStatic(); + } + }; + + const handleMotionChange = (event: MediaQueryListEvent) => { + prefersReduced = event.matches; + + if (prefersReduced) { + cancelAnimationFrame(animationFrameId); + renderStatic(); + return; + } + + render(); + }; + + window.addEventListener('resize', handleResize); + motionQuery.addEventListener('change', handleMotionChange); + + if (prefersReduced) { + renderStatic(); + } else { + render(); + } return () => { - window.removeEventListener('resize', setSize); + window.removeEventListener('resize', handleResize); + motionQuery.removeEventListener('change', handleMotionChange); cancelAnimationFrame(animationFrameId); }; - }, []); + }, [vanishYOffset, centerProtectRadius]); return ; } diff --git a/src/components/LobbyBackground.tsx b/src/components/LobbyBackground.tsx index e665728..e5e3df8 100644 --- a/src/components/LobbyBackground.tsx +++ b/src/components/LobbyBackground.tsx @@ -1,57 +1,330 @@ +"use client"; + +import { useEffect, useState } from "react"; + export default function LobbyBackground() { + const [isMount, setIsMount] = useState(false); + + useEffect(() => { + setIsMount(true); + }, []); + + // Helper for random delay/duration style + const getRandomStyle = () => ({ + animationDelay: `${Math.random() * 5}s`, + animationDuration: `${2 + Math.random() * 4}s`, + }); + + if (!isMount) return
; + return (
- {/* Orion (Approximate) - Bottom Left */} -
- - {/* Stars */} - {/* Betelgeuse */} - {/* Rigel */} - {/* Saiph */} - {/* Bellatrix */} - - {/* Belt */} - - - + {/* Orion - Bottom Left */} +
+ + + + + + + + - {/* Lines */} - - - - + + + +
- {/* Lyra (Approximate) - Top Right */} -
- - {/* Vega */} - - - - - - - - - - + {/* Auriga (마차부자리) - Top Right */} +
+ + {" "} + {/* Capella */} + + + + + + + + +
- {/* Ursa Major (Big Dipper) - Top Left */} -
- - - - - - - - + {/* Big Dipper (북두칠성) - Top Left */} +
+ + {/* Handle */} + + + + {/* Bowl */} + + + + - + + + + + + +
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 0a9677e..5c94c54 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,25 +1,25 @@ import { Route } from "@/types"; export const ROUTES: Route[] = [ - { - id: 'orion', - name: '오리온', - durationMinutes: 180, - tag: '딥워크', - description: '집필, 코딩 등 긴 호흡이 필요한 작업' + { + id: "station", + name: "우주정거장", + durationMinutes: 0, // 0 implies unlimited + tag: "대기/자유", + description: "시간 제한 없이 머무를 수 있는 안전지대", }, - { - id: 'lyra', - name: '거문고', - durationMinutes: 60, - tag: '정리/기획', - description: '기획안 작성, 문서 정리' + { + id: "orion", + name: "오리온", + durationMinutes: 60, + tag: "딥워크", + description: "60분 집중 항해", }, - { - id: 'cygnus', - name: '백조', - durationMinutes: 30, - tag: '리뷰/회고', - description: '하루 회고, 코드 리뷰' + { + id: "gemini", + name: "쌍둥이자리", + durationMinutes: 30, + tag: "숏스프린트", + description: "30분 집중 항해", }, ];