refactor(toast): HUD 상태 라인 피드백 통합 및 우선순위 큐 적용
This commit is contained in:
103
src/shared/lib/useHudStatusLine.ts
Normal file
103
src/shared/lib/useHudStatusLine.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
export type HudStatusLinePriority = 'normal' | 'undo';
|
||||||
|
|
||||||
|
export interface HudStatusLineAction {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudStatusLinePayload {
|
||||||
|
message: string;
|
||||||
|
durationMs?: number;
|
||||||
|
action?: HudStatusLineAction;
|
||||||
|
priority?: HudStatusLinePriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudStatusLineItem extends HudStatusLinePayload {
|
||||||
|
id: number;
|
||||||
|
priority: HudStatusLinePriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_PENDING_MESSAGES = 2;
|
||||||
|
const MAX_TOTAL_MESSAGES = MAX_PENDING_MESSAGES + 1;
|
||||||
|
const DEFAULT_DURATION_MS = 2000;
|
||||||
|
const DEFAULT_UNDO_DURATION_MS = 4200;
|
||||||
|
|
||||||
|
export const useHudStatusLine = (enabled = true) => {
|
||||||
|
const [queue, setQueue] = useState<HudStatusLineItem[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQueue([]);
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active = queue[0];
|
||||||
|
const durationMs =
|
||||||
|
active.durationMs ?? (active.action ? DEFAULT_UNDO_DURATION_MS : DEFAULT_DURATION_MS);
|
||||||
|
const timerId = window.setTimeout(() => {
|
||||||
|
setQueue((current) => current.slice(1));
|
||||||
|
}, durationMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timerId);
|
||||||
|
};
|
||||||
|
}, [enabled, queue]);
|
||||||
|
|
||||||
|
const pushStatusLine = useCallback((payload: HudStatusLinePayload) => {
|
||||||
|
const nextItem: HudStatusLineItem = {
|
||||||
|
id: Date.now() + Math.floor(Math.random() * 10000),
|
||||||
|
...payload,
|
||||||
|
priority: payload.priority ?? (payload.action ? 'undo' : 'normal'),
|
||||||
|
};
|
||||||
|
|
||||||
|
setQueue((current) => {
|
||||||
|
if (current.length === 0) {
|
||||||
|
return [nextItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [active, ...pending] = current;
|
||||||
|
const nextQueue =
|
||||||
|
nextItem.priority === 'undo'
|
||||||
|
? [active, nextItem, ...pending]
|
||||||
|
: [active, ...pending, nextItem];
|
||||||
|
|
||||||
|
return nextQueue.slice(0, MAX_TOTAL_MESSAGES);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dismissActiveStatus = useCallback(() => {
|
||||||
|
setQueue((current) => current.slice(1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const runActiveAction = useCallback(() => {
|
||||||
|
const active = queue[0];
|
||||||
|
|
||||||
|
if (!active?.action) {
|
||||||
|
dismissActiveStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
active.action.onClick();
|
||||||
|
dismissActiveStatus();
|
||||||
|
}, [dismissActiveStatus, queue]);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
activeStatus: queue[0] ?? null,
|
||||||
|
pushStatusLine,
|
||||||
|
runActiveAction,
|
||||||
|
dismissActiveStatus,
|
||||||
|
};
|
||||||
|
}, [dismissActiveStatus, pushStatusLine, queue, runActiveAction]);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import type { HudStatusLineItem, HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
||||||
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
|
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
|
||||||
import { useToast } from '@/shared/ui';
|
|
||||||
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
|
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
|
||||||
import { GoalCompleteSheet } from './GoalCompleteSheet';
|
import { GoalCompleteSheet } from './GoalCompleteSheet';
|
||||||
import { GoalFlashOverlay } from './GoalFlashOverlay';
|
import { GoalFlashOverlay } from './GoalFlashOverlay';
|
||||||
@@ -10,6 +10,9 @@ interface SpaceFocusHudWidgetProps {
|
|||||||
timerLabel: string;
|
timerLabel: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onGoalUpdate: (nextGoal: string) => void;
|
onGoalUpdate: (nextGoal: string) => void;
|
||||||
|
statusLine: HudStatusLineItem | null;
|
||||||
|
onStatusAction: () => void;
|
||||||
|
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaceFocusHudWidget = ({
|
export const SpaceFocusHudWidget = ({
|
||||||
@@ -17,8 +20,10 @@ export const SpaceFocusHudWidget = ({
|
|||||||
timerLabel,
|
timerLabel,
|
||||||
visible,
|
visible,
|
||||||
onGoalUpdate,
|
onGoalUpdate,
|
||||||
|
statusLine,
|
||||||
|
onStatusAction,
|
||||||
|
onStatusMessage,
|
||||||
}: SpaceFocusHudWidgetProps) => {
|
}: SpaceFocusHudWidgetProps) => {
|
||||||
const { pushToast } = useToast();
|
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
const [flashVisible, setFlashVisible] = useState(false);
|
const [flashVisible, setFlashVisible] = useState(false);
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
@@ -95,9 +100,18 @@ export const SpaceFocusHudWidget = ({
|
|||||||
<SpaceTimerHudWidget
|
<SpaceTimerHudWidget
|
||||||
timerLabel={timerLabel}
|
timerLabel={timerLabel}
|
||||||
goal={goal}
|
goal={goal}
|
||||||
|
statusLine={
|
||||||
|
statusLine
|
||||||
|
? {
|
||||||
|
message: statusLine.message,
|
||||||
|
actionLabel: statusLine.action?.label,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
isImmersionMode
|
isImmersionMode
|
||||||
className="pr-[4.2rem]"
|
className="pr-[4.2rem]"
|
||||||
onGoalCompleteRequest={handleOpenCompleteSheet}
|
onGoalCompleteRequest={handleOpenCompleteSheet}
|
||||||
|
onStatusAction={onStatusAction}
|
||||||
onPlaybackStateChange={(state) => {
|
onPlaybackStateChange={(state) => {
|
||||||
if (reducedMotion) {
|
if (reducedMotion) {
|
||||||
playbackStateRef.current = state;
|
playbackStateRef.current = state;
|
||||||
@@ -117,21 +131,21 @@ export const SpaceFocusHudWidget = ({
|
|||||||
onClose={() => setSheetOpen(false)}
|
onClose={() => setSheetOpen(false)}
|
||||||
onRest={() => {
|
onRest={() => {
|
||||||
setSheetOpen(false);
|
setSheetOpen(false);
|
||||||
pushToast({ title: '좋아요. 5분 뒤에 다시 알려드릴게요.' });
|
onStatusMessage({ message: '좋아요. 5분 뒤에 다시 알려드릴게요.' });
|
||||||
|
|
||||||
if (restReminderTimerRef.current) {
|
if (restReminderTimerRef.current) {
|
||||||
window.clearTimeout(restReminderTimerRef.current);
|
window.clearTimeout(restReminderTimerRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
restReminderTimerRef.current = window.setTimeout(() => {
|
restReminderTimerRef.current = window.setTimeout(() => {
|
||||||
pushToast({ title: '5분이 지났어요. 다음 한 조각으로 돌아와요.' });
|
onStatusMessage({ message: '5분이 지났어요. 다음 한 조각으로 돌아와요.' });
|
||||||
restReminderTimerRef.current = null;
|
restReminderTimerRef.current = null;
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
}}
|
}}
|
||||||
onConfirm={(nextGoal) => {
|
onConfirm={(nextGoal) => {
|
||||||
onGoalUpdate(nextGoal);
|
onGoalUpdate(nextGoal);
|
||||||
setSheetOpen(false);
|
setSheetOpen(false);
|
||||||
pushToast({ title: '다음 한 조각으로 이어갑니다.' });
|
onStatusMessage({ message: '다음 한 조각으로 이어갑니다.' });
|
||||||
triggerFlash(1200);
|
triggerFlash(1200);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ interface SpaceTimerHudWidgetProps {
|
|||||||
goal: string;
|
goal: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
isImmersionMode?: boolean;
|
isImmersionMode?: boolean;
|
||||||
|
statusLine?: {
|
||||||
|
message: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
} | null;
|
||||||
onPlaybackStateChange?: (state: 'running' | 'paused') => void;
|
onPlaybackStateChange?: (state: 'running' | 'paused') => void;
|
||||||
onGoalCompleteRequest?: () => void;
|
onGoalCompleteRequest?: () => void;
|
||||||
|
onStatusAction?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HUD_ACTIONS = [
|
const HUD_ACTIONS = [
|
||||||
@@ -27,8 +32,10 @@ export const SpaceTimerHudWidget = ({
|
|||||||
goal,
|
goal,
|
||||||
className,
|
className,
|
||||||
isImmersionMode = false,
|
isImmersionMode = false,
|
||||||
|
statusLine = null,
|
||||||
onPlaybackStateChange,
|
onPlaybackStateChange,
|
||||||
onGoalCompleteRequest,
|
onGoalCompleteRequest,
|
||||||
|
onStatusAction,
|
||||||
}: SpaceTimerHudWidgetProps) => {
|
}: SpaceTimerHudWidgetProps) => {
|
||||||
const { isBreatheMode, hintMessage, triggerRestart } = useRestart30s();
|
const { isBreatheMode, hintMessage, triggerRestart } = useRestart30s();
|
||||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
|
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : '이번 한 조각을 설정해 주세요.';
|
||||||
@@ -89,7 +96,22 @@ export const SpaceTimerHudWidget = ({
|
|||||||
완료
|
완료
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{hintMessage ? (
|
{statusLine ? (
|
||||||
|
<div className="mt-1 flex min-w-0 items-center gap-2">
|
||||||
|
<p className={cn('min-w-0 truncate text-[11px]', isImmersionMode ? 'text-white/68' : 'text-white/66')}>
|
||||||
|
{statusLine.message}
|
||||||
|
</p>
|
||||||
|
{statusLine.actionLabel ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onStatusAction}
|
||||||
|
className="shrink-0 text-[11px] font-medium text-white/76 underline-offset-2 transition-colors hover:text-white/92 hover:underline"
|
||||||
|
>
|
||||||
|
{statusLine.actionLabel}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : hintMessage ? (
|
||||||
<p className={cn('mt-1 truncate text-[10px]', isImmersionMode ? 'text-white/52' : 'text-white/52')}>
|
<p className={cn('mt-1 truncate text-[10px]', isImmersionMode ? 'text-white/52' : 'text-white/52')}>
|
||||||
{hintMessage}
|
{hintMessage}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -2,30 +2,30 @@ interface ApplyQuickPackParams {
|
|||||||
packId: 'balanced' | 'deep-work' | 'gentle';
|
packId: 'balanced' | 'deep-work' | 'gentle';
|
||||||
onTimerSelect: (timerLabel: string) => void;
|
onTimerSelect: (timerLabel: string) => void;
|
||||||
onSelectPreset: (presetId: string) => void;
|
onSelectPreset: (presetId: string) => void;
|
||||||
pushToast: (payload: { title: string; description?: string }) => void;
|
onApplied?: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const applyQuickPack = ({
|
export const applyQuickPack = ({
|
||||||
packId,
|
packId,
|
||||||
onTimerSelect,
|
onTimerSelect,
|
||||||
onSelectPreset,
|
onSelectPreset,
|
||||||
pushToast,
|
onApplied,
|
||||||
}: ApplyQuickPackParams) => {
|
}: ApplyQuickPackParams) => {
|
||||||
if (packId === 'balanced') {
|
if (packId === 'balanced') {
|
||||||
onTimerSelect('25/5');
|
onTimerSelect('25/5');
|
||||||
onSelectPreset('rain-focus');
|
onSelectPreset('rain-focus');
|
||||||
pushToast({ title: 'Balanced 팩을 적용했어요.' });
|
onApplied?.('Balanced 팩을 적용했어요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packId === 'deep-work') {
|
if (packId === 'deep-work') {
|
||||||
onTimerSelect('50/10');
|
onTimerSelect('50/10');
|
||||||
onSelectPreset('deep-white');
|
onSelectPreset('deep-white');
|
||||||
pushToast({ title: 'Deep Work 팩을 적용했어요.' });
|
onApplied?.('Deep Work 팩을 적용했어요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onTimerSelect('25/5');
|
onTimerSelect('25/5');
|
||||||
onSelectPreset('silent');
|
onSelectPreset('silent');
|
||||||
pushToast({ title: 'Gentle 팩을 적용했어요.' });
|
onApplied?.('Gentle 팩을 적용했어요.');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/
|
|||||||
import { ExitHoldButton } from '@/features/exit-hold';
|
import { ExitHoldButton } from '@/features/exit-hold';
|
||||||
import { ManagePlanSheetContent, PaywallSheetContent } from '@/features/paywall-sheet';
|
import { ManagePlanSheetContent, PaywallSheetContent } from '@/features/paywall-sheet';
|
||||||
import { PlanPill } from '@/features/plan-pill';
|
import { PlanPill } from '@/features/plan-pill';
|
||||||
|
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
|
||||||
import { useToast } from '@/shared/ui';
|
import { useToast } from '@/shared/ui';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
import { ControlCenterSheetWidget } from '@/widgets/control-center-sheet';
|
import { ControlCenterSheetWidget } from '@/widgets/control-center-sheet';
|
||||||
@@ -40,6 +41,7 @@ interface SpaceToolsDockWidgetProps {
|
|||||||
onRestoreThought: (thought: RecentThought) => void;
|
onRestoreThought: (thought: RecentThought) => void;
|
||||||
onRestoreThoughts: (thoughts: RecentThought[]) => void;
|
onRestoreThoughts: (thoughts: RecentThought[]) => void;
|
||||||
onClearInbox: () => RecentThought[];
|
onClearInbox: () => RecentThought[];
|
||||||
|
onStatusMessage: (payload: HudStatusLinePayload) => void;
|
||||||
onExitRequested: () => void;
|
onExitRequested: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ export const SpaceToolsDockWidget = ({
|
|||||||
onRestoreThought,
|
onRestoreThought,
|
||||||
onRestoreThoughts,
|
onRestoreThoughts,
|
||||||
onClearInbox,
|
onClearInbox,
|
||||||
|
onStatusMessage,
|
||||||
onExitRequested,
|
onExitRequested,
|
||||||
}: SpaceToolsDockWidgetProps) => {
|
}: SpaceToolsDockWidgetProps) => {
|
||||||
const { pushToast } = useToast();
|
const { pushToast } = useToast();
|
||||||
@@ -171,9 +174,10 @@ export const SpaceToolsDockWidget = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setNoteDraft('');
|
setNoteDraft('');
|
||||||
pushToast({
|
onStatusMessage({
|
||||||
title: '인박스에 저장됨',
|
message: '인박스에 저장됨',
|
||||||
durationMs: 4200,
|
durationMs: 4200,
|
||||||
|
priority: 'undo',
|
||||||
action: {
|
action: {
|
||||||
label: '실행취소',
|
label: '실행취소',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -183,7 +187,7 @@ export const SpaceToolsDockWidget = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToast({ title: '저장 취소됨' });
|
onStatusMessage({ message: '저장 취소됨' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -198,14 +202,15 @@ export const SpaceToolsDockWidget = ({
|
|||||||
|
|
||||||
const willBeCompleted = !thought.isCompleted;
|
const willBeCompleted = !thought.isCompleted;
|
||||||
|
|
||||||
pushToast({
|
onStatusMessage({
|
||||||
title: willBeCompleted ? '완료 처리됨' : '완료 해제됨',
|
message: willBeCompleted ? '완료 처리됨' : '완료 해제됨',
|
||||||
durationMs: 4200,
|
durationMs: 4200,
|
||||||
|
priority: 'undo',
|
||||||
action: {
|
action: {
|
||||||
label: '실행취소',
|
label: '실행취소',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onRestoreThought(previousThought);
|
onRestoreThought(previousThought);
|
||||||
pushToast({ title: willBeCompleted ? '완료 처리를 취소했어요.' : '완료 해제를 취소했어요.' });
|
onStatusMessage({ message: willBeCompleted ? '완료 처리를 취소했어요.' : '완료 해제를 취소했어요.' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -218,14 +223,15 @@ export const SpaceToolsDockWidget = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToast({
|
onStatusMessage({
|
||||||
title: '삭제됨',
|
message: '삭제됨',
|
||||||
durationMs: 4200,
|
durationMs: 4200,
|
||||||
|
priority: 'undo',
|
||||||
action: {
|
action: {
|
||||||
label: '실행취소',
|
label: '실행취소',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onRestoreThought(removedThought);
|
onRestoreThought(removedThought);
|
||||||
pushToast({ title: '삭제를 취소했어요.' });
|
onStatusMessage({ message: '삭제를 취소했어요.' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -235,18 +241,19 @@ export const SpaceToolsDockWidget = ({
|
|||||||
const snapshot = onClearInbox();
|
const snapshot = onClearInbox();
|
||||||
|
|
||||||
if (snapshot.length === 0) {
|
if (snapshot.length === 0) {
|
||||||
pushToast({ title: '인박스가 비어 있어요.' });
|
onStatusMessage({ message: '인박스가 비어 있어요.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToast({
|
onStatusMessage({
|
||||||
title: '모두 비워짐',
|
message: '모두 비워짐',
|
||||||
durationMs: 4200,
|
durationMs: 4200,
|
||||||
|
priority: 'undo',
|
||||||
action: {
|
action: {
|
||||||
label: '실행취소',
|
label: '실행취소',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onRestoreThoughts(snapshot);
|
onRestoreThoughts(snapshot);
|
||||||
pushToast({ title: '인박스를 복구했어요.' });
|
onStatusMessage({ message: '인박스를 복구했어요.' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -273,7 +280,14 @@ export const SpaceToolsDockWidget = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleApplyPack = (packId: 'balanced' | 'deep-work' | 'gentle') =>
|
const handleApplyPack = (packId: 'balanced' | 'deep-work' | 'gentle') =>
|
||||||
applyQuickPack({ packId, onTimerSelect, onSelectPreset, pushToast });
|
applyQuickPack({
|
||||||
|
packId,
|
||||||
|
onTimerSelect,
|
||||||
|
onSelectPreset,
|
||||||
|
onApplied: (message) => {
|
||||||
|
onStatusMessage({ message, durationMs: 1200 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const showVolumeFeedback = (nextVolume: number) => {
|
const showVolumeFeedback = (nextVolume: number) => {
|
||||||
setVolumeFeedback(`${nextVolume}%`);
|
setVolumeFeedback(`${nextVolume}%`);
|
||||||
@@ -413,7 +427,6 @@ export const SpaceToolsDockWidget = ({
|
|||||||
onVolumeKeyDown={handleVolumeKeyDown}
|
onVolumeKeyDown={handleVolumeKeyDown}
|
||||||
onSelectPreset={(presetId) => {
|
onSelectPreset={(presetId) => {
|
||||||
onSelectPreset(presetId);
|
onSelectPreset(presetId);
|
||||||
pushToast({ title: '사운드 변경(더미)' });
|
|
||||||
setOpenPopover(null);
|
setOpenPopover(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -447,15 +460,12 @@ export const SpaceToolsDockWidget = ({
|
|||||||
soundPresets={SOUND_PRESETS}
|
soundPresets={SOUND_PRESETS}
|
||||||
onSelectRoom={(roomId) => {
|
onSelectRoom={(roomId) => {
|
||||||
onRoomSelect(roomId);
|
onRoomSelect(roomId);
|
||||||
pushToast({ title: '공간을 바꿨어요.' });
|
|
||||||
}}
|
}}
|
||||||
onSelectTimer={(label) => {
|
onSelectTimer={(label) => {
|
||||||
onTimerSelect(label);
|
onTimerSelect(label);
|
||||||
pushToast({ title: `타이머를 ${label}로 바꿨어요.` });
|
|
||||||
}}
|
}}
|
||||||
onSelectSound={(presetId) => {
|
onSelectSound={(presetId) => {
|
||||||
onSelectPreset(presetId);
|
onSelectPreset(presetId);
|
||||||
pushToast({ title: '사운드를 바꿨어요.' });
|
|
||||||
}}
|
}}
|
||||||
onApplyPack={handleApplyPack}
|
onApplyPack={handleApplyPack}
|
||||||
onLockedClick={handleLockedClick}
|
onLockedClick={handleLockedClick}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type TimerPreset,
|
type TimerPreset,
|
||||||
} from '@/entities/session';
|
} from '@/entities/session';
|
||||||
import { useSoundPresetSelection } from '@/features/sound-preset';
|
import { useSoundPresetSelection } from '@/features/sound-preset';
|
||||||
|
import { useHudStatusLine } from '@/shared/lib/useHudStatusLine';
|
||||||
import { useToast } from '@/shared/ui';
|
import { useToast } from '@/shared/ui';
|
||||||
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
import { SpaceFocusHudWidget } from '@/widgets/space-focus-hud';
|
||||||
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
||||||
@@ -102,6 +103,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
|
|
||||||
const canStart = goalInput.trim().length > 0;
|
const canStart = goalInput.trim().length > 0;
|
||||||
const isFocusMode = workspaceMode === 'focus';
|
const isFocusMode = workspaceMode === 'focus';
|
||||||
|
const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode);
|
||||||
|
|
||||||
const handleGoalChipSelect = (chip: GoalChip) => {
|
const handleGoalChipSelect = (chip: GoalChip) => {
|
||||||
setSelectedGoalId(chip.id);
|
setSelectedGoalId(chip.id);
|
||||||
@@ -122,9 +124,8 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setWorkspaceMode('focus');
|
setWorkspaceMode('focus');
|
||||||
|
pushStatusLine({
|
||||||
pushToast({
|
message: `목표: ${goalInput.trim()} 시작해요.`,
|
||||||
title: `목표: ${goalInput.trim()} 시작해요.`,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,6 +183,9 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
goal={goalInput.trim()}
|
goal={goalInput.trim()}
|
||||||
timerLabel={selectedTimerLabel}
|
timerLabel={selectedTimerLabel}
|
||||||
visible={isFocusMode}
|
visible={isFocusMode}
|
||||||
|
statusLine={activeStatus}
|
||||||
|
onStatusAction={runActiveAction}
|
||||||
|
onStatusMessage={pushStatusLine}
|
||||||
onGoalUpdate={(nextGoal) => {
|
onGoalUpdate={(nextGoal) => {
|
||||||
setGoalInput(nextGoal);
|
setGoalInput(nextGoal);
|
||||||
setSelectedGoalId(null);
|
setSelectedGoalId(null);
|
||||||
@@ -210,6 +214,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
onRestoreThought={restoreThought}
|
onRestoreThought={restoreThought}
|
||||||
onRestoreThoughts={restoreThoughts}
|
onRestoreThoughts={restoreThoughts}
|
||||||
onClearInbox={clearThoughts}
|
onClearInbox={clearThoughts}
|
||||||
|
onStatusMessage={pushStatusLine}
|
||||||
onExitRequested={handleExitRequested}
|
onExitRequested={handleExitRequested}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user