ERP-node/frontend/components/layout/MainLayout.tsx

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>
);
}