"use client"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Settings, Menu, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { menuApi, MenuItem } from "@/lib/api/menu"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; import { useRouter, usePathname } from "next/navigation"; import { getMenuTextSync, MENU_MANAGEMENT_KEYS, useMenuManagementText, setTranslationCache, } from "@/lib/utils/multilang"; import { useMultiLang } from "@/hooks/useMultiLang"; import { tokenSync } from "@/lib/sessionManager"; // 아이콘 매핑 const ICON_MAP: { [key: string]: any } = { Settings, }; // 기본 관리자 메뉴 (메뉴관리에서 등록된 메뉴가 없을 때 사용) // 동적으로 생성되는 메뉴들만 사용하므로 기본 메뉴 제거 const DEFAULT_ADMIN_MENUS: any[] = []; /** * 관리자 팝업 전용 레이아웃 */ export default function AdminLayout({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const { userLang, setGlobalChangeLangCallback } = useMultiLang({ companyCode: "*" }); const { getMenuText } = useMenuManagementText(); const [adminMenus, setAdminMenus] = useState([]); const [loading, setLoading] = useState(true); const [activeMenu, setActiveMenu] = useState("company"); const [translationsLoaded, setTranslationsLoaded] = useState(false); const [forceUpdate, setForceUpdate] = useState(0); const [sidebarOpen, setSidebarOpen] = useState(true); // 사이드바 토글 상태 추가 const [isAuthorized, setIsAuthorized] = useState(null); const [menuTranslations, setMenuTranslations] = useState<{ title: string; description: string; }>({ title: "메뉴 관리", description: "시스템의 메뉴 구조와 권한을 관리합니다.", }); // 토큰 확인 및 인증 상태 체크 useEffect(() => { console.log("=== AdminLayout 토큰 확인 ==="); console.log("현재 경로:", pathname); console.log("현재 URL:", window.location.href); const checkToken = () => { const token = localStorage.getItem("authToken"); console.log("localStorage 토큰:", token ? "존재" : "없음"); console.log("토큰 길이:", token ? token.length : 0); console.log("토큰 시작:", token ? token.substring(0, 30) + "..." : "없음"); // sessionStorage도 확인 const sessionToken = sessionStorage.getItem("authToken"); console.log("sessionStorage 토큰:", sessionToken ? "존재" : "없음"); // 현재 인증 상태도 확인 console.log("현재 isAuthorized 상태:", isAuthorized); // 토큰이 없으면 sessionStorage에서 복원 시도 if (!token && sessionToken) { console.log("🔄 sessionStorage에서 토큰 복원 시도"); const restored = tokenSync.restoreFromSession(); if (restored) { console.log("✅ 토큰 복원 성공"); setIsAuthorized(true); return; } } // 토큰 유효성 검증 if (token && !tokenSync.validateToken(token)) { console.log("❌ 토큰 유효성 검증 실패"); localStorage.removeItem("authToken"); sessionStorage.removeItem("authToken"); setIsAuthorized(false); setTimeout(() => { console.log("리다이렉트 실행: /login"); window.location.href = "/login"; }, 5000); return; } if (!token) { console.log("❌ 토큰이 없음 - 로그인 페이지로 이동"); setIsAuthorized(false); // 5초 후 리다이렉트 (디버깅을 위해 시간 늘림) setTimeout(() => { console.log("리다이렉트 실행: /login"); window.location.href = "/login"; }, 5000); return; } // 토큰이 있으면 인증된 것으로 간주 console.log("✅ 토큰 존재 - 인증된 것으로 간주"); setIsAuthorized(true); // 토큰 강제 동기화 (다른 탭과 동기화) tokenSync.forceSync(); }; // 초기 토큰 확인 checkToken(); // localStorage 변경 이벤트 리스너 추가 const handleStorageChange = (e: StorageEvent) => { if (e.key === "authToken") { console.log("🔄 localStorage authToken 변경 감지:", e.newValue ? "설정됨" : "제거됨"); checkToken(); } }; // 페이지 포커스 시 토큰 재확인 const handleFocus = () => { console.log("🔄 페이지 포커스 - 토큰 재확인"); checkToken(); }; window.addEventListener("storage", handleStorageChange); window.addEventListener("focus", handleFocus); return () => { window.removeEventListener("storage", handleStorageChange); window.removeEventListener("focus", handleFocus); }; }, [pathname]); // 관리자 메뉴 로드 useEffect(() => { if (isAuthorized) { loadAdminMenus(); } }, [isAuthorized]); // pathname 변경 시 활성 메뉴 업데이트 useEffect(() => { if (adminMenus.length > 0) { const currentPath = pathname; console.log("🔄 경로 변경 감지:", currentPath); // 정확한 URL 매칭을 위한 함수 const isExactMatch = (menuUrl: string, currentPath: string) => { // 메뉴 URL이 현재 경로와 정확히 일치하거나 // 메뉴 URL이 현재 경로의 시작 부분과 일치하는지 확인 // 단, 메뉴 관리 페이지는 제외 (별도 처리) if (currentPath === "/admin/menu" || currentPath.startsWith("/admin/menu/")) { return false; // 메뉴 관리 페이지는 별도 로직에서 처리 } // 정확한 매칭: URL이 정확히 일치하거나 현재 경로가 메뉴 URL로 시작하는 경우 // 단, 메뉴 URL이 "/admin"이고 현재 경로가 "/admin/menu"인 경우는 제외 if (menuUrl === "/admin" && currentPath.startsWith("/admin/menu")) { return false; } // 정확한 매칭: URL이 정확히 일치하는 경우 if (menuUrl === currentPath) { return true; } // 경로 매칭: 현재 경로가 메뉴 URL로 시작하는 경우 // 단, 메뉴 URL이 슬래시로 끝나지 않으면 정확한 경로 매칭만 허용 if (currentPath.startsWith(menuUrl + "/")) { return true; } return false; }; // 메뉴 관리 페이지인지 확인 (정확한 매칭) if (currentPath === "/admin/menu" || currentPath.startsWith("/admin/menu/")) { // 메뉴 관리 페이지는 URL 매칭과 메뉴명 매칭 모두 시도 const menuManagementMenu = adminMenus.find((menu) => { console.log("🔍 메뉴 검사:", { id: menu.id, name: menu.name, url: menu.url, level: menu.lev, }); // URL 매칭 (가장 우선) if (menu.url && (menu.url === "/admin/menu" || menu.url === "/admin/menu/")) { console.log("✅ URL 매칭 성공:", menu.url); return true; } // 메뉴명 매칭 (메뉴 관리 관련 메뉴) if (menu.name && (menu.name.includes("메뉴 관리") || menu.name.includes("Menu Management"))) { console.log("✅ 메뉴명 매칭 성공:", menu.name); return true; } return false; }); if (menuManagementMenu && menuManagementMenu.id) { console.log("✅ 메뉴 관리 메뉴 활성화:", menuManagementMenu.id, "이름:", menuManagementMenu.name); setActiveMenu(menuManagementMenu.id); } else { console.log("❌ 메뉴 관리 메뉴를 찾을 수 없음"); } } else { // 다른 메뉴들에 대한 정확한 매칭 console.log("🔍 다른 메뉴 매칭 시도:", currentPath); // 모든 메뉴에 대해 매칭 시도 const matchingMenus = adminMenus.filter((menu) => menu.url && isExactMatch(menu.url, currentPath)); if (matchingMenus.length > 0) { // 가장 구체적인 매칭을 찾기 (가장 긴 URL이 우선) const bestMatch = matchingMenus.reduce((best, current) => { return current.url && current.url.length > (best.url ? best.url.length : 0) ? current : best; }); console.log("✅ 매칭된 메뉴 활성화:", bestMatch.id, "URL:", bestMatch.url, "레벨:", bestMatch.lev); setActiveMenu(bestMatch.id); } else { // URL 매칭이 실패한 경우, 경로 기반으로 가장 적절한 메뉴 찾기 console.log("🔍 URL 매칭 실패, 경로 기반 매칭 시도"); // 현재 경로의 세그먼트 수 계산 const pathSegments = currentPath.split("/").filter((segment: string) => segment.length > 0); const currentPathLevel = pathSegments.length; console.log("현재 경로 레벨:", currentPathLevel, "세그먼트:", pathSegments); // 현재 경로 레벨과 일치하는 메뉴 찾기 const levelMatchingMenus = adminMenus.filter((menu) => { if (!menu.url) return false; const menuPathSegments = menu.url.split("/").filter((segment: string) => segment.length > 0); const menuLevel = menuPathSegments.length; // 현재 경로가 메뉴 URL로 시작하고, 레벨이 일치하는 경우 return currentPath.startsWith(menu.url) && menuLevel === currentPathLevel; }); if (levelMatchingMenus.length > 0) { const bestLevelMatch = levelMatchingMenus.reduce((best, current) => { return current.url && current.url.length > (best.url ? best.url.length : 0) ? current : best; }); console.log( "✅ 레벨 기반 매칭 성공:", bestLevelMatch.id, "URL:", bestLevelMatch.url, "레벨:", bestLevelMatch.lev, ); setActiveMenu(bestLevelMatch.id); } else { console.log("❌ 매칭되는 메뉴를 찾을 수 없음:", currentPath); // 매칭되는 메뉴가 없으면 첫 번째 메뉴를 기본값으로 설정 if (adminMenus.length > 0) { console.log("🔧 기본 메뉴로 설정:", adminMenus[0].id); setActiveMenu(adminMenus[0].id); } } } } } }, [pathname, adminMenus]); // 전역 언어 변경 콜백 등록 useEffect(() => { setGlobalChangeLangCallback((newLang: string) => { console.log("🔄 전역 언어 변경 감지:", newLang); // 번역 상태 리셋 setMenuTranslations({ title: "메뉴 관리", description: "시스템의 메뉴 구조와 권한을 관리합니다.", }); // 새로운 번역 로드 및 메뉴 데이터 재로드 setTimeout(() => { loadTranslations(); // 언어 변경 시 메뉴 데이터를 다시 로드하여 번역된 메뉴명 가져오기 loadAdminMenus(); }, 100); }); return () => { setGlobalChangeLangCallback(() => {}); }; }, [adminMenus]); // 번역 데이터 로드 useEffect(() => { if (userLang) { console.log("🔄 userLang 변경 감지:", userLang); // 번역 상태 리셋 setTranslationsLoaded(false); setMenuTranslations({ title: "메뉴 관리", description: "시스템의 메뉴 구조와 권한을 관리합니다.", }); // 새로운 번역 로드 loadTranslations(); } }, [userLang]); // 컴포넌트 마운트 시 강제로 번역 로드 (userLang이 아직 설정되지 않았을 수 있음) useEffect(() => { const timer = setTimeout(() => { if (!userLang) { console.log("🔄 Admin Layout 마운트 후 강제 번역 로드 (userLang 없음)"); loadTranslations(); } }, 100); // 100ms 후 실행 return () => clearTimeout(timer); }, []); // 컴포넌트 마운트 시 한 번만 실행 // 키보드 단축키로 사이드바 토글 useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Ctrl+B 또는 Cmd+B로 사이드바 토글 if ((event.ctrlKey || event.metaKey) && event.key === "b") { event.preventDefault(); setSidebarOpen((prev) => !prev); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, []); // 번역 로드 완료 이벤트 감지 useEffect(() => { const handleTranslationLoaded = (event: any) => { const { key, text } = event.detail; if (key === MENU_MANAGEMENT_KEYS.TITLE) { setMenuTranslations((prev) => ({ ...prev, title: text })); } else if (key === MENU_MANAGEMENT_KEYS.DESCRIPTION) { setMenuTranslations((prev) => ({ ...prev, description: text })); } setForceUpdate((prev) => prev + 1); }; window.addEventListener("translation-loaded", handleTranslationLoaded); return () => window.removeEventListener("translation-loaded", handleTranslationLoaded); }, []); const loadTranslations = async () => { try { // 현재 사용자 언어 사용 const currentUserLang = userLang || "en"; console.log("🌐 Admin Layout 번역 로드 시작", { userLang, currentUserLang, }); // API 직접 호출로 현재 언어 사용 (배치 조회 방식) const companyCode = "*"; try { // 배치 조회 API 사용 const response = await apiClient.post( "/multilang/batch", { langKeys: [MENU_MANAGEMENT_KEYS.TITLE, MENU_MANAGEMENT_KEYS.DESCRIPTION], }, { params: { companyCode, menuCode: "MENU_MANAGEMENT", userLang: currentUserLang, }, }, ); if (response.data.success && response.data.data) { const translations = response.data.data; const title = translations[MENU_MANAGEMENT_KEYS.TITLE] || "메뉴 관리"; const description = translations[MENU_MANAGEMENT_KEYS.DESCRIPTION] || "시스템의 메뉴 구조와 권한을 관리합니다."; // 번역 캐시에 저장 setTranslationCache(currentUserLang, translations); // 상태 업데이트 setMenuTranslations({ title, description }); console.log("🌐 Admin Layout 번역 로드 완료 (배치)", { title, description, userLang: currentUserLang }); } else { // 기본값 사용 const title = "메뉴 관리"; const description = "시스템의 메뉴 구조와 권한을 관리합니다."; setMenuTranslations({ title, description }); console.log("🌐 Admin Layout 기본값 사용", { title, description, userLang: currentUserLang }); } } catch (error) { console.error("❌ Admin Layout 배치 번역 로드 실패:", error); // 오류 시 기본값 사용 const title = "메뉴 관리"; const description = "시스템의 메뉴 구조와 권한을 관리합니다."; setMenuTranslations({ title, description }); } } catch (error) { console.error("❌ Admin Layout 번역 로드 실패:", error); } }; const loadAdminMenus = async () => { try { setLoading(true); const response = await menuApi.getAdminMenus(); if (response.success && response.data) { // 메뉴 데이터를 관리자 메뉴 형식으로 변환 const convertedMenus = response.data.map((menu: MenuItem) => ({ id: menu.objid || menu.OBJID, name: menu.translated_name || menu.TRANSLATED_NAME || menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음", icon: Settings, // 기본 아이콘 description: menu.translated_desc || menu.TRANSLATED_DESC || menu.menu_desc || menu.MENU_DESC || "메뉴 설명", url: menu.menu_url || menu.MENU_URL, menuType: menu.menu_type || menu.MENU_TYPE, // 번역 키 정보 저장 (언어 변경 시 사용) lang_key: menu.lang_key || menu.LANG_KEY, lang_key_desc: menu.lang_key_desc || menu.LANG_KEY_DESC, // 레벨 정보 추가 lev: (menu as any).lev || (menu as any).LEV, })); // 동적으로 생성된 메뉴들만 사용 console.log("📋 로드된 메뉴 목록:"); convertedMenus.forEach((menu, index) => { console.log(` ${index + 1}. ID: ${menu.id}, 이름: ${menu.name}, URL: ${menu.url}, 레벨: ${menu.lev}`); }); setAdminMenus(convertedMenus); // 백엔드에서 이미 번역된 데이터를 제공하므로 별도 번역 로드 불필요 // await loadDynamicMenuTranslations(convertedMenus); } } catch (error) { console.error("관리자 메뉴 로드 오류:", error); } finally { setLoading(false); } }; const currentMenu = adminMenus.find((menu) => menu.id === activeMenu) || adminMenus[0]; // 메뉴 클릭 시 URL 이동 처리 const handleMenuClick = (menu: any) => { console.log("=== 메뉴 클릭 ==="); console.log("클릭된 메뉴:", menu); // 메뉴 클릭 시 토큰 재확인 const token = localStorage.getItem("authToken"); console.log("메뉴 클릭 시 토큰 확인:", token ? "존재" : "없음"); if (!token) { console.log("❌ 메뉴 클릭 시 토큰이 없음 - 경고 표시"); alert("인증 토큰이 없습니다. 다시 로그인해주세요."); window.location.href = "/login"; return; } setActiveMenu(menu.id); if (menu.url) { // 외부 URL인 경우 새 탭에서 열기 if (menu.url.startsWith("http://") || menu.url.startsWith("https://")) { console.log("외부 URL 열기:", menu.url); window.open(menu.url, "_blank"); } else { // 내부 URL인 경우 라우터로 이동 console.log("내부 URL 이동:", menu.url); router.push(menu.url); } } else { // URL이 없는 메뉴는 현재 페이지에서 컨텐츠만 변경 console.log("URL이 없는 메뉴:", menu); } }; return (
{/* 인증 상태 확인 */} {isAuthorized === null && (

로딩 중...

)} {isAuthorized === false && (

인증 실패

토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.

디버깅 정보

현재 경로: {pathname}

토큰: {localStorage.getItem("authToken") ? "존재" : "없음"}

)} {isAuthorized === true && ( <> {/* 왼쪽 사이드바 */}
{sidebarOpen && ( <>

관리자 설정

시스템 관리 도구

)} {!sidebarOpen && ( )}
{/* 오른쪽 컨텐츠 영역 */}

{pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") ? menuTranslations.title : currentMenu?.name || "관리자 설정"}

{pathname === "/admin/menu" || pathname.startsWith("/admin/menu/") ? menuTranslations.description : currentMenu?.description || "시스템 관리 도구"}

{children}
)}
); }