594 lines
22 KiB
TypeScript
594 lines
22 KiB
TypeScript
"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<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 [isAuthorized, setIsAuthorized] = useState<boolean | null>(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 = (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 companyCode = "*";
|
|
|
|
const [titleResponse, descriptionResponse] = await Promise.all([
|
|
apiClient.get(
|
|
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.TITLE}?userLang=${currentUserLang}`,
|
|
),
|
|
apiClient.get(
|
|
`/multilang/user-text/${companyCode}/MENU_MANAGEMENT/${MENU_MANAGEMENT_KEYS.DESCRIPTION}?userLang=${currentUserLang}`,
|
|
),
|
|
]);
|
|
|
|
const titleData = titleResponse.data;
|
|
const descriptionData = descriptionResponse.data;
|
|
|
|
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) => {
|
|
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 (
|
|
<div className="bg-background flex h-screen">
|
|
{/* 인증 상태 확인 */}
|
|
{isAuthorized === null && (
|
|
<div className="flex h-screen w-full items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="mb-4 text-2xl font-bold">로딩 중...</h1>
|
|
<LoadingSpinner />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isAuthorized === false && (
|
|
<div className="flex h-screen w-full items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="mb-4 text-2xl font-bold text-red-600">인증 실패</h1>
|
|
<p className="mb-4">토큰이 없습니다. 3초 후 로그인 페이지로 이동합니다.</p>
|
|
<div className="rounded bg-yellow-100 p-4">
|
|
<h2 className="mb-2 font-semibold">디버깅 정보</h2>
|
|
<p>현재 경로: {pathname}</p>
|
|
<p>토큰: {localStorage.getItem("authToken") ? "존재" : "없음"}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isAuthorized === true && (
|
|
<>
|
|
{/* 왼쪽 사이드바 */}
|
|
<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>
|
|
);
|
|
}
|