diff --git a/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx b/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx new file mode 100644 index 0000000..3b1edfc --- /dev/null +++ b/src/widgets/space-focus-hud/ui/GoalCompleteSheet.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { cn } from '@/shared/lib/cn'; + +interface GoalCompleteSheetProps { + open: boolean; + currentGoal: string; + onConfirm: (nextGoal: string) => void; + onRest: () => void; + onClose: () => void; +} + +const GOAL_SUGGESTIONS = [ + '리뷰 코멘트 2개 처리', + '문서 1문단 다듬기', + '이슈 1개 정리', + '메일 2개 회신', +]; + +export const GoalCompleteSheet = ({ + open, + currentGoal, + onConfirm, + onRest, + onClose, +}: GoalCompleteSheetProps) => { + const inputRef = useRef(null); + const [draft, setDraft] = useState(''); + + useEffect(() => { + if (!open) { + setDraft(''); + return; + } + + const rafId = window.requestAnimationFrame(() => { + inputRef.current?.focus(); + }); + + return () => { + window.cancelAnimationFrame(rafId); + }; + }, [open]); + + const placeholder = useMemo(() => { + const trimmed = currentGoal.trim(); + + if (!trimmed) { + return '다음 한 조각을 적어보세요'; + } + + return `예: ${trimmed}`; + }, [currentGoal]); + + const canConfirm = draft.trim().length > 0; + + return ( +
+
+
+
+

좋아요. 다음 한 조각은?

+

너무 크게 잡지 말고, 바로 다음 한 조각만.

+
+ +
+ +
+ setDraft(event.target.value)} + placeholder={placeholder} + className="h-9 w-full rounded-xl border border-white/14 bg-white/[0.04] px-3 text-sm text-white placeholder:text-white/40 focus:border-sky-200/42 focus:outline-none" + /> + +
+ {GOAL_SUGGESTIONS.map((suggestion) => ( + + ))} +
+
+ +
+ + +
+
+
+ ); +}; diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx index 0ea056c..9626c71 100644 --- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx +++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx @@ -1,23 +1,32 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useReducedMotion } from '@/shared/lib/useReducedMotion'; +import { cn } from '@/shared/lib/cn'; +import { useToast } from '@/shared/ui'; import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud'; +import { GoalCompleteSheet } from './GoalCompleteSheet'; import { GoalFlashOverlay } from './GoalFlashOverlay'; interface SpaceFocusHudWidgetProps { goal: string; timerLabel: string; visible: boolean; + onGoalUpdate: (nextGoal: string) => void; } export const SpaceFocusHudWidget = ({ goal, timerLabel, visible, + onGoalUpdate, }: SpaceFocusHudWidgetProps) => { + const { pushToast } = useToast(); const reducedMotion = useReducedMotion(); const [flashVisible, setFlashVisible] = useState(false); + const [sheetOpen, setSheetOpen] = useState(false); + const [completePulseVisible, setCompletePulseVisible] = useState(false); const playbackStateRef = useRef<'running' | 'paused'>('running'); const flashTimerRef = useRef(null); + const completePulseTimerRef = useRef(null); const triggerFlash = useCallback((durationMs: number) => { if (reducedMotion || !visible) { @@ -42,6 +51,10 @@ export const SpaceFocusHudWidget = ({ window.clearTimeout(flashTimerRef.current); flashTimerRef.current = null; } + if (completePulseTimerRef.current) { + window.clearTimeout(completePulseTimerRef.current); + completePulseTimerRef.current = null; + } }; }, []); @@ -55,13 +68,15 @@ export const SpaceFocusHudWidget = ({ }, [visible, reducedMotion, triggerFlash]); useEffect(() => { - if (!visible || reducedMotion) { + const ENABLE_PERIODIC_FLASH = false; + + if (!visible || reducedMotion || !ENABLE_PERIODIC_FLASH) { return; } const intervalId = window.setInterval(() => { triggerFlash(800); - }, 5 * 60 * 1000); + }, 10 * 60 * 1000); return () => { window.clearInterval(intervalId); @@ -72,14 +87,41 @@ export const SpaceFocusHudWidget = ({ return null; } + const handleOpenCompleteSheet = () => { + setCompletePulseVisible(true); + + if (completePulseTimerRef.current) { + window.clearTimeout(completePulseTimerRef.current); + } + + completePulseTimerRef.current = window.setTimeout(() => { + setCompletePulseVisible(false); + setSheetOpen(true); + completePulseTimerRef.current = null; + }, 700); + }; + return ( <> +
+
+ 완료! +
+
{ if (reducedMotion) { playbackStateRef.current = state; @@ -93,6 +135,21 @@ export const SpaceFocusHudWidget = ({ playbackStateRef.current = state; }} /> + setSheetOpen(false)} + onRest={() => { + setSheetOpen(false); + pushToast({ title: '좋아요, 잠깐 쉬고 다시 이어가요.' }); + }} + onConfirm={(nextGoal) => { + onGoalUpdate(nextGoal); + setSheetOpen(false); + pushToast({ title: '다음 한 조각으로 이어갑니다.' }); + triggerFlash(1200); + }} + /> ); }; diff --git a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx index 648fd82..4da6cdb 100644 --- a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx +++ b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx @@ -1,7 +1,6 @@ 'use client'; import { cn } from '@/shared/lib/cn'; -import { useToast } from '@/shared/ui'; import { RECOVERY_30S_MODE_LABEL, Restart30sAction, @@ -14,6 +13,7 @@ interface SpaceTimerHudWidgetProps { className?: string; isImmersionMode?: boolean; onPlaybackStateChange?: (state: 'running' | 'paused') => void; + onGoalCompleteRequest?: () => void; } const HUD_ACTIONS = [ @@ -28,8 +28,8 @@ export const SpaceTimerHudWidget = ({ className, isImmersionMode = false, onPlaybackStateChange, + onGoalCompleteRequest, }: SpaceTimerHudWidgetProps) => { - const { pushToast } = useToast(); const { isBreatheMode, hintMessage, triggerRestart } = useRestart30s(); const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.'; @@ -76,24 +76,18 @@ export const SpaceTimerHudWidget = ({ {timerLabel} -
-
-

Goal

- -
-

- {normalizedGoal} +

+

+ 이번 한 조각 · + {normalizedGoal}

+
{hintMessage ? (

diff --git a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx index 3608765..ccc9bfc 100644 --- a/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx +++ b/src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx @@ -182,6 +182,10 @@ export const SpaceWorkspaceWidget = () => { goal={goalInput.trim()} timerLabel={selectedTimerLabel} visible={isFocusMode} + onGoalUpdate={(nextGoal) => { + setGoalInput(nextGoal); + setSelectedGoalId(null); + }} />