fix(space-focus): 목표 안내를 상단 토스트로 통합

맥락:
- space 진입 직후 목표를 확정하면 하단 HUD 위에 GoalFlashOverlay가 노출되어 상단 토스트 흐름과 UI 기준점이 분리되어 있었다.

변경사항:
- GoalFlashOverlay 컴포넌트를 제거했다.
- SpaceFocusHudWidget에서 집중 진입 시점과 paused -> running 복귀 시점의 목표 안내를 onStatusMessage로 올리도록 변경했다.
- 목표 안내가 기존 FocusTopToast를 통해 상단에서 일관되게 보이도록 정리했다.

검증:
- npm run lint

세션-상태: 목표 안내가 하단 오버레이 없이 상단 토스트만 사용하는 상태
세션-다음: 실제 사운드/배경 적용 시 상단 상태 메시지 우선순위를 함께 점검
세션-리스크: 연속 상태 메시지가 짧은 간격으로 발생하면 토스트 큐 길이에 따라 일부 메시지가 뒤로 밀릴 수 있음
This commit is contained in:
2026-03-07 18:37:05 +09:00
parent d18d9b2bb9
commit 8184915cb1
2 changed files with 13 additions and 95 deletions

View File

@@ -1,25 +0,0 @@
import { cn } from '@/shared/lib/cn';
interface GoalFlashOverlayProps {
goal: string;
visible: boolean;
}
export const GoalFlashOverlay = ({ goal, visible }: GoalFlashOverlayProps) => {
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 잊지 마세요.';
return (
<div
className={cn(
'pointer-events-none fixed inset-x-0 z-20 flex justify-center px-4 transition-opacity duration-[240ms] ease-out motion-reduce:duration-0',
visible ? 'opacity-100' : 'opacity-0',
)}
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.1rem)' }}
aria-hidden
>
<div className="rounded-full border border-white/12 bg-black/24 px-3 py-1.5 text-xs text-white/82 backdrop-blur-md">
: <span className="text-white/92">{normalizedGoal}</span>
</div>
</div>
);
};

View File

@@ -1,9 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
import { GoalCompleteSheet } from './GoalCompleteSheet';
import { GoalFlashOverlay } from './GoalFlashOverlay';
interface SpaceFocusHudWidgetProps {
goal: string;
@@ -34,36 +32,14 @@ export const SpaceFocusHudWidget = ({
onGoalUpdate,
onStatusMessage,
}: SpaceFocusHudWidgetProps) => {
const reducedMotion = useReducedMotion();
const [flashVisible, setFlashVisible] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const visibleRef = useRef(false);
const playbackStateRef = useRef<'running' | 'paused'>(playbackState);
const flashTimerRef = useRef<number | null>(null);
const restReminderTimerRef = 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]);
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '집중을 시작해요.';
useEffect(() => {
return () => {
if (flashTimerRef.current) {
window.clearTimeout(flashTimerRef.current);
flashTimerRef.current = null;
}
if (restReminderTimerRef.current) {
window.clearTimeout(restReminderTimerRef.current);
restReminderTimerRef.current = null;
@@ -72,56 +48,24 @@ export const SpaceFocusHudWidget = ({
}, []);
useEffect(() => {
if (!visible || reducedMotion) {
const rafId = window.requestAnimationFrame(() => {
setFlashVisible(false);
if (visible && !visibleRef.current) {
onStatusMessage({
message: `이번 한 조각 · ${normalizedGoal}`,
});
return () => {
window.cancelAnimationFrame(rafId);
};
}
const timeoutId = window.setTimeout(() => {
triggerFlash(2000);
}, 0);
return () => {
window.clearTimeout(timeoutId);
};
}, [visible, reducedMotion, triggerFlash]);
visibleRef.current = visible;
}, [normalizedGoal, onStatusMessage, visible]);
useEffect(() => {
if (playbackStateRef.current === 'paused' && playbackState === 'running' && visible && !reducedMotion) {
const timeoutId = window.setTimeout(() => {
triggerFlash(1000);
}, 0);
playbackStateRef.current = playbackState;
return () => {
window.clearTimeout(timeoutId);
};
if (playbackStateRef.current === 'paused' && playbackState === 'running' && visible) {
onStatusMessage({
message: `이번 한 조각 · ${normalizedGoal}`,
});
}
playbackStateRef.current = playbackState;
}, [playbackState, reducedMotion, triggerFlash, visible]);
useEffect(() => {
const ENABLE_PERIODIC_FLASH = false;
if (!visible || reducedMotion || !ENABLE_PERIODIC_FLASH) {
return;
}
const intervalId = window.setInterval(() => {
triggerFlash(800);
}, 10 * 60 * 1000);
return () => {
window.clearInterval(intervalId);
};
}, [visible, reducedMotion, triggerFlash]);
}, [normalizedGoal, onStatusMessage, playbackState, visible]);
if (!visible) {
return null;
@@ -133,7 +77,6 @@ export const SpaceFocusHudWidget = ({
return (
<>
<GoalFlashOverlay goal={goal} visible={flashVisible} />
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}