diff --git a/src/widgets/space-focus-hud/ui/GoalFlashOverlay.tsx b/src/widgets/space-focus-hud/ui/GoalFlashOverlay.tsx
new file mode 100644
index 0000000..02e008f
--- /dev/null
+++ b/src/widgets/space-focus-hud/ui/GoalFlashOverlay.tsx
@@ -0,0 +1,25 @@
+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 (
+
+
+ 이번 한 조각: {normalizedGoal}
+
+
+ );
+};
diff --git a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx
index dba47be..0ea056c 100644
--- a/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx
+++ b/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx
@@ -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(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 (
-
+ <>
+
+ {
+ if (reducedMotion) {
+ playbackStateRef.current = state;
+ return;
+ }
+
+ if (playbackStateRef.current === 'paused' && state === 'running') {
+ triggerFlash(1000);
+ }
+
+ playbackStateRef.current = state;
+ }}
+ />
+ >
);
};
diff --git a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx
index 5a88c75..648fd82 100644
--- a/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx
+++ b/src/widgets/space-timer-hud/ui/SpaceTimerHudWidget.tsx
@@ -1,6 +1,7 @@
'use client';
import { cn } from '@/shared/lib/cn';
+import { useToast } from '@/shared/ui';
import {
RECOVERY_30S_MODE_LABEL,
Restart30sAction,
@@ -12,6 +13,7 @@ interface SpaceTimerHudWidgetProps {
goal: string;
className?: string;
isImmersionMode?: boolean;
+ onPlaybackStateChange?: (state: 'running' | 'paused') => void;
}
const HUD_ACTIONS = [
@@ -25,8 +27,11 @@ export const SpaceTimerHudWidget = ({
goal,
className,
isImmersionMode = false,
+ onPlaybackStateChange,
}: SpaceTimerHudWidgetProps) => {
+ const { pushToast } = useToast();
const { isBreatheMode, hintMessage, triggerRestart } = useRestart30s();
+ const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
return (
+
+
+
Goal
+
+
+
+ {normalizedGoal}
+
+
{hintMessage ? (
-
+
{hintMessage}
- ) : (
-
- 목표: {goal}
-
- )}
+ ) : null}
@@ -89,6 +109,15 @@ export const SpaceTimerHudWidget = ({
key={action.id}
type="button"
title={action.label}
+ onClick={() => {
+ if (action.id === 'start') {
+ onPlaybackStateChange?.('running');
+ }
+
+ if (action.id === 'pause') {
+ onPlaybackStateChange?.('paused');
+ }
+ }}
className={cn(
'inline-flex h-8 w-8 items-center justify-center rounded-full border text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-200/80',
isImmersionMode