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

408 lines
13 KiB
TypeScript
Raw Normal View History

2025-08-26 17:20:45 +09:00
"use client";
import { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Shield,
Menu,
Home,
Settings,
BarChart3,
FileText,
Users,
Package,
ChevronDown,
ChevronRight,
UserCheck,
} from "lucide-react";
import { useMenu } from "@/contexts/MenuContext";
import { useAuth } from "@/hooks/useAuth";
import { useProfile } from "@/hooks/useProfile";
import { MenuItem } from "@/lib/api/menu";
import { MainHeader } from "./MainHeader";
import { ProfileModal } from "./ProfileModal";
2025-08-26 17:51:23 +09:00
import { PageHeader } from "./PageHeader";
import { getPageInfo } from "@/constants/pageInfo";
2025-08-26 17:20:45 +09:00
// 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;
}
interface AppLayoutProps {
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[] => {
const filteredMenus = 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));
// 최상위 레벨에서 "사용자", "관리자" 카테고리가 있으면 그 하위 메뉴들을 직접 표시
if (parentId === "0") {
const allMenus: any[] = [];
for (const menu of filteredMenus) {
const menuName = (menu.menu_name_kor || menu.MENU_NAME_KOR || "").toLowerCase();
// "사용자" 또는 "관리자" 카테고리면 하위 메뉴들을 직접 추가
if (menuName.includes("사용자") || menuName.includes("관리자")) {
const childMenus = convertMenuToUI(menus, userInfo, menu.objid || menu.OBJID);
allMenus.push(...childMenus);
} else {
// 일반 메뉴는 그대로 추가
allMenus.push(convertSingleMenu(menu, menus, userInfo));
}
}
return allMenus;
}
return filteredMenus.map((menu) => convertSingleMenu(menu, menus, userInfo));
};
// 단일 메뉴 변환 함수
const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null): any => {
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 children = convertMenuToUI(allMenus, userInfo, menuId);
return {
id: menuId,
name: getDisplayText(menu),
icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || ""),
url: menu.menu_url || menu.MENU_URL || "#",
children: children.length > 0 ? children : undefined,
hasChildren: children.length > 0,
};
};
export function AppLayout({ children }: AppLayoutProps) {
const router = useRouter();
const pathname = usePathname();
const { user, logout, refreshUserData } = useAuth();
const { userMenus, adminMenus, loading } = useMenu();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
// 프로필 관련 로직
const {
isModalOpen,
formData,
selectedImage,
isSaving,
alertModal,
closeAlert,
openProfileModal,
closeProfileModal,
updateFormData,
selectImage,
removeImage,
saveProfile,
} = useProfile(user, refreshUserData);
// 현재 경로에 따라 어드민 모드인지 판단
const isAdminMode = pathname.startsWith("/admin");
// 현재 모드에 따라 표시할 메뉴 결정
const currentMenus = isAdminMode ? adminMenus : userMenus;
// 메뉴 토글 함수
const toggleMenu = (menuId: string) => {
const newExpanded = new Set(expandedMenus);
if (newExpanded.has(menuId)) {
newExpanded.delete(menuId);
} else {
newExpanded.add(menuId);
}
setExpandedMenus(newExpanded);
};
// 메뉴 클릭 핸들러
const handleMenuClick = (menu: any) => {
if (menu.hasChildren) {
toggleMenu(menu.id);
} else if (menu.url && menu.url !== "#") {
router.push(menu.url);
setSidebarOpen(false);
}
};
// 모드 전환 핸들러
const handleModeSwitch = () => {
if (isAdminMode) {
router.push("/main");
} else {
router.push("/admin");
}
};
// 로그아웃 핸들러
const handleLogout = async () => {
try {
await logout();
router.push("/login");
} catch (error) {
console.error("로그아웃 실패:", error);
}
};
// 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용)
const renderMenu = (menu: any, level: number = 0) => {
const isExpanded = expandedMenus.has(menu.id);
return (
<div key={menu.id}>
<div
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:cursor-pointer ${
isExpanded ? "bg-slate-100 text-slate-900" : "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
} ${level > 0 ? "ml-6" : ""}`}
onClick={() => handleMenuClick(menu)}
>
<div className="flex items-center">
{menu.icon}
<span className="ml-3">{menu.name}</span>
</div>
{menu.hasChildren && (
<div className="ml-auto">
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</div>
)}
</div>
{/* 서브메뉴 */}
{menu.hasChildren && isExpanded && (
<div className="mt-1 space-y-1 pl-6">
{menu.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:cursor-pointer hover:bg-slate-50 hover:text-slate-900"
onClick={() => handleMenuClick(child)}
>
{child.icon}
<span className="ml-3">{child.name}</span>
</div>
))}
</div>
)}
</div>
);
};
// 사용자 정보가 없으면 로딩 표시
if (!user) {
return (
<div className="flex h-screen items-center justify-center">
2025-08-26 17:51:23 +09:00
<div className="flex flex-col items-center">
2025-08-26 17:20:45 +09:00
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
2025-08-26 17:51:23 +09:00
<p>...</p>
2025-08-26 17:20:45 +09:00
</div>
</div>
);
}
// UI 변환된 메뉴 데이터
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
return (
<div className="flex h-screen flex-col bg-slate-50">
{/* MainHeader 컴포넌트 사용 */}
<MainHeader
user={user}
onSidebarToggle={() => setSidebarOpen(!sidebarOpen)}
onProfileClick={openProfileModal}
onLogout={handleLogout}
/>
<div className="flex flex-1">
{/* 모바일 사이드바 오버레이 */}
{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-14 left-0 z-40 flex h-full 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`}
>
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
{(user as ExtendedUserInfo)?.userType === "admin" && (
<div className="border-b border-slate-200 p-3">
<Button
onClick={handleModeSwitch}
className={`flex w-full items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 hover:cursor-pointer ${
isAdminMode
? "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100"
: "border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100"
}`}
>
{isAdminMode ? (
<>
<UserCheck className="h-4 w-4" />
</>
) : (
<>
<Shield className="h-4 w-4" />
</>
)}
</Button>
</div>
)}
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-1 px-3">
{loading ? (
<div className="animate-pulse space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-8 rounded bg-slate-200"></div>
))}
</div>
) : (
uiMenus.map((menu) => renderMenu(menu))
)}
</nav>
</div>
</aside>
{/* 가운데 컨텐츠 영역 */}
2025-08-26 17:51:23 +09:00
<main className="bg-background flex-1 p-6">
<PageHeader title={getPageInfo(pathname).title} description={getPageInfo(pathname).description} />
{children}
</main>
2025-08-26 17:20:45 +09:00
</div>
{/* 프로필 수정 모달 */}
<ProfileModal
isOpen={isModalOpen}
user={user}
formData={formData}
selectedImage={selectedImage}
isSaving={isSaving}
alertModal={alertModal}
onClose={closeProfileModal}
onFormChange={updateFormData}
onImageSelect={selectImage}
onImageRemove={removeImage}
onSave={saveProfile}
onAlertClose={closeAlert}
/>
</div>
);
}