"use client"; import { useState, Suspense, useEffect, useCallback } 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, FileCheck, Monitor, } from "lucide-react"; import { useMenu } from "@/contexts/MenuContext"; import { useAuth } from "@/hooks/useAuth"; import { useProfile } from "@/hooks/useProfile"; import { MenuItem, menuApi } 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 { ThemeToggle } from "./ThemeToggle"; import { CosmicBackground } from "./CosmicBackground"; import { useTheme } from "next-themes"; 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"; interface ExtendedUserInfo { user_id: string; user_name: string; userNameEng?: string; userNameCn?: string; deptCode?: string; dept_name?: string; positionCode?: string; position_name?: string; email?: string; tel?: string; cellPhone?: string; user_type?: string; userTypeName?: string; authName?: string; partnerCd?: string; isAdmin: boolean; sabun?: string; photo?: string | null; company_code?: string; locale?: string; } interface AppLayoutProps { children: React.ReactNode; } 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 ; }; 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; 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); const menuUrl = menu.menu_url || menu.MENU_URL || "#"; const screenCode = menu.screen_code || menu.SCREEN_CODE || null; const menuType = String(menu.menu_type ?? menu.MENU_TYPE ?? ""); let screenId: number | null = null; const screensMatch = menuUrl.match(/^\/screens\/(\d+)/); if (screensMatch) { screenId = parseInt(screensMatch[1]); } return { id: menuId, objid: menuId, name: displayName, tabTitle, icon: getMenuIcon(menu.menu_name_kor || menu.MENU_NAME_KOR || "", menu.menu_icon || menu.MENU_ICON), url: menuUrl, screenCode, screen_id: screenId, menu_type: menuType, 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 } = useAuth(); const { user_menus: userMenus, admin_menus: adminMenus, loading, refreshMenus } = useMenu(); const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [tabsCollapsed, setTabsCollapsed] = useState(false); const [flyoutMenu, setFlyoutMenu] = useState<{ menu: any; rect: DOMRect } | null>(null); const [modeTransition, setModeTransition] = useState<"idle" | "out" | "in">("idle"); const [expandedMenus, setExpandedMenus] = useState>(new Set()); const [isMobile, setIsMobile] = useState(false); const [showCompanySwitcher, setShowCompanySwitcher] = useState(false); const [currentCompanyName, setCurrentCompanyName] = useState(""); const { theme, setTheme: rawSetTheme } = useTheme(); const setNextTheme = useCallback((t: string) => { if (theme === t) return; const fade = document.getElementById("v5-theme-fade"); if (fade) { fade.style.background = t === "dark" ? "radial-gradient(ellipse at center,#0c0b18,#06050e)" : "radial-gradient(ellipse at center,#f3f2fa,#fafaff)"; fade.classList.add("in"); setTimeout(() => { rawSetTheme(t); setTimeout(() => fade.classList.remove("in"), 50); }, 420); } else { rawSetTheme(t); } }, [theme, rawSetTheme]); // URL 직접 접근 시 탭 자동 열기 useEffect(() => { const store = useTabStore.getState(); const currentModeTabs = store[store.mode].tabs; if (currentModeTabs.length > 0) return; const screenMatch = pathname.match(/^\/screens\/(\d+)/); if (screenMatch) { const screenId = parseInt(screenMatch[1]); const menu_objid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined; store.openTab({ type: "screen", title: `화면 ${screenId}`, screen_id: screenId, menu_objid }); return; } if (pathname.startsWith("/admin") && pathname !== "/admin") { store.setMode("admin"); store.openTab({ type: "admin", title: pathname.split("/").pop() || "관리자", admin_url: pathname }); } }, []); // 현재 회사명 조회 (SUPER_ADMIN 전용) useEffect(() => { const fetchCurrentCompanyName = async () => { if ((user as ExtendedUserInfo)?.user_type === "SUPER_ADMIN") { const companyCode = (user as ExtendedUserInfo)?.company_code; 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 { setCurrentCompanyName(companyCode); } } } }; fetchCurrentCompanyName(); }, [(user as ExtendedUserInfo)?.company_code, (user as ExtendedUserInfo)?.user_type]); // 화면 크기 감지 및 사이드바 초기 상태 설정 useEffect(() => { const checkIsMobile = () => { const mobile = window.innerWidth < 1024; 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"; const isPreviewMode = searchParams.get("preview") === "true"; const currentMenus = isAdminMode ? adminMenus : userMenus; const currentTabs = useTabStore((s) => s[s.mode].tabs); const currentActiveTabId = useTabStore((s) => s[s.mode].active_tab_id); const activeTab = currentTabs.find((t) => t.id === currentActiveTabId); 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); return; } const menuName = menu.tabTitle || menu.label || menu.name || "메뉴"; if (typeof window !== "undefined") { localStorage.setItem("currentMenuName", menuName); } const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0"); const isAdminMenu = menu.menu_type === "0"; console.log("[handleMenuClick] 메뉴 클릭:", { menuName, menuObjid, menu_type: menu.menu_type, isAdminMenu, screen_id: menu.screen_id, screenCode: menu.screenCode, url: menu.url, fullMenu: menu, }); // 관리자 메뉴 (menu_type = 0): URL 직접 입력 → admin 탭 if (isAdminMenu) { if (menu.url && menu.url !== "#") { console.log("[handleMenuClick] → admin 탭:", menu.url); openTab({ type: "admin", title: menuName, admin_url: menu.url }); if (isMobile) setSidebarOpen(false); } else { toast.warning("이 메뉴에는 연결된 페이지가 없습니다."); } return; } // 사용자 메뉴 (menu_type = 1, 2): 화면/대시보드 할당 // 1) screenId가 메뉴 URL에서 추출된 경우 바로 screen 탭 if (menu.screen_id) { console.log("[handleMenuClick] → screen 탭 (URL에서 screenId 추출):", menu.screen_id); openTab({ type: "screen", title: menuName, screen_id: menu.screen_id, menu_objid: menuObjid }); if (isMobile) setSidebarOpen(false); return; } // 2) screen_menu_assignments 테이블 조회 if (menuObjid) { try { console.log("[handleMenuClick] → screen_menu_assignments 조회 시도, menuObjid:", menuObjid); const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid); console.log("[handleMenuClick] → 조회 결과:", assignedScreens); if (assignedScreens.length > 0) { const assignedScreenId = assignedScreens[0].screen_id; console.log("[handleMenuClick] → screen 탭 (assignments):", assignedScreenId); openTab({ type: "screen", title: menuName, screen_id: assignedScreenId, menu_objid: menuObjid }); if (isMobile) setSidebarOpen(false); return; } } catch (err) { console.error("[handleMenuClick] 할당된 화면 조회 실패:", err); } } // 3) 대시보드 할당 (/dashboard/xxx) → admin 탭으로 렌더링 (AdminPageRenderer가 처리) if (menu.url && menu.url.startsWith("/dashboard/")) { console.log("[handleMenuClick] → 대시보드 탭:", menu.url); openTab({ type: "admin", title: menuName, admin_url: menu.url }); if (isMobile) setSidebarOpen(false); return; } // 4) 커스텀 페이지 URL (React 직접 구현 페이지) → admin 탭으로 렌더링 if (menu.url && menu.url !== "#" && !menu.url.startsWith("/screen/") && !menu.url.startsWith("/screens/")) { console.log("[handleMenuClick] → 커스텀 페이지 탭:", menu.url); openTab({ type: "admin", title: menuName, admin_url: menu.url }); if (isMobile) setSidebarOpen(false); return; } console.warn("[handleMenuClick] 어떤 조건에도 매칭 안 됨:", { menuName, menu_type: menu.menu_type, url: menu.url, screen_id: menu.screen_id }); toast.warning("이 메뉴에 할당된 화면이 없습니다. 메뉴 설정을 확인해주세요."); }; const handleModeSwitch = useCallback(() => { if (modeTransition !== "idle") return; setModeTransition("out"); // Phase 1: slide out sidebar + fade tabs (300ms) setTimeout(() => { // Phase 2: swap mode — React re-renders with new menus/tabs setTabMode(isAdminMode ? "user" : "admin"); setModeTransition("in"); // Phase 3: slide in completes, cleanup requestAnimationFrame(() => { setTimeout(() => setModeTransition("idle"), 450); }); }, 300); }, [isAdminMode, setTabMode, modeTransition]); const handleLogout = async () => { try { await logout(); router.push("/login"); } catch { // 로그아웃 실패 } }; 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); }; // POP 모드 진입 핸들러 const handlePopModeClick = async () => { try { const response = await menuApi.getPopMenus(); if (response.success && response.data) { const { childMenus, landingMenu } = response.data; if (landingMenu?.menu_url) { router.push(landingMenu.menu_url); } else if (childMenus.length === 0) { toast.info("설정된 POP 화면이 없습니다"); } else if (childMenus.length === 1) { router.push(childMenus[0].menu_url); } else { router.push("/pop"); } } else { toast.info("설정된 POP 화면이 없습니다"); } } catch (error) { toast.error("POP 메뉴 조회 중 오류가 발생했습니다"); } }; // pathname + 활성 탭 기반 활성 메뉴 판별 (탭 네비게이션에서도 사이드바 활성 표시) const isMenuActive = useCallback( (menu: any): boolean => { if (pathname === menu.url) return true; if (!activeTab) return false; const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0"); if (activeTab.type === "admin" && activeTab.admin_url) { return menu.url === activeTab.admin_url; } if (activeTab.type === "screen") { if (activeTab.menu_objid != null && menuObjid === activeTab.menu_objid) return true; const { screen_id: activeTabScreenId } = activeTab; if (activeTabScreenId != null && menu.screen_id === activeTabScreenId) return true; } return false; }, [pathname, activeTab], ); // 접힌 사이드바에서 부모 메뉴 클릭 → 플라이아웃 const handleCollapsedMenuClick = useCallback((menu: any, e: React.MouseEvent) => { if (!sidebarCollapsed || !menu.hasChildren) return false; e.stopPropagation(); const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); setFlyoutMenu((prev) => prev?.menu.id === menu.id ? null : { menu, rect }); return true; }, [sidebarCollapsed]); // 플라이아웃에서 메뉴 선택 const handleFlyoutSelect = useCallback((child: any) => { setFlyoutMenu(null); handleMenuClick(child); }, []); // 바깥 클릭 시 플라이아웃 닫기 useEffect(() => { if (!flyoutMenu) return; const close = (e: MouseEvent) => { if (!(e.target as HTMLElement).closest(".v5-side-flyout") && !(e.target as HTMLElement).closest(".v5-si")) { setFlyoutMenu(null); } }; document.addEventListener("click", close); return () => document.removeEventListener("click", close); }, [flyoutMenu]); // 메뉴 트리 렌더링 (v5 glassmorphism) const renderMenu = (menu: any, level: number = 0) => { const isExpanded = expandedMenus.has(menu.id); const isLeaf = !menu.hasChildren; return (
handleMenuDragStart(e, menu)} className={`v5-si ${isMenuActive(menu) ? "on" : ""} ${level > 0 ? "ml-6" : ""}`} title={menu.name} onClick={(e) => { if (handleCollapsedMenuClick(menu, e)) return; handleMenuClick(menu); }} > {menu.icon} {menu.name} {menu.hasChildren && !sidebarCollapsed && ( {isExpanded ? : } )}
{/* 플라이아웃 (접힌 상태에서만) */} {sidebarCollapsed && flyoutMenu?.menu.id === menu.id && menu.hasChildren && (
{menu.name}
{menu.children?.map((child: any) => (
handleFlyoutSelect(child)} > {child.icon} {child.name}
))}
)} {/* 하위 메뉴 — 항상 렌더링, CSS로 높이 제어 */} {!sidebarCollapsed && menu.hasChildren && (
{menu.children?.map((child: any, idx: number) => (
handleMenuDragStart(e, child)} className={`v5-si v5-sub-item ${isMenuActive(child) ? "on" : ""}`} style={{ transitionDelay: isExpanded ? `${idx * 30}ms` : "0ms" }} onClick={() => handleMenuClick(child)} > {child.icon} {child.name}
))}
)}
); }; if (isPreviewMode) { return (
{children}
); } const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : []; // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 useEffect(() => { if (!activeTab || uiMenus.length === 0) return; const toExpand: string[] = []; for (const menu of uiMenus) { if (menu.hasChildren && menu.children) { const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); if (hasActiveChild && !expandedMenus.has(menu.id)) { toExpand.push(menu.id); } } } if (toExpand.length > 0) { setExpandedMenus((prev) => { const next = new Set(prev); toExpand.forEach((id) => next.add(id)); return next; }); } }, [activeTab, uiMenus, isMenuActive, expandedMenus]); if (!user) { return (

로딩중...

); } // Admin permission check const isAdmin = (user as ExtendedUserInfo)?.isAdmin || (user as ExtendedUserInfo)?.user_type === "SUPER_ADMIN" || (user as ExtendedUserInfo)?.user_type === "COMPANY_ADMIN" || (user as ExtendedUserInfo)?.user_type === "ADMIN" || (user as ExtendedUserInfo)?.user_type === "admin"; // Breadcrumb from active tab const breadcrumbText = activeTab?.title || "대시보드"; return ( <> {/* Cosmic background */} {/* Theme fade overlay */}
{/* Mode transition — no overlay, sidebar/tabs animate via modeTransition state */} {/* V5 Shell */}
{/* ===== Glass Header ===== */}
{/* Mobile hamburger */}
INVION
{isAdminMode ? "관리자" : "홈"} › {breadcrumbText}
관리자 모드
{/* Theme pill */}
{/* Mini tab icon (visible when tabs collapsed) */} {tabsCollapsed && ( )} {/* Bell / Notifications */} {/* Admin toggle (gear ↔ home) */} {isAdmin && ( )} {/* Avatar dropdown */}
{user.user_name?.substring(0, 1)?.toUpperCase() || "U"}
{user.user_name?.substring(0, 1)?.toUpperCase() || "U"}

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

{user.email || user.user_id}

내 정보 router.push("/admin/approvalBox")}> 결재함 POP 모드 로그아웃
{/* ===== Tab Bar ===== */} setTabsCollapsed(!tabsCollapsed)} modeTransition={modeTransition} /> {/* Mobile overlay */} {sidebarOpen && isMobile && (
setSidebarOpen(false)} /> )} {/* ===== Body (sidebar + content) ===== */}
{/* Sidebar */} {/* Content area */}
회사 선택 관리할 회사를 선택하면 해당 회사의 관점에서 시스템을 사용할 수 있습니다.
setShowCompanySwitcher(false)} isOpen={showCompanySwitcher} />
); } export function AppLayout({ children }: AppLayoutProps) { return (

로딩중...

} > {children} ); }