408 lines
15 KiB
TypeScript
408 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Shield,
|
|
User,
|
|
LogOut,
|
|
Menu,
|
|
X,
|
|
Home,
|
|
Settings,
|
|
BarChart3,
|
|
FileText,
|
|
Users,
|
|
Package,
|
|
Wrench,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
} from "lucide-react";
|
|
import { useMenu } from "@/contexts/MenuContext";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { MenuItem } from "@/lib/api/menu";
|
|
|
|
// useAuth의 UserInfo 타입을 확장
|
|
interface ExtendedUserInfo {
|
|
userId: string;
|
|
userName: string;
|
|
userNameEng?: string;
|
|
userNameCn?: string;
|
|
deptCode?: string;
|
|
deptName?: string;
|
|
positionCode?: string;
|
|
positionName?: string;
|
|
email?: string;
|
|
tel?: string;
|
|
cellPhone?: string;
|
|
userType?: string;
|
|
userTypeName?: string;
|
|
authName?: string;
|
|
partnerCd?: string;
|
|
isAdmin: boolean;
|
|
sabun?: string;
|
|
photo?: string | null;
|
|
companyCode?: string;
|
|
locale?: string;
|
|
}
|
|
|
|
// useAuth의 UserInfo 타입을 사용하므로 별도 정의 불필요
|
|
|
|
interface MainLayoutProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
// 메뉴 아이콘 매핑 함수
|
|
const getMenuIcon = (menuName: string) => {
|
|
const name = menuName.toLowerCase();
|
|
if (name.includes("대시보드") || name.includes("dashboard")) return <Home className="h-4 w-4" />;
|
|
if (name.includes("관리자") || name.includes("admin")) return <Shield className="h-4 w-4" />;
|
|
if (name.includes("사용자") || name.includes("user")) return <Users className="h-4 w-4" />;
|
|
if (name.includes("프로젝트") || name.includes("project")) return <BarChart3 className="h-4 w-4" />;
|
|
if (name.includes("제품") || name.includes("product")) return <Package className="h-4 w-4" />;
|
|
if (name.includes("설정") || name.includes("setting")) return <Settings className="h-4 w-4" />;
|
|
if (name.includes("로그") || name.includes("log")) return <FileText className="h-4 w-4" />;
|
|
if (name.includes("메뉴") || name.includes("menu")) return <Menu className="h-4 w-4" />;
|
|
return <FileText className="h-4 w-4" />;
|
|
};
|
|
|
|
// 메뉴 데이터를 UI용으로 변환하는 함수
|
|
const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0"): any[] => {
|
|
return menus
|
|
.filter((menu) => (menu.parent_obj_id || menu.PARENT_OBJ_ID) === parentId)
|
|
.filter((menu) => (menu.status || menu.STATUS) === "active")
|
|
.sort((a, b) => (a.seq || a.SEQ || 0) - (b.seq || b.SEQ || 0))
|
|
.map((menu) => {
|
|
const menuId = menu.objid || menu.OBJID;
|
|
|
|
// 사용자 locale 기준으로 번역 처리
|
|
const getDisplayText = (menu: MenuItem) => {
|
|
// 다국어 텍스트가 있으면 사용, 없으면 기본 텍스트 사용
|
|
if (menu.translated_name || menu.TRANSLATED_NAME) {
|
|
return menu.translated_name || menu.TRANSLATED_NAME;
|
|
}
|
|
|
|
const baseName = menu.menu_name_kor || menu.MENU_NAME_KOR || "메뉴명 없음";
|
|
|
|
// 사용자 정보에서 locale 가져오기
|
|
const userLocale = userInfo?.locale || "ko";
|
|
|
|
if (userLocale === "EN") {
|
|
// 영어 번역
|
|
const translations: { [key: string]: string } = {
|
|
관리자: "Administrator",
|
|
사용자: "User Management",
|
|
메뉴: "Menu Management",
|
|
대시보드: "Dashboard",
|
|
권한: "Permission Management",
|
|
코드: "Code Management",
|
|
설정: "Settings",
|
|
로그: "Log Management",
|
|
프로젝트: "Project Management",
|
|
제품: "Product Management",
|
|
};
|
|
|
|
for (const [korean, english] of Object.entries(translations)) {
|
|
if (baseName.includes(korean)) {
|
|
return baseName.replace(korean, english);
|
|
}
|
|
}
|
|
} else if (userLocale === "JA") {
|
|
// 일본어 번역
|
|
const translations: { [key: string]: string } = {
|
|
관리자: "管理者",
|
|
사용자: "ユーザー管理",
|
|
메뉴: "メニュー管理",
|
|
대시보드: "ダッシュボード",
|
|
권한: "権限管理",
|
|
코드: "コード管理",
|
|
설정: "設定",
|
|
로그: "ログ管理",
|
|
프로젝트: "プロジェクト管理",
|
|
제품: "製品管理",
|
|
};
|
|
|
|
for (const [korean, japanese] of Object.entries(translations)) {
|
|
if (baseName.includes(korean)) {
|
|
return baseName.replace(korean, japanese);
|
|
}
|
|
}
|
|
} else if (userLocale === "ZH") {
|
|
// 중국어 번역
|
|
const translations: { [key: string]: string } = {
|
|
관리자: "管理员",
|
|
사용자: "用户管理",
|
|
메뉴: "菜单管理",
|
|
대시보드: "仪表板",
|
|
권한: "权限管理",
|
|
코드: "代码管理",
|
|
설정: "设置",
|
|
로그: "日志管理",
|
|
프로젝트: "项目管理",
|
|
제품: "产品管理",
|
|
};
|
|
|
|
for (const [korean, chinese] of Object.entries(translations)) {
|
|
if (baseName.includes(korean)) {
|
|
return baseName.replace(korean, chinese);
|
|
}
|
|
}
|
|
}
|
|
|
|
return baseName;
|
|
};
|
|
|
|
const menuName = getDisplayText(menu);
|
|
const menuUrl = menu.menu_url || menu.MENU_URL;
|
|
|
|
console.log(
|
|
`메뉴 ${menuId}: 번역명="${menu.translated_name || menu.TRANSLATED_NAME}", 기본명="${menu.menu_name_kor || menu.MENU_NAME_KOR}", 최종명="${menuName}"`,
|
|
);
|
|
|
|
const children = convertMenuToUI(menus, userInfo, menuId);
|
|
|
|
return {
|
|
id: menuId,
|
|
title: menuName,
|
|
icon: getMenuIcon(menuName || ""),
|
|
path: menuUrl,
|
|
children: children.length > 0 ? children : undefined,
|
|
};
|
|
});
|
|
};
|
|
|
|
export default function MainLayout({ children }: MainLayoutProps) {
|
|
const router = useRouter();
|
|
const { adminMenus, userMenus, loading: menuLoading } = useMenu();
|
|
const { user, loading: authLoading, logout } = useAuth();
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
const [expandedMenus, setExpandedMenus] = useState<string[]>([]);
|
|
|
|
// 사용자의 회사 코드에 따라 메뉴 필터링
|
|
const filterMenusByCompany = (menus: MenuItem[]) => {
|
|
console.log("=== 메뉴 필터링 시작 ===");
|
|
console.log("사용자 정보:", user);
|
|
console.log("사용자 회사 코드:", (user as ExtendedUserInfo)?.companyCode);
|
|
console.log("전체 메뉴 수:", menus.length);
|
|
console.log("현재 경로:", window.location.pathname);
|
|
|
|
// 모든 메뉴 표시 (원래 상태로 복원)
|
|
console.log("모든 메뉴 표시");
|
|
return menus;
|
|
};
|
|
|
|
// 활성 메뉴만 필터링하여 UI용으로 변환 (언어 변경 시 재계산)
|
|
const activeMenus = [...adminMenus, ...userMenus].filter((menu) => (menu.status || menu.STATUS) === "active");
|
|
const filteredMenus = filterMenusByCompany(activeMenus);
|
|
const menuItems = convertMenuToUI(filteredMenus, user);
|
|
|
|
// 디버깅 로그 추가
|
|
console.log("=== MainLayout 메뉴 디버깅 ===");
|
|
console.log("adminMenus 개수:", adminMenus.length);
|
|
console.log("userMenus 개수:", userMenus.length);
|
|
console.log("activeMenus 개수:", activeMenus.length);
|
|
console.log("filteredMenus 개수:", filteredMenus.length);
|
|
console.log("menuItems 개수:", menuItems.length);
|
|
|
|
// 번역 데이터 확인
|
|
if (menuItems.length > 0) {
|
|
console.log("첫 번째 메뉴 아이템:", {
|
|
id: menuItems[0].id,
|
|
title: menuItems[0].title,
|
|
hasChildren: !!menuItems[0].children,
|
|
});
|
|
}
|
|
|
|
if (adminMenus.length > 0) {
|
|
console.log("첫 번째 adminMenu:", adminMenus[0]);
|
|
}
|
|
if (userMenus.length > 0) {
|
|
console.log("첫 번째 userMenu:", userMenus[0]);
|
|
}
|
|
|
|
// useAuth 훅에서 인증 상태를 관리하므로 별도 인증 확인 불필요
|
|
// useEffect(() => {
|
|
// checkAuthStatus();
|
|
// }, []);
|
|
|
|
// 키보드 단축키로 사이드바 토글
|
|
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);
|
|
}, []);
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
};
|
|
|
|
const toggleMenu = (menuId: string) => {
|
|
setExpandedMenus((prev) => (prev.includes(menuId) ? prev.filter((id) => id !== menuId) : [...prev, menuId]));
|
|
};
|
|
|
|
const handleMenuClick = (path?: string) => {
|
|
if (path) {
|
|
router.push(path);
|
|
}
|
|
};
|
|
|
|
if (authLoading) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<div className="text-center">
|
|
<Shield className="mx-auto mb-4 h-12 w-12 animate-pulse text-slate-400" />
|
|
<p className="text-slate-600">로딩 중...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen flex-col bg-slate-50">
|
|
{/* 상단 헤더 */}
|
|
<header className="z-10 border-b border-slate-200 bg-white shadow-sm">
|
|
<div className="flex h-16 items-center justify-between px-4">
|
|
<div className="flex items-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
className="mr-3"
|
|
title="사이드바 토글 (Ctrl+B)"
|
|
>
|
|
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</Button>
|
|
|
|
<div className="flex items-center">
|
|
<Shield className="mr-3 h-8 w-8 text-slate-900" />
|
|
<h1 className="text-xl font-semibold text-slate-900">PLM 솔루션</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center text-sm text-slate-600">
|
|
<User className="mr-2 h-4 w-4" />
|
|
<span className="font-medium">{user.userName}</span>
|
|
{user.deptName && <span className="ml-2 text-slate-400">({user.deptName})</span>}
|
|
</div>
|
|
|
|
<Button variant="outline" size="sm" onClick={handleLogout} className="text-slate-600 hover:text-slate-900">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
로그아웃
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* 모바일 사이드바 오버레이 */}
|
|
{sidebarOpen && (
|
|
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
|
|
)}
|
|
|
|
{/* 왼쪽 사이드바 */}
|
|
<aside
|
|
className={`${
|
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
} fixed top-16 left-0 z-40 flex h-[calc(100vh-4rem)] w-64 flex-col border-r border-slate-200 bg-white transition-transform duration-300 lg:relative lg:top-0 lg:z-auto lg:h-full lg:translate-x-0 lg:transform-none ${
|
|
sidebarOpen ? "lg:w-64" : "lg:w-16"
|
|
}`}
|
|
>
|
|
<div className="flex-1 overflow-y-auto py-4">
|
|
<nav className="space-y-1 px-3">
|
|
{menuItems.map((item) => (
|
|
<div key={item.id}>
|
|
<div
|
|
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
|
expandedMenus.includes(item.id)
|
|
? "bg-slate-100 text-slate-900"
|
|
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
|
}`}
|
|
onClick={() => {
|
|
if (item.children) {
|
|
toggleMenu(item.id);
|
|
} else {
|
|
handleMenuClick(item.path);
|
|
}
|
|
}}
|
|
>
|
|
<div className="flex items-center">
|
|
{item.icon}
|
|
{sidebarOpen && <span className="ml-3">{item.title}</span>}
|
|
</div>
|
|
{sidebarOpen && item.children && (
|
|
<div className="ml-auto">
|
|
{expandedMenus.includes(item.id) ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 서브메뉴 */}
|
|
{sidebarOpen && item.children && expandedMenus.includes(item.id) && (
|
|
<div className="mt-1 space-y-1 pl-6">
|
|
{item.children.map((child: any) => (
|
|
<div
|
|
key={child.id}
|
|
className="flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-900"
|
|
onClick={() => handleMenuClick(child.path)}
|
|
>
|
|
{child.icon}
|
|
<span className="ml-3">{child.title}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* 사이드바 하단 영역 */}
|
|
<div className="space-y-3 border-t border-slate-200 p-3">
|
|
{/* 사용자 locale 정보 표시 */}
|
|
{sidebarOpen && (user as ExtendedUserInfo)?.locale && (
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-slate-500">언어</span>
|
|
<span className="text-xs text-slate-700">{(user as ExtendedUserInfo).locale!.toUpperCase()}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 사이드바 토글 버튼 (데스크톱용) */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
className="hidden w-full justify-start lg:flex"
|
|
title={sidebarOpen ? "사이드바 접기" : "사이드바 펼치기"}
|
|
>
|
|
{sidebarOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
|
|
{sidebarOpen && <span className="ml-3">메뉴 접기</span>}
|
|
</Button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* 가운데 컨텐츠 영역 */}
|
|
<main className="flex-1 overflow-y-auto bg-slate-50">
|
|
<div className="h-full">{children}</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|