From 35f1dfb92d164cf87967dab5b39d12a05e63bb62 Mon Sep 17 00:00:00 2001 From: corpi Date: Wed, 11 Mar 2026 15:08:36 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20FSD=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=95=ED=99=94=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20500?= =?UTF-8?q?=EC=A4=84=20=EC=A0=9C=ED=95=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=8C=80=EA=B7=9C=EB=AA=A8=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SpaceWorkspaceWidget 로직을 전용 훅 및 유틸리티로 분리 (900줄 -> 300줄) - useSpaceWorkspaceSelection 훅을 기능별(영속성, 진단 등) 소형 훅으로 분리 - SpaceToolsDockWidget의 상태 및 핸들러 로직 추출 - 거대 i18n 번역 파일(ko.ts)을 도메인별 메시지 파일로 구조화 - AdminConsoleWidget 누락분 추가 및 미디어 엔티티 타입 오류 수정 --- src/app/(app)/layout.tsx | 2 +- src/app/admin/page.tsx | 709 +------------ src/entities/auth/index.ts | 1 + src/entities/auth/model/types.ts | 22 + .../session}/api/inboxApi.ts | 2 +- src/entities/session/model/useThoughtInbox.ts | 2 +- .../auth/components/AuthRedirectButton.tsx | 2 +- src/shared/config/authTokens.ts | 2 + src/shared/i18n/ko.ts | 733 +------------- src/shared/i18n/messages/admin.ts | 131 +++ src/shared/i18n/messages/app.ts | 241 +++++ src/shared/i18n/messages/core.ts | 172 ++++ src/shared/i18n/messages/product.ts | 72 ++ src/shared/i18n/messages/scenes.ts | 92 ++ src/shared/i18n/messages/session.ts | 77 ++ src/shared/i18n/messages/space.ts | 184 ++++ src/shared/lib/apiClient.ts | 9 +- src/store/useAuthStore.ts | 4 +- src/widgets/admin-console/index.ts | 1 + .../admin-console/model/sessionStorage.ts | 35 + src/widgets/admin-console/model/types.ts | 14 + .../admin-console/model/useAdminConsole.ts | 208 ++++ .../admin-console/ui/AdminConsoleWidget.tsx | 41 + .../admin-console/ui/AdminLoginView.tsx | 127 +++ src/widgets/admin-console/ui/constants.ts | 13 + .../model/useSpaceToolsDockHandlers.ts | 206 ++++ .../model/useSpaceToolsDockState.ts | 143 +++ .../space-tools-dock/ui/FocusModeAnchors.tsx | 160 +++ .../ui/SpaceToolsDockWidget.tsx | 467 ++------- src/widgets/space-workspace/model/types.ts | 15 + .../model/useSpaceWorkspaceSelection.ts | 440 ++++++++ .../model/useSpaceWorkspaceSessionControls.ts | 316 ++++++ .../model/useWorkspaceMediaDiagnostics.ts | 70 ++ .../model/useWorkspacePersistence.ts | 60 ++ .../model/workspaceSelection.ts | 140 +++ .../ui/SpaceWorkspaceWidget.tsx | 936 +++--------------- 36 files changed, 3238 insertions(+), 2611 deletions(-) create mode 100644 src/entities/auth/index.ts create mode 100644 src/entities/auth/model/types.ts rename src/{features/inbox => entities/session}/api/inboxApi.ts (97%) create mode 100644 src/shared/config/authTokens.ts create mode 100644 src/shared/i18n/messages/admin.ts create mode 100644 src/shared/i18n/messages/app.ts create mode 100644 src/shared/i18n/messages/core.ts create mode 100644 src/shared/i18n/messages/product.ts create mode 100644 src/shared/i18n/messages/scenes.ts create mode 100644 src/shared/i18n/messages/session.ts create mode 100644 src/shared/i18n/messages/space.ts create mode 100644 src/widgets/admin-console/index.ts create mode 100644 src/widgets/admin-console/model/sessionStorage.ts create mode 100644 src/widgets/admin-console/model/types.ts create mode 100644 src/widgets/admin-console/model/useAdminConsole.ts create mode 100644 src/widgets/admin-console/ui/AdminConsoleWidget.tsx create mode 100644 src/widgets/admin-console/ui/AdminLoginView.tsx create mode 100644 src/widgets/admin-console/ui/constants.ts create mode 100644 src/widgets/space-tools-dock/model/useSpaceToolsDockHandlers.ts create mode 100644 src/widgets/space-tools-dock/model/useSpaceToolsDockState.ts create mode 100644 src/widgets/space-tools-dock/ui/FocusModeAnchors.tsx create mode 100644 src/widgets/space-workspace/model/types.ts create mode 100644 src/widgets/space-workspace/model/useSpaceWorkspaceSelection.ts create mode 100644 src/widgets/space-workspace/model/useSpaceWorkspaceSessionControls.ts create mode 100644 src/widgets/space-workspace/model/useWorkspaceMediaDiagnostics.ts create mode 100644 src/widgets/space-workspace/model/useWorkspacePersistence.ts create mode 100644 src/widgets/space-workspace/model/workspaceSelection.ts diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index ec542c4..252c5ad 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from "react"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { TOKEN_COOKIE_KEY } from "@/features/auth/model/constants"; +import { TOKEN_COOKIE_KEY } from "@/shared/config/authTokens"; interface AppLayoutProps { children: ReactNode; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index f403aea..8cc65a2 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,712 +1,7 @@ 'use client'; -import { FormEvent, useEffect, useState } from 'react'; -import { adminApi, type SceneMediaAssetUploadResponse, type SoundMediaAssetUploadResponse } from '@/features/admin/api/adminApi'; -import type { AuthResponse } from '@/features/auth/types'; -import { copy } from '@/shared/i18n'; -import { Button } from '@/shared/ui'; - -const ADMIN_STORAGE_KEY = 'vr_admin_session'; - -type AdminView = 'scene' | 'sound'; - -type UploadResult = - | { type: 'scene'; payload: SceneMediaAssetUploadResponse } - | { type: 'sound'; payload: SoundMediaAssetUploadResponse }; - -type NavItem = { - id: AdminView; - title: string; - subtitle: string; - section: string; -}; - -const navItems: NavItem[] = [...copy.admin.navItems]; - -const readStoredSession = (): AuthResponse | null => { - if (typeof window === 'undefined') { - return null; - } - - const rawValue = window.localStorage.getItem(ADMIN_STORAGE_KEY); - if (!rawValue) { - return null; - } - - try { - return JSON.parse(rawValue) as AuthResponse; - } catch { - window.localStorage.removeItem(ADMIN_STORAGE_KEY); - return null; - } -}; - -const storeSession = (session: AuthResponse | null) => { - if (typeof window === 'undefined') { - return; - } - - if (!session) { - window.localStorage.removeItem(ADMIN_STORAGE_KEY); - return; - } - - window.localStorage.setItem(ADMIN_STORAGE_KEY, JSON.stringify(session)); -}; - -const fieldClassName = - 'h-11 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-sky-400 focus:ring-2 focus:ring-sky-100'; - -const fileClassName = - 'block w-full rounded-xl border border-dashed border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white hover:file:bg-slate-800'; - -const textareaClassName = - 'min-h-32 w-full rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-sky-400 focus:ring-2 focus:ring-sky-100'; - -const getViewMeta = (view: AdminView) => { - return copy.admin.views[view]; -}; - -const formatResultSummary = (uploadResult: UploadResult | null) => { - if (!uploadResult) { - return copy.admin.inspector.noUploadSummary; - } - - if (uploadResult.type === 'scene') { - return copy.admin.messages.sceneSummary(uploadResult.payload.sceneId, uploadResult.payload.assetVersion); - } - - return copy.admin.messages.soundSummary(uploadResult.payload.presetId, uploadResult.payload.assetVersion); -}; +import { AdminConsoleWidget } from '@/widgets/admin-console'; export default function AdminPage() { - const [session, setSession] = useState(null); - const [activeView, setActiveView] = useState('scene'); - const [isDurationOverrideEnabled, setIsDurationOverrideEnabled] = useState(false); - const [loginId, setLoginId] = useState(copy.admin.defaultLoginId); - const [password, setPassword] = useState(copy.admin.defaultPassword); - const [loginError, setLoginError] = useState(null); - const [loginPending, setLoginPending] = useState(false); - const [scenePending, setScenePending] = useState(false); - const [soundPending, setSoundPending] = useState(false); - const [sceneMessage, setSceneMessage] = useState(null); - const [soundMessage, setSoundMessage] = useState(null); - const [uploadResult, setUploadResult] = useState(null); - - useEffect(() => { - setSession(readStoredSession()); - }, []); - - const handleLogin = async (event: FormEvent) => { - event.preventDefault(); - setLoginPending(true); - setLoginError(null); - - try { - const response = await adminApi.login({ - loginId: loginId.trim(), - password, - }); - - if (response.user?.grade !== 'ADMIN') { - throw new Error(copy.admin.messages.nonAdmin); - } - - setSession(response); - storeSession(response); - } catch (error) { - setLoginError(error instanceof Error ? error.message : copy.admin.messages.loginFailed); - } finally { - setLoginPending(false); - } - }; - - const handleLogout = () => { - setSession(null); - setActiveView('scene'); - setIsDurationOverrideEnabled(false); - setSceneMessage(null); - setSoundMessage(null); - setUploadResult(null); - storeSession(null); - }; - - const handleSceneUpload = async (event: FormEvent) => { - event.preventDefault(); - if (!session?.accessToken) { - setSceneMessage(copy.admin.messages.loginRequired); - return; - } - - setScenePending(true); - setSceneMessage(null); - - try { - const form = event.currentTarget; - const source = new FormData(form); - const sceneId = String(source.get('sceneId') ?? '').trim(); - - if (!sceneId) { - throw new Error(copy.admin.messages.sceneIdRequired); - } - - const formData = new FormData(); - const sourceImageFile = source.get('sourceImageFile'); - - if (sourceImageFile instanceof File && sourceImageFile.size > 0) { - formData.append('sourceImageFile', sourceImageFile); - } - - const blurDataUrl = String(source.get('blurDataUrl') ?? '').trim(); - if (blurDataUrl) { - formData.append('blurDataUrl', blurDataUrl); - } - - const response = await adminApi.uploadScene(sceneId, formData, session.accessToken); - setUploadResult({ type: 'scene', payload: response }); - setSceneMessage(copy.admin.messages.sceneUploadDone(sceneId)); - form.reset(); - } catch (error) { - setSceneMessage(error instanceof Error ? error.message : copy.admin.messages.sceneUploadFailed); - } finally { - setScenePending(false); - } - }; - - const handleSoundUpload = async (event: FormEvent) => { - event.preventDefault(); - if (!session?.accessToken) { - setSoundMessage(copy.admin.messages.loginRequired); - return; - } - - setSoundPending(true); - setSoundMessage(null); - - try { - const form = event.currentTarget; - const source = new FormData(form); - const presetId = String(source.get('presetId') ?? '').trim(); - - if (!presetId) { - throw new Error(copy.admin.messages.presetIdRequired); - } - - const formData = new FormData(); - for (const fieldName of ['loopFile', 'previewFile', 'fallbackLoopFile']) { - const file = source.get(fieldName); - if (file instanceof File && file.size > 0) { - formData.append(fieldName, file); - } - } - - const defaultVolume = String(source.get('defaultVolume') ?? '').trim(); - if (defaultVolume) { - formData.append('defaultVolume', defaultVolume); - } - - if (isDurationOverrideEnabled) { - const durationSec = String(source.get('durationSec') ?? '').trim(); - if (durationSec) { - formData.append('durationSec', durationSec); - } - } - - const response = await adminApi.uploadSound(presetId, formData, session.accessToken); - setUploadResult({ type: 'sound', payload: response }); - setSoundMessage(copy.admin.messages.soundUploadDone(presetId)); - form.reset(); - setIsDurationOverrideEnabled(false); - } catch (error) { - setSoundMessage(error instanceof Error ? error.message : copy.admin.messages.soundUploadFailed); - } finally { - setSoundPending(false); - } - }; - - const activeMeta = getViewMeta(activeView); - const currentMessage = activeView === 'scene' ? sceneMessage : soundMessage; - const lastExtractedDurationSec = - uploadResult?.type === 'sound' ? uploadResult.payload.durationSec ?? null : null; - - if (!session) { - return ( -
-
- - -
-
-
-

{copy.admin.signInEyebrow}

-

- {copy.admin.loginTitle} -

-

- {copy.admin.loginDescription} -

-
- -
-
- - setLoginId(event.target.value)} - autoComplete="username" - placeholder={copy.admin.defaultLoginId} - /> -
-
- - setPassword(event.target.value)} - autoComplete="current-password" - placeholder={copy.admin.defaultPassword} - /> -
- {loginError ? ( -
- {loginError} -
- ) : null} - -
-
-
-
-
- ); - } - - return ( -
-
- - -
-
-
-
- -
- - - ⌕ - -
-
- -
-
- - {copy.admin.manifestReady} -
-
-
- A -
-
-

{session.user?.name ?? copy.common.admin}

-

{session.user?.email}

-
-
-
-
-
- -
-
-
-
-
-

- {activeMeta.eyebrow} -

-

- {activeMeta.title} -

-

- {activeMeta.description} -

-
-
-

{activeMeta.statTitle}

-

{activeMeta.statValue}

-

{activeMeta.statHint}

-
-
-

{copy.admin.inspector.currentRoleTitle}

-

- {session.user?.grade ?? 'ADMIN'} -

-

{copy.admin.inspector.bearerTokenSession}

-
-
- - {activeView === 'scene' ? ( -
-
-

{copy.admin.views.scene.workspaceTitle}

-

- {copy.admin.views.scene.workspaceDescription} -

-
- -
-
-
-
- - -

{copy.admin.views.scene.sceneIdHint}

-
-
- -
-
- - -

{copy.admin.views.scene.sourceImageHint}

-

{copy.admin.views.scene.sourceImageDerivedHint}

-
-
- -
- -