refactor: FSD 구조 강화 및 파일 500줄 제한에 따른 대규모 리팩토링

- SpaceWorkspaceWidget 로직을 전용 훅 및 유틸리티로 분리 (900줄 -> 300줄)
- useSpaceWorkspaceSelection 훅을 기능별(영속성, 진단 등) 소형 훅으로 분리
- SpaceToolsDockWidget의 상태 및 핸들러 로직 추출
- 거대 i18n 번역 파일(ko.ts)을 도메인별 메시지 파일로 구조화
- AdminConsoleWidget 누락분 추가 및 미디어 엔티티 타입 오류 수정
This commit is contained in:
2026-03-11 15:08:36 +09:00
parent 7867bd39ca
commit 35f1dfb92d
36 changed files with 3238 additions and 2611 deletions

View File

@@ -0,0 +1,206 @@
'use client';
import { useCallback, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
import type { RecentThought } from '@/entities/session';
import { copy } from '@/shared/i18n';
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
import type { SpaceAnchorPopoverId, SpaceUtilityPanelId } from './types';
import type { PlanTier } from '@/entities/plan';
interface UseSpaceToolsDockHandlersParams {
setIdle: (idle: boolean) => void;
setOpenPopover: (popover: SpaceAnchorPopoverId | null) => void;
setUtilityPanel: (panel: SpaceUtilityPanelId | null) => void;
onCaptureThought: (note: string) => RecentThought | null;
onDeleteThought: (thoughtId: string) => RecentThought | null;
onSetThoughtCompleted: (thoughtId: string, isCompleted: boolean) => RecentThought | null;
onRestoreThought: (thought: RecentThought) => void;
onRestoreThoughts: (thoughts: RecentThought[]) => void;
onClearInbox: () => RecentThought[];
onStatusMessage: (payload: HudStatusLinePayload) => void;
onSetSoundVolume: (volume: number) => void;
onSetSoundMuted: (muted: boolean) => void;
soundVolume: number;
isSoundMuted: boolean;
showVolumeFeedback: (volume: number) => void;
}
export const useSpaceToolsDockHandlers = ({
setIdle,
setOpenPopover,
setUtilityPanel,
onCaptureThought,
onDeleteThought,
onSetThoughtCompleted,
onRestoreThought,
onRestoreThoughts,
onClearInbox,
onStatusMessage,
onSetSoundVolume,
onSetSoundMuted,
soundVolume,
isSoundMuted,
showVolumeFeedback,
}: UseSpaceToolsDockHandlersParams) => {
const { toolsDock } = copy.space;
const [noteDraft, setNoteDraft] = useState('');
const [plan, setPlan] = useState<PlanTier>('normal');
const openUtilityPanel = useCallback((panel: SpaceUtilityPanelId) => {
setIdle(false);
setOpenPopover(null);
setUtilityPanel(panel);
}, [setIdle, setOpenPopover, setUtilityPanel]);
const handleNoteSubmit = useCallback(() => {
const trimmedNote = noteDraft.trim();
if (!trimmedNote) {
return;
}
const addedThought = onCaptureThought(trimmedNote);
if (!addedThought) {
return;
}
setNoteDraft('');
onStatusMessage({
message: toolsDock.inboxSaved,
durationMs: 4200,
priority: 'undo',
action: {
label: toolsDock.undo,
onClick: () => {
const removed = onDeleteThought(addedThought.id);
if (!removed) {
return;
}
onStatusMessage({ message: toolsDock.inboxSaveUndone });
},
},
});
}, [noteDraft, onCaptureThought, onDeleteThought, onStatusMessage, toolsDock.inboxSaved, toolsDock.inboxSaveUndone, toolsDock.undo]);
const handleInboxComplete = useCallback((thought: RecentThought) => {
onSetThoughtCompleted(thought.id, !thought.isCompleted);
}, [onSetThoughtCompleted]);
const handleInboxDelete = useCallback((thought: RecentThought) => {
const removedThought = onDeleteThought(thought.id);
if (!removedThought) {
return;
}
onStatusMessage({
message: toolsDock.deleted,
durationMs: 4200,
priority: 'undo',
action: {
label: toolsDock.undo,
onClick: () => {
onRestoreThought(removedThought);
onStatusMessage({ message: toolsDock.deleteUndone });
},
},
});
}, [onDeleteThought, onRestoreThought, onStatusMessage, toolsDock.deleted, toolsDock.deleteUndone, toolsDock.undo]);
const handleInboxClear = useCallback(() => {
const snapshot = onClearInbox();
if (snapshot.length === 0) {
onStatusMessage({ message: toolsDock.emptyToClear });
return;
}
onStatusMessage({
message: toolsDock.clearedAll,
durationMs: 4200,
priority: 'undo',
action: {
label: toolsDock.undo,
onClick: () => {
onRestoreThoughts(snapshot);
onStatusMessage({ message: toolsDock.restored });
},
},
});
}, [onClearInbox, onRestoreThoughts, onStatusMessage, toolsDock.clearedAll, toolsDock.emptyToClear, toolsDock.restored, toolsDock.undo]);
const handlePlanPillClick = useCallback(() => {
if (plan === 'pro') {
openUtilityPanel('manage-plan');
return;
}
onStatusMessage({ message: toolsDock.normalPlanInfo });
}, [openUtilityPanel, onStatusMessage, plan, toolsDock.normalPlanInfo]);
const handleLockedClick = useCallback((source: string) => {
onStatusMessage({ message: toolsDock.proFeatureLocked(source) });
openUtilityPanel('paywall');
}, [onStatusMessage, openUtilityPanel, toolsDock]);
const handleSelectProFeature = useCallback((featureId: string) => {
const label =
featureId === 'scene-packs'
? toolsDock.featureLabels.scenePacks
: featureId === 'sound-packs'
? toolsDock.featureLabels.soundPacks
: toolsDock.featureLabels.profiles;
onStatusMessage({ message: toolsDock.proFeaturePending(label) });
}, [onStatusMessage, toolsDock.featureLabels]);
const handleStartPro = useCallback(() => {
setPlan('pro');
onStatusMessage({ message: toolsDock.purchaseMock });
openUtilityPanel('control-center');
}, [onStatusMessage, openUtilityPanel, toolsDock.purchaseMock]);
const handleVolumeChange = useCallback((nextVolume: number) => {
const clamped = Math.min(100, Math.max(0, nextVolume));
onSetSoundVolume(clamped);
if (isSoundMuted && clamped > 0) {
onSetSoundMuted(false);
}
showVolumeFeedback(clamped);
}, [isSoundMuted, onSetSoundMuted, onSetSoundVolume, showVolumeFeedback]);
const handleVolumeKeyDown = useCallback((event: ReactKeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
return;
}
event.preventDefault();
const step = event.shiftKey ? 10 : 5;
const delta = event.key === 'ArrowRight' ? step : -step;
handleVolumeChange(soundVolume + delta);
}, [handleVolumeChange, soundVolume]);
return {
noteDraft,
setNoteDraft,
plan,
setPlan,
openUtilityPanel,
handleNoteSubmit,
handleInboxComplete,
handleInboxDelete,
handleInboxClear,
handlePlanPillClick,
handleLockedClick,
handleSelectProFeature,
handleStartPro,
handleVolumeChange,
handleVolumeKeyDown,
};
};