diff --git a/src/widgets/space-focus-hud/ui/CompletionResultModal.tsx b/src/widgets/space-focus-hud/ui/CompletionResultModal.tsx deleted file mode 100644 index f48ab30..0000000 --- a/src/widgets/space-focus-hud/ui/CompletionResultModal.tsx +++ /dev/null @@ -1,138 +0,0 @@ -'use client'; - -import { copy } from '@/shared/i18n'; -import { cn } from '@/shared/lib/cn'; -import type { CompletionResult } from '@/features/focus-session'; - -interface CompletionResultModalProps { - open: boolean; - result: CompletionResult | null; - onClose: () => void; -} - -const formatFocusedMinutes = (focusedSeconds: number) => { - const safeSeconds = Math.max(0, focusedSeconds); - return Math.round(safeSeconds / 60); -}; - -export const CompletionResultModal = ({ - open, - result, - onClose, -}: CompletionResultModalProps) => { - if (!result) { - return null; - } - - const focusedMinutes = formatFocusedMinutes(result.focusedSeconds); - const hasThoughts = result.thoughts.length > 0; - - return ( -
-
- -
-
-
-
-
- -
-

- {copy.space.completionResult.eyebrow} -

-

- {copy.space.completionResult.title} -

-

- {copy.space.completionResult.description} -

-
- -
-
-

- {copy.space.completionResult.focusedLabel} -

-

- {copy.space.completionResult.focusedValue(focusedMinutes)} -

-
- -
-

- {copy.space.completionResult.goalLabel} -

-

- {result.completedGoal} -

-
- - {hasThoughts ? ( -
-
-

- {copy.space.completionResult.thoughtsLabel} -

-

- {copy.space.completionResult.thoughtCount(result.thoughts.length)} -

-
-
- {result.thoughts.map((thought) => ( -
-

- {thought.text} -

-

- {thought.sceneName} -

-
- ))} -
-
- ) : null} -
- -
- -
-
-
-
- ); -}; diff --git a/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx b/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx index ba5437c..1852b65 100644 --- a/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx +++ b/src/widgets/space-focus-hud/ui/EndSessionConfirmModal.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { cn } from '@/shared/lib/cn'; +import type { CompletionResult } from '@/features/focus-session'; type EndSessionStage = 'decision' | 'success' | 'unfinished'; @@ -10,10 +11,15 @@ interface EndSessionConfirmModalProps { currentGoal: string; onClose: () => void; onAdvanceGoal: (nextGoal: string) => Promise | boolean; // kept for compatibility if needed - onFinishHere: () => Promise | boolean; // User achieved goal -> exit + onFinishHere: () => Promise | CompletionResult | null; // User achieved goal -> returns result onEndSession: () => Promise | boolean; // User did not achieve -> exit } +const formatFocusedMinutes = (focusedSeconds: number) => { + const safeSeconds = Math.max(0, focusedSeconds); + return Math.round(safeSeconds / 60); +}; + export const EndSessionConfirmModal = ({ open, currentGoal, @@ -23,22 +29,39 @@ export const EndSessionConfirmModal = ({ }: EndSessionConfirmModalProps) => { const [stage, setStage] = useState('decision'); const [isSubmitting, setIsSubmitting] = useState(false); + const [result, setResult] = useState(null); const trimmedGoal = currentGoal.trim() || '목표 없음'; useEffect(() => { if (!open) { - setTimeout(() => setStage('decision'), 300); // Reset after close animation + setTimeout(() => { + setStage('decision'); + setResult(null); + }, 500); // Reset after close animation setIsSubmitting(false); + return; } - }, [open]); + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !isSubmitting) { + onClose(); + } + }; + + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [open, isSubmitting, onClose]); const handleFinish = async () => { if (isSubmitting) return; setIsSubmitting(true); try { - const didFinish = await onFinishHere(); - if (didFinish) onClose(); + const completionResult = await onFinishHere(); + if (completionResult) { + setResult(completionResult); + setStage('success'); // Transition to the grand finale instead of closing + } } finally { setIsSubmitting(false); } @@ -55,18 +78,24 @@ export const EndSessionConfirmModal = ({ } }; + const hasThoughts = result && result.thoughts.length > 0; + const focusedMinutes = result ? formatFocusedMinutes(result.focusedSeconds) : 0; + return (
@@ -76,16 +105,6 @@ export const EndSessionConfirmModal = ({ open ? 'pointer-events-auto translate-y-0 scale-100' : 'pointer-events-none translate-y-8 scale-95', )} > - - {stage === 'decision' && (

@@ -107,40 +126,107 @@ export const EndSessionConfirmModal = ({

+ + {/* The Subdued Cancel Action */} +
)} - {stage === 'success' && ( + {stage === 'success' && result && (
-
- + {/* Glowing Success Icon */} +
+ 🏆
-

+ +

+ Session Closed +

+ +

완벽합니다.

+

성공적으로 목표를 완수했습니다.
스스로에게 보상을 줄 시간입니다.

+ +
+ {/* Massive Time Stat */} +
+

+ Total Focus Time +

+

+ {focusedMinutes}m +

+
+ + {/* Goal Card */} +
+

+ Completed Goal +

+

+ {result.completedGoal} +

+
+ + {/* Thoughts Dumped */} + {hasThoughts ? ( +
+
+

+ Distractions Dumped +

+ + {result.thoughts.length} + +
+
+ {result.thoughts.map((thought) => ( +
+

+ {thought.text} +

+
+ ))} +
+
+ ) : null} +
+
)} @@ -164,6 +250,14 @@ export const EndSessionConfirmModal = ({ > {isSubmitting ? '저장 중...' : '저장하고 로비로 돌아가기'} +
)}
diff --git a/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx b/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx index 0f8b993..e915284 100644 --- a/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx +++ b/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx @@ -1,13 +1,14 @@ 'use client'; import { useMemo, useState } from 'react'; +import type { CompletionResult } from '@/features/focus-session'; import { copy } from '@/shared/i18n'; import { cn } from '@/shared/lib/cn'; interface GoalCompleteSheetProps { open: boolean; currentGoal: string; - onFinish: () => Promise | boolean; + onFinish: () => Promise | boolean | CompletionResult | null; onExtendTenMinutes?: () => Promise | boolean; onClose: () => void; } diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx index 7bd197a..ee19b8b 100644 --- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx +++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx @@ -1,6 +1,7 @@ import { copy } from "@/shared/i18n"; import { cn } from "@/shared/lib/cn"; import type { HudStatusLinePayload } from "@/shared/lib/useHudStatusLine"; +import type { CompletionResult } from "@/features/focus-session"; import { useEffect, useRef, useState } from "react"; import { EndSessionConfirmModal } from "./EndSessionConfirmModal"; import { GoalCompleteSheet } from "./GoalCompleteSheet"; @@ -17,13 +18,14 @@ interface SpaceFocusHudWidgetProps { hasActiveSession?: boolean; playbackState?: "running" | "paused"; sessionPhase?: "focus" | "break" | null; + completionResult?: CompletionResult | null; onIntentUpdate: (payload: { goal?: string; microStep?: string | null; }) => boolean | Promise; onGoalUpdate: (nextGoal: string) => boolean | Promise; - onGoalCompleteFinish: () => boolean | Promise; - onTimerFinish: () => boolean | Promise; + onGoalCompleteFinish: () => Promise | CompletionResult | null; + onTimerFinish: () => Promise | CompletionResult | null; onAddTenMinutes: () => boolean | Promise; onStatusMessage: (payload: HudStatusLinePayload) => void; onCaptureThought: (note: string) => void; @@ -40,6 +42,7 @@ export const SpaceFocusHudWidget = ({ hasActiveSession = false, playbackState = "paused", sessionPhase = "focus", + completionResult = null, onIntentUpdate, onGoalUpdate, onGoalCompleteFinish, @@ -66,12 +69,18 @@ export const SpaceFocusHudWidget = ({ : null; useEffect(() => { - if (!hasActiveSession) { + if (completionResult && overlay === "none") { + setOverlay("end-session"); // Show the success stage automatically if data arrives + } + }, [completionResult, overlay]); + + useEffect(() => { + if (!hasActiveSession && !completionResult) { setOverlay("none"); setSavingIntent(false); timerPromptSignatureRef.current = null; } - }, [hasActiveSession]); + }, [hasActiveSession, completionResult]); useEffect(() => { if (!visibleRef.current && playbackState === "running") { @@ -88,8 +97,8 @@ export const SpaceFocusHudWidget = ({ return; } - setOverlay("none"); - }, [overlay, timerCompletionSignature]); + if (!completionResult) setOverlay("none"); + }, [overlay, timerCompletionSignature, completionResult]); useEffect(() => { if (!timerCompletionSignature) { @@ -123,6 +132,8 @@ export const SpaceFocusHudWidget = ({ } }; + const isOverlayOpen = overlay !== "none"; + return ( <> setOverlay("none")} + onClose={() => { + if (completionResult) { + void onExitRequested(); + } else { + setOverlay("none"); + } + }} onAdvanceGoal={(nextGoal) => Promise.resolve(onGoalUpdate(nextGoal))} onFinishHere={() => Promise.resolve(onGoalCompleteFinish())} onEndSession={() => Promise.resolve(onExitRequested())} diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index 9b15d1b..e770990 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -20,7 +20,6 @@ import { } from "@/features/sound-preset"; import { useHudStatusLine } from "@/shared/lib/useHudStatusLine"; import { SpaceFocusHudWidget } from "@/widgets/space-focus-hud"; -import { CompletionResultModal } from "@/widgets/space-focus-hud/ui/CompletionResultModal"; import { findAtmosphereOptionForSelection, getRecommendedDurationMinutes, @@ -107,7 +106,6 @@ export const SpaceWorkspaceWidget = () => { completeSession, advanceGoal, } = useFocusSessionEngine(); - const isCompletionResultOpen = pendingCompletionResult !== null; const isFocusMode = workspaceMode === "focus"; const resolvedPlaybackState = currentSession?.state ?? previewPlaybackState; @@ -271,7 +269,7 @@ export const SpaceWorkspaceWidget = () => {
- {isFocusMode ? ( + {isFocusMode || pendingCompletionResult ? ( { hasActiveSession={Boolean(currentSession)} playbackState={resolvedPlaybackState} sessionPhase={phase ?? 'focus'} + completionResult={pendingCompletionResult} onIntentUpdate={controls.handleIntentUpdate} onGoalCompleteFinish={async () => { const completionResult = await controls.handleGoalComplete(); - if (completionResult) { setPendingCompletionResult(completionResult); setCurrentSessionThoughts([]); } - - return Boolean(completionResult); + return completionResult; }} onTimerFinish={async () => { const completionResult = await controls.handleTimerComplete(); - if (completionResult) { setPendingCompletionResult(completionResult); setCurrentSessionThoughts([]); } - - return Boolean(completionResult); + return completionResult; }} onAddTenMinutes={() => controls.handleExtendCurrentPhase(10)} onGoalUpdate={controls.handleGoalAdvance} @@ -328,28 +323,25 @@ export const SpaceWorkspaceWidget = () => { }); }} onExitRequested={async () => { - const completionResult = await controls.handleManualEnd(); + if (pendingCompletionResult) { + setPendingCompletionResult(null); + setCurrentSessionThoughts([]); + return true; + } + const completionResult = await controls.handleManualEnd(); if (completionResult) { setPendingCompletionResult(completionResult); setCurrentSessionThoughts([]); + } else { + // If no result (cancelled or error), still try to go back if session is gone + if (!currentSession) router.replace('/app'); } - return Boolean(completionResult); }} /> ) : null} - { - setPendingCompletionResult(null); - setCurrentSessionThoughts([]); - void router.replace('/app'); - }} - /> -