'use client'; import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useMediaCatalog, getSceneStageBackgroundStyle } from '@/entities/media'; import { usePlanTier } from '@/entities/plan'; import { getSceneById, SCENE_THEMES } from '@/entities/scene'; import { SOUND_PRESETS } from '@/entities/session'; import { PaywallSheetContent } from '@/features/paywall-sheet'; import { PlanPill } from '@/features/plan-pill'; import { focusSessionApi, type FocusSession } from '@/features/focus-session/api/focusSessionApi'; import { useFocusStats, type ReviewCarryHint } from '@/features/stats'; import { copy } from '@/shared/i18n'; import { cn } from '@/shared/lib/cn'; const DEFAULT_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id; const DEFAULT_SOUND_ID = SOUND_PRESETS.find((preset) => preset.id === 'forest-birds')?.id ?? SOUND_PRESETS[0].id; const DEFAULT_TIMER_ID = '50-10'; const REVIEW_ENTRY_PRESETS = { 'forest-50-10': { sceneId: DEFAULT_SCENE_ID, soundPresetId: DEFAULT_SOUND_ID, timerPresetId: DEFAULT_TIMER_ID, label: '숲 · 50/10 · Forest Birds', }, } as const; const GOAL_SUGGESTIONS = copy.session.goalChips.slice(0, 4); const entryCopy = { eyebrow: 'VibeRoom', title: '지금 붙잡을 한 가지', description: '길게 정리하지 말고, 한 줄만 남기고 바로 들어가요.', goalPlaceholder: '예: 제안서 첫 문단만 다듬기', microStepLabel: '지금 할 한 조각', microStepPlaceholder: '예: 파일 열고 첫 문장만 정리하기', microStepHelper: '선택 사항이에요. 바로 손이 가게 만드는 한 조각이면 충분해요.', startNow: '지금 시작', startLoading: '몰입 준비 중...', ritualHint: '기본 ritual · 숲 · 50/10 · Forest Birds', ritualHelper: '공간과 사운드는 들어간 뒤에도 바꿀 수 있어요.', resumeEyebrow: 'Resume', resumeRunning: '진행 중인 세션이 있어요.', resumePaused: '잠시 멈춘 세션이 있어요.', resumeCta: '이어서 몰입하기', resumeRefocusCta: '한 조각 다시 잡기', resumeRouting: '진행 중인 세션으로 돌아가는 중이에요.', resumeMicroStepLabel: '마지막 한 조각', resumePausedHint: '같은 목표를 바로 이어가거나, 다시 시작할 한 조각만 먼저 정리할 수 있어요.', resumeNewGoalHint: '새 목표는 현재 세션을 마무리한 뒤 시작할 수 있어요.', loadFailed: '세션 상태를 불러오지 못했어요. 새로 시작은 계속 할 수 있어요.', reviewEyebrow: 'Weekly Review', reviewTitle: '이번 주 review를 잠깐 보고 갈까요?', reviewCta: '주간 review 보기', reviewHelper: '다음 세션 전에 가볍게 보고 갈 수 있어요.', reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?', reviewCtaPro: '나에게 맞는 흐름 보기', reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.', resumeReviewEyebrow: 'Weekly Review', resumeReviewTitle: '잠깐 review를 보고 다시 들어갈 수 있어요.', resumeReviewHelper: '현재 세션은 그대로 두고, 이번 주 흐름만 짧게 확인합니다.', reviewReturnEyebrow: '방금 본 review 기준', reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.', reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.', reviewReturnTitleClosure: '이번엔 어디서 닫을지 먼저 정해보세요.', reviewReturnTitleStart: '이번 주는 시작 횟수 하나를 더 만드는 게 먼저예요.', reviewReturnBodySteady: 'goal은 직접 정하되, 지금처럼 가볍게 들어가는 리듬을 유지해 보세요.', reviewReturnBodySmaller: '길이를 늘리기보다, 더 작은 goal과 더 구체적인 첫 한 조각으로 시작하면 이어가기 쉬워져요.', reviewReturnBodyClosure: '큰 흐름보다 지금 블록을 어디서 마무리할지 먼저 떠올리면 끝까지 가져가기 쉬워져요.', reviewReturnBodyStart: '길이를 늘리기보다, 아주 작은 goal로 이번 주 첫 세션 하나를 더 여는 데 집중해 보세요.', reviewReturnRitualLabel: '추천 ritual · 숲 · 50/10 · Forest Birds', paywallLead: 'Calm Session OS PRO', paywallBody: 'Pro는 더 빠른 ritual과 더 깊은 review로 시작과 복귀를 가볍게 만듭니다.', }; const goalCardClass = 'w-full rounded-[2rem] border border-white/12 bg-[#0f1115]/26 px-6 py-6 shadow-[0_24px_60px_rgba(3,7,18,0.32)] backdrop-blur-xl md:px-8 md:py-8'; const inputShellClass = 'w-full rounded-[1.75rem] border border-white/14 bg-white/[0.06] px-5 py-4 text-white outline-none transition focus:border-white/24 focus:bg-white/[0.09]'; const primaryButtonClass = 'inline-flex items-center justify-center rounded-full border border-white/16 bg-white/[0.14] px-6 py-3 text-sm font-medium text-white transition hover:bg-white/[0.18] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-48'; const secondaryButtonClass = 'inline-flex items-center justify-center rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-medium text-white/84 transition hover:bg-white/[0.1] hover:text-white active:scale-[0.99]'; const timerLabelById: Record = { '25-5': '25/5', '50-10': '50/10', '90-20': '90/20', }; const resolveSoundLabel = (soundPresetId?: string | null) => { if (!soundPresetId) { return 'Silent'; } return SOUND_PRESETS.find((preset) => preset.id === soundPresetId)?.label ?? 'Silent'; }; const reviewCarryCopyByHint: Record< ReviewCarryHint, { title: string; body: string } > = { steady: { title: entryCopy.reviewReturnTitleSteady, body: entryCopy.reviewReturnBodySteady, }, smaller: { title: entryCopy.reviewReturnTitleSmaller, body: entryCopy.reviewReturnBodySmaller, }, closure: { title: entryCopy.reviewReturnTitleClosure, body: entryCopy.reviewReturnBodyClosure, }, start: { title: entryCopy.reviewReturnTitleStart, body: entryCopy.reviewReturnBodyStart, }, }; export const FocusDashboardWidget = () => { const router = useRouter(); const searchParams = useSearchParams(); const { plan, isPro, setPlan } = usePlanTier(); const { sceneAssetMap } = useMediaCatalog(); const { review, summary: weeklySummary } = useFocusStats(); const reviewEntryPreset = searchParams.get('entryPreset'); const reviewEntryPresetConfig = useMemo(() => { if (!reviewEntryPreset) { return null; } return REVIEW_ENTRY_PRESETS[reviewEntryPreset as keyof typeof REVIEW_ENTRY_PRESETS] ?? null; }, [reviewEntryPreset]); const [goalDraft, setGoalDraft] = useState(''); const [microStepDraft, setMicroStepDraft] = useState(''); const [isStartingSession, setIsStartingSession] = useState(false); const [paywallSource, setPaywallSource] = useState(null); const [currentSession, setCurrentSession] = useState(null); const [isCheckingSession, setIsCheckingSession] = useState(true); const [sessionLookupError, setSessionLookupError] = useState(null); const goalInputRef = useRef(null); const activeScene = useMemo(() => { return getSceneById(currentSession?.sceneId ?? reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0]; }, [currentSession?.sceneId, reviewEntryPresetConfig?.sceneId]); const activeRitualMeta = useMemo(() => { const timerLabel = timerLabelById[currentSession?.timerPresetId ?? DEFAULT_TIMER_ID] ?? '50/10'; const soundLabel = resolveSoundLabel(currentSession?.soundPresetId ?? DEFAULT_SOUND_ID); return `${activeScene.name} · ${timerLabel} · ${soundLabel}`; }, [activeScene.name, currentSession?.soundPresetId, currentSession?.timerPresetId]); const trimmedGoal = goalDraft.trim(); const canStart = trimmedGoal.length > 0 && !isStartingSession && !currentSession; const hasEnoughWeeklyData = weeklySummary.last7Days.startedSessions >= 3 && (weeklySummary.last7Days.completedSessions >= 2 || weeklySummary.recovery.pausedSessions > 0); const reviewSource = searchParams.get('review'); const reviewCarryHint = searchParams.get('carryHint'); const normalizedReviewCarryHint: ReviewCarryHint | null = reviewCarryHint === 'steady' || reviewCarryHint === 'smaller' || reviewCarryHint === 'closure' || reviewCarryHint === 'start' ? reviewCarryHint : null; const isReviewReturn = reviewSource === 'weekly' && normalizedReviewCarryHint !== null; const reviewReturnCopy = normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null; const reviewReturnRitualLabel = isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null; const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle; const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary; const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper; const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta; const entryRitualHint = reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : entryCopy.ritualHint; const isRunningSession = currentSession?.state === 'running'; const isPausedSession = currentSession?.state === 'paused'; useEffect(() => { let cancelled = false; const loadCurrentSession = async () => { setIsCheckingSession(true); try { const session = await focusSessionApi.getCurrentSession(); if (cancelled) { return; } setCurrentSession(session); setSessionLookupError(null); } catch (error) { if (cancelled) { return; } setCurrentSession(null); setSessionLookupError(error instanceof Error ? error.message : entryCopy.loadFailed); } finally { if (!cancelled) { setIsCheckingSession(false); } } }; void loadCurrentSession(); return () => { cancelled = true; }; }, []); useEffect(() => { if (!isCheckingSession && isRunningSession) { router.replace('/space'); } }, [isCheckingSession, isRunningSession, router]); const openPaywall = () => { if (!isPro) { setPaywallSource('app-entry-plan-pill'); } }; const handleSelectSuggestion = (label: string) => { setGoalDraft(label); goalInputRef.current?.focus(); }; const handleStartSession = async () => { if (!trimmedGoal || isStartingSession || currentSession) { if (!trimmedGoal) { goalInputRef.current?.focus(); } return; } setIsStartingSession(true); try { await focusSessionApi.startSession({ goal: trimmedGoal, microStep: microStepDraft.trim() || null, sceneId: reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID, soundPresetId: reviewEntryPresetConfig?.soundPresetId ?? DEFAULT_SOUND_ID, timerPresetId: reviewEntryPresetConfig?.timerPresetId ?? DEFAULT_TIMER_ID, entryPoint: 'space-setup', }); router.push('/space'); return; } catch (error) { console.error('Failed to start focus session from /app', error); } setIsStartingSession(false); }; const handleResumeSession = () => { router.push('/space?resume=continue'); }; const handleResumeRefocus = () => { router.push('/space?resume=refocus'); }; const shouldShowWeeklyReviewTeaser = !isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn; const shouldShowResumeReviewEntry = !isCheckingSession && isPausedSession && hasEnoughWeeklyData; return (

{entryCopy.eyebrow}

{isCheckingSession ? (

{entryCopy.resumeEyebrow}

세션 상태를 불러오는 중이에요.

) : (
{reviewReturnCopy ? (

{entryCopy.reviewReturnEyebrow}

{reviewReturnCopy.title}

{reviewReturnCopy.body}

{reviewReturnRitualLabel ? (

{reviewReturnRitualLabel}

) : null}
) : null} {isRunningSession ? (

{entryCopy.resumeEyebrow}

{entryCopy.resumeRouting}

) : currentSession ? (

{entryCopy.resumeEyebrow}

{currentSession.goal}

{entryCopy.resumePaused}

{currentSession.microStep ? (

{entryCopy.resumeMicroStepLabel}

{currentSession.microStep}

) : null}

{activeRitualMeta}

{entryCopy.resumePausedHint}

{entryCopy.resumeNewGoalHint}

{shouldShowResumeReviewEntry ? (

{entryCopy.resumeReviewEyebrow}

{entryCopy.resumeReviewTitle}

{isPro ? review.carryForward.keepDoing : entryCopy.resumeReviewHelper}

{reviewTeaserCta}
) : null}
) : ( <>

{entryCopy.title}

{entryCopy.description}

{entryCopy.microStepHelper}

{GOAL_SUGGESTIONS.map((suggestion) => { const isActive = trimmedGoal === suggestion.label; return ( ); })}

{entryRitualHint}

{entryCopy.ritualHelper}

{sessionLookupError ? (

{entryCopy.loadFailed}

) : null}
{shouldShowWeeklyReviewTeaser ? (

{entryCopy.reviewEyebrow}

{reviewTeaserTitle}

{reviewTeaserSummary}

{reviewTeaserHelper}

{reviewTeaserCta}
) : null} )}
)}
{paywallSource ? (
) : null}
); };