Files
viberoom-web/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx
corpi 3fbeee059a refactor(focus): 몰입모드 토글 제거 및 Focus-First 기본 구조로 전환
맥락:
- 몰입모드 토글은 상태 인지 비용을 높여 집중 흐름을 끊고, Quick Controls 헤더를 대시보드형으로 보이게 만들고 있었습니다.

변경사항:
- Quick Controls에서 기본/몰입 모드 토글 UI를 완전히 제거했습니다.
- Focus 화면의 HUD 톤은 외부 모드 상태 없이 항상 몰입 톤으로 렌더링되도록 고정했습니다.
- workspace/tools-dock/focus-hud 간 모드 토글 상태 전달 경로를 정리해, 컨트롤은 패널을 열었을 때만 보이는 Focus-First 흐름으로 단순화했습니다.

검증:
- npx tsc --noEmit

세션-상태: 모드 토글 없이 패널 열림 상태만으로 컨트롤 노출이 정의됩니다.
세션-다음: (선택) 컨트롤 자동 숨김 표시 정책 옵션을 패널 내부에 추가합니다.
세션-리스크: 자동 숨김 정책이 아직 없어 패널을 열어둔 채 방치되는 경우 수동 닫기가 필요합니다.
2026-03-05 15:12:00 +09:00

153 lines
4.1 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import type { HudStatusLineItem, 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;
timerLabel: string;
visible: boolean;
onGoalUpdate: (nextGoal: string) => void;
statusLine: HudStatusLineItem | null;
onStatusAction: () => void;
onStatusMessage: (payload: HudStatusLinePayload) => void;
}
export const SpaceFocusHudWidget = ({
goal,
timerLabel,
visible,
onGoalUpdate,
statusLine,
onStatusAction,
onStatusMessage,
}: SpaceFocusHudWidgetProps) => {
const reducedMotion = useReducedMotion();
const [flashVisible, setFlashVisible] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const playbackStateRef = useRef<'running' | 'paused'>('running');
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]);
useEffect(() => {
return () => {
if (flashTimerRef.current) {
window.clearTimeout(flashTimerRef.current);
flashTimerRef.current = null;
}
if (restReminderTimerRef.current) {
window.clearTimeout(restReminderTimerRef.current);
restReminderTimerRef.current = null;
}
};
}, []);
useEffect(() => {
if (!visible || reducedMotion) {
setFlashVisible(false);
return;
}
triggerFlash(2000);
}, [visible, reducedMotion, triggerFlash]);
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]);
if (!visible) {
return null;
}
const handleOpenCompleteSheet = () => {
setSheetOpen(true);
};
return (
<>
<GoalFlashOverlay goal={goal} visible={flashVisible} />
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}
statusLine={
statusLine
? {
message: statusLine.message,
actionLabel: statusLine.action?.label,
}
: null
}
isImmersionMode
className="pr-[4.2rem]"
onGoalCompleteRequest={handleOpenCompleteSheet}
onStatusAction={onStatusAction}
onPlaybackStateChange={(state) => {
if (reducedMotion) {
playbackStateRef.current = state;
return;
}
if (playbackStateRef.current === 'paused' && state === 'running') {
triggerFlash(1000);
}
playbackStateRef.current = state;
}}
/>
<GoalCompleteSheet
open={sheetOpen}
currentGoal={goal}
onClose={() => setSheetOpen(false)}
onRest={() => {
setSheetOpen(false);
if (restReminderTimerRef.current) {
window.clearTimeout(restReminderTimerRef.current);
}
restReminderTimerRef.current = window.setTimeout(() => {
onStatusMessage({ message: '5분이 지났어요. 다음 한 조각으로 돌아와요.' });
restReminderTimerRef.current = null;
}, 5 * 60 * 1000);
}}
onConfirm={(nextGoal) => {
onGoalUpdate(nextGoal);
setSheetOpen(false);
triggerFlash(1200);
}}
/>
</>
);
};