"use client"; import { useState, Suspense, useEffect } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Shield, Menu, Home, Settings, BarChart3, FileText, Users, Package, ChevronDown, ChevronRight, UserCheck, LogOut, User, Building2, } 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 { menuScreenApi } from "@/lib/api/screen"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; import { ProfileModal } from "./ProfileModal"; import { Logo } from "./Logo"; import { SideMenu } from "./SideMenu"; import { TabBar } from "./TabBar"; import { TabContent } from "./TabContent"; import { useTabStore } from "@/stores/tabStore"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { CompanySwitcher } from "@/components/admin/CompanySwitcher"; import { getIconComponent } from "@/components/admin/MenuIconPicker"; // 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; } // 메뉴 아이콘 매핑 함수 (DB 아이콘 우선, 없으면 키워드 기반 fallback) const getMenuIcon = (menuName: string, dbIconName?: string | null) => { if (dbIconName) { const DbIcon = getIconComponent(dbIconName); if (DbIcon) return ; } const name = menuName.toLowerCase(); if (name.includes("대시보드") || name.includes("dashboard")) return ; if (name.includes("관리자") || name.includes("admin")) return ; if (name.includes("사용자") || name.includes("user")) return ; if (name.includes("프로젝트") || name.includes("project")) return ; if (name.includes("제품") || name.includes("product")) return ; if (name.includes("설정") || name.includes("setting")) return ; if (name.includes("로그") || name.includes("log")) return ; if (name.includes("메뉴") || name.includes("menu")) return ; if (name.includes("화면관리") || name.includes("screen")) return ; return ; }; // 메뉴 데이터를 UI용으로 변환하는 함수 (최상위 "사용자", "관리자" 제외) // parentPath: 탭 제목에 "기준정보 - 회사관리" 형태로 상위 카테고리를 포함하기 위한 경로 const convertMenuToUI = (menus: MenuItem[], userInfo: ExtendedUserInfo | null, parentId: string = "0", parentPath: string = ""): 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, parentPath)); }; // 단일 메뉴 변환 함수 const convertSingleMenu = (menu: MenuItem, allMenus: MenuItem[], userInfo: ExtendedUserInfo | null, parentPath: string = ""): any => { const menuId = menu.objid || menu.OBJID; // 사용자 locale 기준으로 번역 처리 const getDisplayText = (m: MenuItem) => { if (m.translated_name || m.TRANSLATED_NAME) { return m.translated_name || m.TRANSLATED_NAME; } const baseName = m.menu_name_kor || m.MENU_NAME_KOR || "메뉴명 없음"; 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 displayName = getDisplayText(menu); const tabTitle = parentPath ? `${parentPath} - ${displayName}` : displayName; const children = convertMenuToUI(allMenus, userInfo, menuId, tabTitle); return { id: menuId, name: displayName, tabTitle, icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || "", menu.menu_icon || menu.MENU_ICON), url: menu.menu_url || menu.MENU_URL || "#", children: children.length > 0 ? children : undefined, hasChildren: children.length > 0, }; }; function AppLayoutInner({ children }: AppLayoutProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const { user, logout, refreshUserData, switchCompany } = useAuth(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); const [sidebarOpen, setSidebarOpen] = useState(true); const [expandedMenus, setExpandedMenus] = useState>(new Set()); const [isMobile, setIsMobile] = useState(false); const [showCompanySwitcher, setShowCompanySwitcher] = useState(false); const [currentCompanyName, setCurrentCompanyName] = useState(""); // URL 직접 접근 시 탭 자동 열기 (북마크/공유 링크 대응) useEffect(() => { const store = useTabStore.getState(); const currentModeTabs = store[store.mode].tabs; if (currentModeTabs.length > 0) return; // /screens/[screenId] 패턴 감지 const screenMatch = pathname.match(/^\/screens\/(\d+)/); if (screenMatch) { const screenId = parseInt(screenMatch[1]); const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined; store.openTab({ type: "screen", title: `화면 ${screenId}`, screenId, menuObjid }); return; } // /admin/* 패턴 감지 -> admin 모드로 전환 후 탭 열기 if (pathname.startsWith("/admin") && pathname !== "/admin") { store.setMode("admin"); store.openTab({ type: "admin", title: pathname.split("/").pop() || "관리자", adminUrl: pathname }); } }, []); // 마운트 시 1회만 실행 // 현재 회사명 조회 (SUPER_ADMIN 전용) useEffect(() => { const fetchCurrentCompanyName = async () => { if ((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN") { const companyCode = (user as ExtendedUserInfo)?.companyCode; if (companyCode === "*") { setCurrentCompanyName("WACE (최고 관리자)"); } else if (companyCode) { try { const response = await apiClient.get("/admin/companies/db"); if (response.data.success) { const company = response.data.data.find((c: any) => c.company_code === companyCode); setCurrentCompanyName(company?.company_name || companyCode); } } catch (error) { setCurrentCompanyName(companyCode); } } } }; fetchCurrentCompanyName(); }, [(user as ExtendedUserInfo)?.companyCode, (user as ExtendedUserInfo)?.userType]); // 화면 크기 감지 및 사이드바 초기 상태 설정 useEffect(() => { const checkIsMobile = () => { const mobile = window.innerWidth < 1024; // lg 브레이크포인트 setIsMobile(mobile); // 모바일에서만 사이드바를 닫음 if (mobile) { setSidebarOpen(false); } else { setSidebarOpen(true); } }; checkIsMobile(); window.addEventListener("resize", checkIsMobile); return () => window.removeEventListener("resize", checkIsMobile); }, []); // 프로필 관련 로직 const { isModalOpen, formData, selectedImage, isSaving, departments, alertModal, closeAlert, openProfileModal, closeProfileModal, updateFormData, selectImage, removeImage, saveProfile, // 운전자 관련 isDriver, hasVehicle, driverInfo, driverFormData, updateDriverFormData, handleDriverStatusChange, handleDriverAccountDelete, handleDeleteVehicle, openVehicleRegisterModal, closeVehicleRegisterModal, isVehicleRegisterModalOpen, newVehicleData, updateNewVehicleData, handleRegisterVehicle, } = useProfile(user, refreshUserData, refreshMenus); // 탭 스토어에서 현재 모드 가져오기 const tabMode = useTabStore((s) => s.mode); const setTabMode = useTabStore((s) => s.setMode); const isAdminMode = tabMode === "admin"; // 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (iframe 임베드용) const isPreviewMode = searchParams.get("preview") === "true"; // 현재 모드에 따라 표시할 메뉴 결정 // 관리자 모드에서는 관리자 메뉴만, 사용자 모드에서는 사용자 메뉴만 표시 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 { openTab } = useTabStore(); // 메뉴 클릭 핸들러 (탭으로 열기) const handleMenuClick = async (menu: any) => { if (menu.hasChildren) { toggleMenu(menu.id); } else { // tabTitle: "기준정보 - 회사관리" 형태의 상위 포함 이름 const menuName = menu.tabTitle || menu.label || menu.name || "메뉴"; if (typeof window !== "undefined") { localStorage.setItem("currentMenuName", menuName); } try { const menuObjid = menu.objid || menu.id; const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid); if (assignedScreens.length > 0) { const firstScreen = assignedScreens[0]; openTab({ type: "screen", title: menuName, screenId: firstScreen.screenId, menuObjid: parseInt(menuObjid), }); if (isMobile) setSidebarOpen(false); return; } } catch (error) { console.warn("할당된 화면 조회 실패:", error); } if (menu.url && menu.url !== "#") { openTab({ type: "admin", title: menuName, adminUrl: menu.url, }); if (isMobile) setSidebarOpen(false); } else { toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다."); } } }; // 모드 전환 핸들러 // 모드 전환: 탭 스토어의 모드만 변경 (각 모드 탭은 독립 보존) const handleModeSwitch = () => { setTabMode(isAdminMode ? "user" : "admin"); }; // 로그아웃 핸들러 const handleLogout = async () => { try { await logout(); router.push("/login"); } catch (error) { // 로그아웃 실패 시 처리 } }; // 사이드바 메뉴 -> 탭 바 드래그용 데이터 생성 const buildMenuDragData = async (menu: any): Promise => { const menuName = menu.label || menu.name || "메뉴"; const menuObjid = menu.objid || menu.id; try { const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid); if (assignedScreens.length > 0) { return JSON.stringify({ type: "screen" as const, title: menuName, screenId: assignedScreens[0].screenId, menuObjid: parseInt(menuObjid), }); } } catch { /* ignore */ } if (menu.url && menu.url !== "#") { return JSON.stringify({ type: "admin" as const, title: menuName, adminUrl: menu.url, }); } return null; }; const handleMenuDragStart = (e: React.DragEvent, menu: any) => { if (menu.hasChildren) { e.preventDefault(); return; } e.dataTransfer.effectAllowed = "copy"; const menuName = menu.tabTitle || menu.label || menu.name || "메뉴"; const menuObjid = menu.objid || menu.id; const dragPayload = JSON.stringify({ menuName, menuObjid, url: menu.url }); e.dataTransfer.setData("application/tab-menu-pending", dragPayload); e.dataTransfer.setData("text/plain", menuName); }; // 메뉴 트리 렌더링 (드래그 가능) const renderMenu = (menu: any, level: number = 0) => { const isExpanded = expandedMenus.has(menu.id); const isLeaf = !menu.hasChildren; return (
handleMenuDragStart(e, menu)} className={`group flex h-10 cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${ pathname === menu.url ? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900" : 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)} >
{menu.icon} {menu.name}
{menu.hasChildren && (
{isExpanded ? : }
)}
{/* 서브메뉴 */} {menu.hasChildren && isExpanded && (
{menu.children?.map((child: any) => (
handleMenuDragStart(e, child)} className={`flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors hover:cursor-pointer ${ pathname === child.url ? "border-primary border-l-4 bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900" : "text-slate-600 hover:bg-slate-50 hover:text-slate-900" }`} onClick={() => handleMenuClick(child)} >
{child.icon} {child.name}
))}
)}
); }; // 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (인증 대기 없이 즉시 렌더링) if (isPreviewMode) { return (
{children}
); } // 사용자 정보가 없으면 로딩 표시 if (!user) { return (

로딩중...

); } // UI 변환된 메뉴 데이터 const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); return (
{/* 모바일 헤더 - 모바일에서만 표시 */} {isMobile && (
{/* 햄버거 메뉴 버튼 */} setSidebarOpen(!sidebarOpen)} />
{/* 사용자 드롭다운 */}

{user.userName || "사용자"}

{user.deptName || user.email || user.userId}

프로필 로그아웃
)} {/* 메인 컨테이너 - 모바일에서는 헤더 높이만큼 패딩 */}
{/* 모바일 사이드바 오버레이 */} {sidebarOpen && isMobile && (
setSidebarOpen(false)} /> )} {/* 왼쪽 사이드바 */} {/* 가운데 컨텐츠 영역 - 탭 시스템 */}
{/* 프로필 수정 모달 */} {/* 회사 전환 모달 (WACE 관리자 전용) */} 회사 선택 관리할 회사를 선택하면 해당 회사의 관점에서 시스템을 사용할 수 있습니다.
setShowCompanySwitcher(false)} isOpen={showCompanySwitcher} />
); } export function AppLayout({ children }: AppLayoutProps) { return (

로딩중...

} > {children} ); }