ERP-node/frontend/app/(main)/admin/layout.tsx

460 lines
17 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
"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 { 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";
// 아이콘 매핑
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<any[]>([]);
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 [menuTranslations, setMenuTranslations] = useState<{
title: string;
description: string;
}>({
title: "메뉴 관리",
description: "시스템의 메뉴 구조와 권한을 관리합니다.",
});
// 관리자 메뉴 로드
useEffect(() => {
loadAdminMenus();
}, []);
// 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 = (typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG) || userLang || "KR";
console.log("🌐 Admin Layout 번역 로드 시작", {
userLang,
globalUserLang: typeof window !== "undefined" && (window as any).__GLOBAL_USER_LANG,
currentUserLang,
});
// API 직접 호출로 현재 언어 사용
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
const companyCode = "*";
const [titleResponse, descriptionResponse] = await Promise.all([
fetch(
`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
{
credentials: "include",
headers: { "Content-Type": "application/json" },
},
),
fetch(
`${API_BASE_URL}/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
{
credentials: "include",
headers: { "Content-Type": "application/json" },
},
),
]);
const [titleData, descriptionData] = await Promise.all([titleResponse.json(), descriptionResponse.json()]);
const title = titleData.success ? titleData.data : "메뉴 관리";
const description = descriptionData.success ? descriptionData.data : "시스템의 메뉴 구조와 권한을 관리합니다.";
// 번역 캐시에 저장
const translations = {
[MENU_MANAGEMENT_KEYS.TITLE]: title,
[MENU_MANAGEMENT_KEYS.DESCRIPTION]: description,
};
setTranslationCache(currentUserLang, translations);
// 상태 업데이트
setMenuTranslations({ title, description });
console.log("🌐 Admin Layout 번역 로드 완료", { title, description, userLang: currentUserLang });
} 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) => {
setActiveMenu(menu.id);
if (menu.url) {
// 외부 URL인 경우 새 탭에서 열기
if (menu.url.startsWith("http://") || menu.url.startsWith("https://")) {
window.open(menu.url, "_blank");
} else {
// 내부 URL인 경우 라우터로 이동
router.push(menu.url);
}
} else {
// URL이 없는 메뉴는 현재 페이지에서 컨텐츠만 변경
console.log("URL이 없는 메뉴:", menu);
}
};
return (
<div className="bg-background flex h-screen">
{/* 왼쪽 사이드바 */}
<div className={cn("bg-muted/30 border-r p-6 transition-all duration-300", sidebarOpen ? "w-64" : "w-16")}>
<div className="mb-4 flex items-center justify-between">
{sidebarOpen && (
<>
<div>
<h2 className="text-lg font-semibold"> </h2>
<p className="text-muted-foreground text-sm"> </p>
</div>
<Button variant="ghost" size="sm" onClick={() => setSidebarOpen(false)} title="사이드바 접기 (Ctrl+B)">
<X className="h-4 w-4" />
</Button>
</>
)}
{!sidebarOpen && (
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen(true)}
title="사이드바 펼치기 (Ctrl+B)"
className="w-full justify-center"
>
<Menu className="h-4 w-4" />
</Button>
)}
</div>
<nav className="space-y-2">
{loading ? (
<div className="flex items-center justify-center p-4">
<LoadingSpinner />
</div>
) : (
adminMenus.map((menu: any) => {
const IconComponent = menu.icon;
const isActive = activeMenu === menu.id;
return (
<Button
key={menu.id}
variant={isActive ? "default" : "ghost"}
className={cn(
"h-10 w-full justify-start gap-2 transition-all duration-200",
isActive && "bg-primary text-primary-foreground",
!sidebarOpen && "justify-center px-2",
)}
onClick={() => handleMenuClick(menu)}
title={!sidebarOpen ? menu.name : undefined}
>
<IconComponent className="h-4 w-4 flex-shrink-0" />
{sidebarOpen && <span className="truncate">{menu.name}</span>}
</Button>
);
})
)}
</nav>
</div>
{/* 오른쪽 컨텐츠 영역 */}
<div className="flex-1 overflow-auto p-6">
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold">
{pathname === "/admin/menu" || pathname.startsWith("/admin/menu/")
? menuTranslations.title
: currentMenu?.name || "관리자 설정"}
</h1>
<p className="text-muted-foreground">
{pathname === "/admin/menu" || pathname.startsWith("/admin/menu/")
? menuTranslations.description
: currentMenu?.description || "시스템 관리 도구"}
</p>
</div>
{children}
</div>
</div>
</div>
);
}