feat(curation): Scene 추천 자동 적용과 override 존중 규칙 도입
맥락: - 목표 입력 후 바로 시작할 수 있도록 Scene 기반 추천값을 자동으로 채우되, 사용자가 직접 바꾼 값은 유지해야 했습니다. 변경사항: - RoomTheme에 recommendedSoundPresetId, recommendedTimerPresetId 필드를 추가하고 각 Scene 더미 데이터에 추천 preset id를 매핑했습니다. - /space 초기 진입 시 선택된 Scene 추천값으로 타이머/사운드 기본값이 설정되도록 초기화 로직을 정리했습니다. - /space 상태에 override.sound/override.timer 플래그를 추가하고, Scene 변경 시 override가 false인 항목만 자동 동기화하도록 반영했습니다. - 추천 복원 액션(추천으로 되돌림)을 위한 핸들러/props 경로를 workspace -> tools-dock -> control-center까지 연결했습니다. 검증: - npx tsc --noEmit 세션-상태: Scene 추천 자동 적용과 override 기반 자동 동기화가 동작합니다. 세션-다음: Control Center를 Scene/Time 중심 구조로 단순화하고 추천 정보/되돌리기 UI를 적용합니다. 세션-리스크: Control Center UI가 아직 기존 구조(Sound/Pack 포함)라 다음 커밋에서 정리가 필요합니다.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
getRoomBackgroundStyle,
|
||||
@@ -22,6 +22,10 @@ import { SpaceSetupDrawerWidget } from '@/widgets/space-setup-drawer';
|
||||
import { SpaceToolsDockWidget } from '@/widgets/space-tools-dock';
|
||||
|
||||
type WorkspaceMode = 'setup' | 'focus';
|
||||
type SelectionOverride = {
|
||||
sound: boolean;
|
||||
timer: boolean;
|
||||
};
|
||||
|
||||
const resolveInitialRoomId = (roomIdFromQuery: string | null) => {
|
||||
if (roomIdFromQuery && getRoomById(roomIdFromQuery)) {
|
||||
@@ -31,11 +35,15 @@ const resolveInitialRoomId = (roomIdFromQuery: string | null) => {
|
||||
return ROOM_THEMES[0].id;
|
||||
};
|
||||
|
||||
const resolveInitialSoundPreset = (presetIdFromQuery: string | null) => {
|
||||
const resolveInitialSoundPreset = (presetIdFromQuery: string | null, recommendedPresetId?: string) => {
|
||||
if (presetIdFromQuery && SOUND_PRESETS.some((preset) => preset.id === presetIdFromQuery)) {
|
||||
return presetIdFromQuery;
|
||||
}
|
||||
|
||||
if (recommendedPresetId && SOUND_PRESETS.some((preset) => preset.id === recommendedPresetId)) {
|
||||
return recommendedPresetId;
|
||||
}
|
||||
|
||||
return SOUND_PRESETS[0].id;
|
||||
};
|
||||
|
||||
@@ -44,11 +52,31 @@ const TIMER_SELECTION_PRESETS = TIMER_PRESETS.filter(
|
||||
typeof preset.focusMinutes === 'number' && typeof preset.breakMinutes === 'number',
|
||||
).slice(0, 3);
|
||||
|
||||
const resolveInitialTimerLabel = (timerLabelFromQuery: string | null) => {
|
||||
const resolveTimerLabelFromPresetId = (presetId?: string) => {
|
||||
if (!presetId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const preset = TIMER_SELECTION_PRESETS.find((candidate) => candidate.id === presetId);
|
||||
|
||||
if (!preset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preset.label;
|
||||
};
|
||||
|
||||
const resolveInitialTimerLabel = (timerLabelFromQuery: string | null, recommendedPresetId?: string) => {
|
||||
if (timerLabelFromQuery && TIMER_SELECTION_PRESETS.some((preset) => preset.label === timerLabelFromQuery)) {
|
||||
return timerLabelFromQuery;
|
||||
}
|
||||
|
||||
const recommendedLabel = resolveTimerLabelFromPresetId(recommendedPresetId);
|
||||
|
||||
if (recommendedLabel) {
|
||||
return recommendedLabel;
|
||||
}
|
||||
|
||||
return TIMER_SELECTION_PRESETS[0]?.label ?? '25/5';
|
||||
};
|
||||
|
||||
@@ -65,15 +93,23 @@ export const SpaceWorkspaceWidget = () => {
|
||||
} = useThoughtInbox();
|
||||
|
||||
const initialRoomId = resolveInitialRoomId(searchParams.get('room'));
|
||||
const initialRoom = getRoomById(initialRoomId) ?? ROOM_THEMES[0];
|
||||
const initialGoal = searchParams.get('goal')?.trim() ?? '';
|
||||
const initialSoundPresetId = resolveInitialSoundPreset(searchParams.get('sound'));
|
||||
const initialTimerLabel = resolveInitialTimerLabel(searchParams.get('timer'));
|
||||
const initialSoundPresetId = resolveInitialSoundPreset(
|
||||
searchParams.get('sound'),
|
||||
initialRoom.recommendedSoundPresetId,
|
||||
);
|
||||
const initialTimerLabel = resolveInitialTimerLabel(
|
||||
searchParams.get('timer'),
|
||||
initialRoom.recommendedTimerPresetId,
|
||||
);
|
||||
|
||||
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('setup');
|
||||
const [selectedRoomId, setSelectedRoomId] = useState(initialRoomId);
|
||||
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
|
||||
const [goalInput, setGoalInput] = useState(initialGoal);
|
||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
||||
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({ sound: false, timer: false });
|
||||
|
||||
const {
|
||||
selectedPresetId,
|
||||
@@ -102,6 +138,88 @@ export const SpaceWorkspaceWidget = () => {
|
||||
const isFocusMode = workspaceMode === 'focus';
|
||||
const { activeStatus, pushStatusLine, runActiveAction } = useHudStatusLine(isFocusMode);
|
||||
|
||||
const applyRecommendedSelections = useCallback((roomId: string) => {
|
||||
const room = getRoomById(roomId);
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectionOverride.timer) {
|
||||
const recommendedTimerLabel = resolveTimerLabelFromPresetId(room.recommendedTimerPresetId);
|
||||
|
||||
if (recommendedTimerLabel) {
|
||||
setSelectedTimerLabel(recommendedTimerLabel);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectionOverride.sound && SOUND_PRESETS.some((preset) => preset.id === room.recommendedSoundPresetId)) {
|
||||
setSelectedPresetId(room.recommendedSoundPresetId);
|
||||
}
|
||||
}, [selectionOverride.sound, selectionOverride.timer, setSelectedPresetId]);
|
||||
|
||||
useEffect(() => {
|
||||
applyRecommendedSelections(selectedRoomId);
|
||||
}, [applyRecommendedSelections, selectedRoomId]);
|
||||
|
||||
const handleSelectRoom = (roomId: string) => {
|
||||
setSelectedRoomId(roomId);
|
||||
};
|
||||
|
||||
const handleSelectTimer = (timerLabel: string, markOverride = false) => {
|
||||
setSelectedTimerLabel(timerLabel);
|
||||
|
||||
if (!markOverride) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectionOverride((current) => {
|
||||
if (current.timer) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return { ...current, timer: true };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectSound = (presetId: string, markOverride = false) => {
|
||||
setSelectedPresetId(presetId);
|
||||
|
||||
if (!markOverride) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectionOverride((current) => {
|
||||
if (current.sound) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return { ...current, sound: true };
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetToSceneRecommended = () => {
|
||||
const room = getRoomById(selectedRoomId);
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectionOverride({ sound: false, timer: false });
|
||||
|
||||
const recommendedTimerLabel = resolveTimerLabelFromPresetId(room.recommendedTimerPresetId);
|
||||
|
||||
if (recommendedTimerLabel) {
|
||||
setSelectedTimerLabel(recommendedTimerLabel);
|
||||
}
|
||||
|
||||
if (SOUND_PRESETS.some((preset) => preset.id === room.recommendedSoundPresetId)) {
|
||||
setSelectedPresetId(room.recommendedSoundPresetId);
|
||||
}
|
||||
|
||||
pushStatusLine({ message: '추천으로 되돌림(더미)' });
|
||||
};
|
||||
|
||||
const handleGoalChipSelect = (chip: GoalChip) => {
|
||||
setSelectedGoalId(chip.id);
|
||||
setGoalInput(chip.label);
|
||||
@@ -164,9 +282,9 @@ export const SpaceWorkspaceWidget = () => {
|
||||
soundPresets={SOUND_PRESETS}
|
||||
timerPresets={TIMER_SELECTION_PRESETS}
|
||||
canStart={canStart}
|
||||
onRoomSelect={setSelectedRoomId}
|
||||
onTimerSelect={setSelectedTimerLabel}
|
||||
onSoundSelect={setSelectedPresetId}
|
||||
onRoomSelect={handleSelectRoom}
|
||||
onTimerSelect={(timerLabel) => handleSelectTimer(timerLabel, true)}
|
||||
onSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
||||
onGoalChange={handleGoalChange}
|
||||
onGoalChipSelect={handleGoalChipSelect}
|
||||
onStart={handleStart}
|
||||
@@ -194,9 +312,12 @@ export const SpaceWorkspaceWidget = () => {
|
||||
thoughts={thoughts}
|
||||
thoughtCount={thoughtCount}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onRoomSelect={setSelectedRoomId}
|
||||
onTimerSelect={setSelectedTimerLabel}
|
||||
onSelectPreset={setSelectedPresetId}
|
||||
onRoomSelect={handleSelectRoom}
|
||||
onTimerSelect={(timerLabel) => handleSelectTimer(timerLabel, true)}
|
||||
onSelectPreset={(presetId) => handleSelectSound(presetId, true)}
|
||||
sceneRecommendedSoundLabel={selectedRoom.recommendedSound}
|
||||
sceneRecommendedTimerLabel={resolveTimerLabelFromPresetId(selectedRoom.recommendedTimerPresetId) ?? selectedTimerLabel}
|
||||
onResetToSceneRecommended={handleResetToSceneRecommended}
|
||||
soundVolume={masterVolume}
|
||||
onSetSoundVolume={setMasterVolume}
|
||||
isSoundMuted={isMuted}
|
||||
|
||||
Reference in New Issue
Block a user