feat(goal): Focus HUD 목표 가독성·Flash 상기·완료 액션 추가

This commit is contained in:
2026-03-04 15:49:42 +09:00
parent 96b6c0cb8f
commit b38455bf56
3 changed files with 139 additions and 13 deletions

View File

@@ -1,4 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
import { GoalFlashOverlay } from './GoalFlashOverlay';
interface SpaceFocusHudWidgetProps {
goal: string;
@@ -11,16 +14,85 @@ export const SpaceFocusHudWidget = ({
timerLabel,
visible,
}: SpaceFocusHudWidgetProps) => {
const reducedMotion = useReducedMotion();
const [flashVisible, setFlashVisible] = useState(false);
const playbackStateRef = useRef<'running' | 'paused'>('running');
const flashTimerRef = useRef<number | null>(null);
const triggerFlash = useCallback((durationMs: number) => {
if (reducedMotion || !visible) {
return;
}
setFlashVisible(true);
if (flashTimerRef.current) {
window.clearTimeout(flashTimerRef.current);
}
flashTimerRef.current = window.setTimeout(() => {
setFlashVisible(false);
flashTimerRef.current = null;
}, durationMs);
}, [reducedMotion, visible]);
useEffect(() => {
return () => {
if (flashTimerRef.current) {
window.clearTimeout(flashTimerRef.current);
flashTimerRef.current = null;
}
};
}, []);
useEffect(() => {
if (!visible || reducedMotion) {
setFlashVisible(false);
return;
}
triggerFlash(2000);
}, [visible, reducedMotion, triggerFlash]);
useEffect(() => {
if (!visible || reducedMotion) {
return;
}
const intervalId = window.setInterval(() => {
triggerFlash(800);
}, 5 * 60 * 1000);
return () => {
window.clearInterval(intervalId);
};
}, [visible, reducedMotion, triggerFlash]);
if (!visible) {
return null;
}
return (
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}
isImmersionMode
className="pr-[4.2rem]"
/>
<>
<GoalFlashOverlay goal={goal} visible={flashVisible} />
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}
isImmersionMode
className="pr-[4.2rem]"
onPlaybackStateChange={(state) => {
if (reducedMotion) {
playbackStateRef.current = state;
return;
}
if (playbackStateRef.current === 'paused' && state === 'running') {
triggerFlash(1000);
}
playbackStateRef.current = state;
}}
/>
</>
);
};