import { lazy, Suspense, useEffect, useRef, useState } from "react"; import { getAudioContentCreateCharacterIdFromPath, getAudioContentDetailRouteFromPath, getAudioContentEditRouteFromPath, getAudioContentListCharacterIdFromPath, getCharacterEditIdFromPath, getCharacterIdFromPath, getCommunityPostCreateCharacterIdFromPath, getCommunityPostListCharacterIdFromPath, getFanTalkListCharacterIdFromPath, getSeriesCreateCharacterIdFromPath, getSeriesDetailRouteFromPath, getSeriesEditRouteFromPath, getSeriesListCharacterIdFromPath, getSeriesOrderCharacterIdFromPath, navigateTo, useBrowserLocation } from "@/app/browser-location"; import { routePaths } from "@/app/route-paths"; import { useAuthSession } from "@/features/auth/model/auth-session-context"; import { authSessionStorage } from "@/features/auth/model/auth-session-storage"; import type { ApiClient } from "@/shared/api/client"; import type { ApiMode } from "@/shared/config/env"; import { MockModeBanner } from "@/shared/ui/mock-mode-banner"; import { PageState } from "@/shared/ui/page-state"; const AudioContentDetailPage = lazy(() => import("@/features/audio-contents/pages/AudioContentDetailPage").then(({ AudioContentDetailPage }) => ({ default: AudioContentDetailPage }))); const AudioContentFormPage = lazy(() => import("@/features/audio-contents/pages/AudioContentFormPage").then(({ AudioContentFormPage }) => ({ default: AudioContentFormPage }))); const AudioContentListPage = lazy(() => import("@/features/audio-contents/pages/AudioContentListPage").then(({ AudioContentListPage }) => ({ default: AudioContentListPage }))); const CharacterCreatePage = lazy(() => import("@/features/characters/pages/CharacterCreatePage").then(({ CharacterCreatePage }) => ({ default: CharacterCreatePage }))); const CharacterDetailPage = lazy(() => import("@/features/characters/pages/CharacterDetailPage").then(({ CharacterDetailPage }) => ({ default: CharacterDetailPage }))); const CharacterEditPage = lazy(() => import("@/features/characters/pages/CharacterEditPage").then(({ CharacterEditPage }) => ({ default: CharacterEditPage }))); const CharacterListPage = lazy(() => import("@/features/characters/pages/CharacterListPage").then(({ CharacterListPage }) => ({ default: CharacterListPage }))); const CommunityPostFormPage = lazy(() => import("@/features/community-posts/pages/CommunityPostFormPage").then(({ CommunityPostFormPage }) => ({ default: CommunityPostFormPage }))); const CommunityPostListPage = lazy(() => import("@/features/community-posts/pages/CommunityPostListPage").then(({ CommunityPostListPage }) => ({ default: CommunityPostListPage }))); const FanTalkListPage = lazy(() => import("@/features/fan-talks/pages/FanTalkListPage").then(({ FanTalkListPage }) => ({ default: FanTalkListPage }))); const SeriesDetailPage = lazy(() => import("@/features/series/pages/SeriesDetailPage").then(({ SeriesDetailPage }) => ({ default: SeriesDetailPage }))); const SeriesFormPage = lazy(() => import("@/features/series/pages/SeriesFormPage").then(({ SeriesFormPage }) => ({ default: SeriesFormPage }))); const SeriesListPage = lazy(() => import("@/features/series/pages/SeriesListPage").then(({ SeriesListPage }) => ({ default: SeriesListPage }))); const SeriesOrderPage = lazy(() => import("@/features/series/pages/SeriesOrderPage").then(({ SeriesOrderPage }) => ({ default: SeriesOrderPage }))); const focusableSelector = "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"; const sessionExpiredNotice = "세션이 만료되었습니다. 다시 로그인하세요."; function NavLink() { return ( { event.preventDefault(); navigateTo(routePaths.aiCharacters); }} > AI 캐릭터 ); } export function ProtectedAdminShell({ apiClient, apiMode, routeError }: { readonly apiClient: ApiClient; readonly apiMode: ApiMode; readonly routeError: string | null }) { const auth = useAuthSession(); const uploadAuth = { clearSession: () => auth.clearSession(sessionExpiredNotice), getToken: () => authSessionStorage.read()?.token ?? null, onAuthExpired: () => navigateTo(routePaths.login), }; const location = useBrowserLocation(); const audioContentDetailRoute = getAudioContentDetailRouteFromPath(location.path); const audioContentEditRoute = getAudioContentEditRouteFromPath(location.path); const audioContentCreateCharacterId = getAudioContentCreateCharacterIdFromPath(location.path); const audioContentListCharacterId = getAudioContentListCharacterIdFromPath(location.path); const seriesDetailRoute = getSeriesDetailRouteFromPath(location.path); const seriesEditRoute = getSeriesEditRouteFromPath(location.path); const seriesCreateCharacterId = getSeriesCreateCharacterIdFromPath(location.path); const seriesOrderCharacterId = getSeriesOrderCharacterIdFromPath(location.path); const seriesListCharacterId = getSeriesListCharacterIdFromPath(location.path); const communityPostListCharacterId = getCommunityPostListCharacterIdFromPath(location.path); const communityPostCreateCharacterId = getCommunityPostCreateCharacterIdFromPath(location.path); const fanTalkListCharacterId = getFanTalkListCharacterIdFromPath(location.path); const characterEditId = getCharacterEditIdFromPath(location.path); const characterId = getCharacterIdFromPath(location.path); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const menuButtonRef = useRef(null); const closeButtonRef = useRef(null); const mobileMenuRef = useRef(null); const mainRef = useRef(null); const shouldRestoreMenuFocusRef = useRef(false); useEffect(() => { if (window.matchMedia === undefined) { return undefined; } const desktopMediaQuery = window.matchMedia("(min-width: 1024px)"); function closeOnDesktopMatch() { if (!desktopMediaQuery.matches) { return; } shouldRestoreMenuFocusRef.current = false; setIsMobileMenuOpen(false); } closeOnDesktopMatch(); desktopMediaQuery.addEventListener("change", closeOnDesktopMatch); return () => desktopMediaQuery.removeEventListener("change", closeOnDesktopMatch); }, []); useEffect(() => { if (isMobileMenuOpen || !shouldRestoreMenuFocusRef.current) { return; } shouldRestoreMenuFocusRef.current = false; menuButtonRef.current?.focus(); }, [isMobileMenuOpen]); useEffect(() => { if (!isMobileMenuOpen) { return undefined; } closeButtonRef.current?.focus(); function closeOnEscape(event: KeyboardEvent) { if (event.key === "Escape") { shouldRestoreMenuFocusRef.current = true; setIsMobileMenuOpen(false); } } window.addEventListener("keydown", closeOnEscape); return () => window.removeEventListener("keydown", closeOnEscape); }, [isMobileMenuOpen]); function keepFocusInMobileMenu(event: React.KeyboardEvent) { if (event.key !== "Tab") { return; } const focusableElements = Array.from(mobileMenuRef.current?.querySelectorAll(focusableSelector) ?? []); const firstElement = focusableElements[0]; const lastElement = focusableElements.at(-1); if (firstElement === undefined || lastElement === undefined) { return; } if (event.shiftKey && document.activeElement === firstElement) { event.preventDefault(); lastElement.focus(); return; } if (!event.shiftKey && document.activeElement === lastElement) { event.preventDefault(); firstElement.focus(); } } function closeMobileMenu() { shouldRestoreMenuFocusRef.current = true; setIsMobileMenuOpen(false); } return ( { event.preventDefault(); mainRef.current?.focus(); }}> 본문으로 건너뛰기 setIsMobileMenuOpen(true)} ref={menuButtonRef} type="button" > 모바일 메뉴 열기 홈 / AI 캐릭터 void auth.logout()} type="button"> 로그아웃 {location.successNotification === null ? null : ( {location.successNotification} )} } key={location.path}> {location.path === routePaths.aiCharacterCreate ? : null} {characterEditId !== null ? : null} {location.path !== routePaths.aiCharacterCreate && characterEditId === null && characterId === null && audioContentListCharacterId === null && audioContentCreateCharacterId === null && audioContentEditRoute === null && audioContentDetailRoute === null && communityPostCreateCharacterId === null && communityPostListCharacterId === null && fanTalkListCharacterId === null && seriesCreateCharacterId === null && seriesEditRoute === null && seriesListCharacterId === null && seriesOrderCharacterId === null && seriesDetailRoute === null ? : null} {location.path !== routePaths.aiCharacterCreate && characterEditId === null && characterId !== null ? : null} {audioContentListCharacterId !== null ? : null} {audioContentCreateCharacterId !== null ? : null} {audioContentEditRoute !== null ? : null} {audioContentDetailRoute !== null ? : null} {communityPostListCharacterId !== null ? : null} {communityPostCreateCharacterId !== null ? : null} {fanTalkListCharacterId !== null ? : null} {seriesListCharacterId !== null ? : null} {seriesCreateCharacterId !== null ? : null} {seriesEditRoute !== null ? : null} {seriesOrderCharacterId !== null ? : null} {seriesDetailRoute !== null ? : null} {isMobileMenuOpen ? ( 모바일 메뉴 닫기 ) : null} ); }
{location.successNotification}