'use client'; import { useMenuStore } from '@/store/menuStore'; import type { SerializableMenuItem } from '@/store/menuStore'; import type { MenuItem } from '@/store/menuStore'; import { useRouter, usePathname } from 'next/navigation'; import { useEffect, useState } from 'react'; import { Menu, Search, User, LogOut, LayoutDashboard, Sun, Moon, Accessibility, ShoppingCart, Building2, Receipt, Package, Settings, DollarSign, ChevronLeft, Home, X, BarChart3, Award, Bell, Clock, } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import Sidebar from '@/components/layout/Sidebar'; import { useTheme } from '@/contexts/ThemeContext'; import { useAuth } from '@/contexts/AuthContext'; import { deserializeMenuItems } from '@/lib/utils/menuTransform'; import { useMenuPolling } from '@/hooks/useMenuPolling'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; // 목업 회사 데이터 const MOCK_COMPANIES = [ { id: 'all', name: '전체' }, { id: 'company1', name: '(주)삼성건설' }, { id: 'company2', name: '현대건설(주)' }, { id: 'company3', name: '대우건설(주)' }, { id: 'company4', name: 'GS건설(주)' }, ]; // 목업 알림 데이터 const MOCK_NOTIFICATIONS = [ { id: 1, category: '안내', title: '시스템 점검 안내', date: '2025.09.03 12:23', isNew: true }, { id: 2, category: '공지사항', title: '신규 기능 업데이트', date: '2025.09.03 12:23', isNew: false }, { id: 3, category: '안내', title: '보안 업데이트 완료', date: '2025.09.03 12:23', isNew: false }, ]; interface AuthenticatedLayoutProps { children: React.ReactNode; } export default function AuthenticatedLayout({ children }: AuthenticatedLayoutProps) { const { menuItems, activeMenu, setActiveMenu, setMenuItems, sidebarCollapsed, toggleSidebar, _hasHydrated } = useMenuStore(); const { theme, setTheme } = useTheme(); const { logout } = useAuth(); const router = useRouter(); const pathname = usePathname(); // 현재 경로 추적 // 확장된 서브메뉴 관리 (기본적으로 sales, master-data 확장) const [expandedMenus, setExpandedMenus] = useState(['sales', 'master-data']); // 모바일 상태 관리 const [isMobile, setIsMobile] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); // 사용자 정보 상태 const [userName, setUserName] = useState("사용자"); const [userPosition, setUserPosition] = useState("직책"); const [userEmail, setUserEmail] = useState(""); const [userCompany, setUserCompany] = useState(""); // 회사 선택 상태 (목업) const [selectedCompany, setSelectedCompany] = useState("all"); // 메뉴 폴링 (30초마다 메뉴 변경 확인) // 백엔드 GET /api/v1/menus API 준비되면 자동 동작 const { restartAfterAuth } = useMenuPolling({ enabled: true, interval: 30000, // 30초 onMenuUpdated: () => { console.log('[Menu] 메뉴가 업데이트되었습니다'); }, onSessionExpired: () => { console.log('[Menu] 세션 만료로 폴링 중지됨'); }, }); // 로그인 성공 후 메뉴 폴링 재시작 useEffect(() => { const justLoggedIn = sessionStorage.getItem('auth_just_logged_in'); if (justLoggedIn === 'true') { console.log('[Menu] 로그인 감지 - 폴링 재시작'); sessionStorage.removeItem('auth_just_logged_in'); restartAfterAuth(); } }, [restartAfterAuth]); // 모바일 감지 + 폴더블 기기 대응 (Galaxy Fold 등) useEffect(() => { const updateViewport = () => { // visualViewport API 우선 사용 (폴더블 기기에서 더 정확) const width = window.visualViewport?.width ?? window.innerWidth; const height = window.visualViewport?.height ?? window.innerHeight; setIsMobile(width < 768); // CSS 변수로 실제 viewport 크기 설정 (폴더블 기기 대응) document.documentElement.style.setProperty('--app-width', `${width}px`); document.documentElement.style.setProperty('--app-height', `${height}px`); }; updateViewport(); // resize 이벤트 window.addEventListener('resize', updateViewport); // visualViewport resize 이벤트 (폴더블 기기 화면 전환 감지) window.visualViewport?.addEventListener('resize', updateViewport); return () => { window.removeEventListener('resize', updateViewport); window.visualViewport?.removeEventListener('resize', updateViewport); }; }, []); // 서버에서 받은 사용자 정보로 초기화 useEffect(() => { // ⚠️ Allow rendering even before hydration (Zustand persist rehydration can be slow) // Commenting out the hydration check prevents infinite loading spinner // if (!_hasHydrated) return; // localStorage에서 사용자 정보 가져오기 const userDataStr = localStorage.getItem("user"); if (userDataStr) { const userData = JSON.parse(userDataStr); // 사용자 이름, 직책, 이메일, 회사 설정 setUserName(userData.name || "사용자"); setUserPosition(userData.position || "직책"); setUserEmail(userData.email || ""); setUserCompany(userData.company || userData.company_name || ""); // 서버에서 받은 메뉴 배열이 있으면 사용, 없으면 기본 메뉴 사용 if (userData.menu && Array.isArray(userData.menu) && userData.menu.length > 0) { // SerializableMenuItem (iconName string)을 MenuItem (icon component)로 변환 const deserializedMenus = deserializeMenuItems(userData.menu as SerializableMenuItem[]); setMenuItems(deserializedMenus); } else { // API가 준비될 때까지 임시 기본 메뉴 const defaultMenu: MenuItem[] = [ { id: "dashboard", label: "대시보드", icon: LayoutDashboard, path: "/dashboard" }, { id: "sales", label: "판매관리", icon: ShoppingCart, path: "#", children: [ { id: "customer-management", label: "거래처관리", icon: Building2, path: "/sales/client-management-sales-admin" }, { id: "quote-management", label: "견적관리", icon: Receipt, path: "/sales/quote-management" }, { id: "pricing-management", label: "단가관리", icon: DollarSign, path: "/sales/pricing-management" }, ], }, { id: "master-data", label: "기준정보", icon: Settings, path: "#", children: [ { id: "item-master", label: "품목기준관리", icon: Package, path: "/master-data/item-master-data-management" }, ], }, ]; setMenuItems(defaultMenu); } } }, [_hasHydrated, setMenuItems]); // 현재 경로에 맞는 메뉴 자동 활성화 (URL 직접 입력, 뒤로가기 대응) // 3depth 이상 메뉴 구조 지원 useEffect(() => { if (!pathname || menuItems.length === 0) return; // 경로 정규화 (로케일 제거) const normalizedPath = pathname.replace(/^\/(ko|en|ja)/, ''); // 메뉴 탐색 함수: 메인 메뉴와 서브메뉴 모두 탐색 // 경로 매칭: 정확히 일치하거나 하위 경로인 경우만 매칭 const isPathMatch = (menuPath: string, currentPath: string): boolean => { if (currentPath === menuPath) return true; return currentPath.startsWith(menuPath + '/'); }; // 재귀적으로 모든 depth의 메뉴를 탐색 (3depth 이상 지원) type MenuMatch = { menuId: string; ancestorIds: string[]; pathLength: number }; const findMenuRecursive = ( items: MenuItem[], currentPath: string, ancestors: string[] = [] ): MenuMatch[] => { const matches: MenuMatch[] = []; for (const item of items) { // 현재 메뉴의 경로 매칭 확인 if (item.path && item.path !== '#' && isPathMatch(item.path, currentPath)) { matches.push({ menuId: item.id, ancestorIds: ancestors, pathLength: item.path.length, }); } // 자식 메뉴가 있으면 재귀적으로 탐색 if (item.children && item.children.length > 0) { const childMatches = findMenuRecursive( item.children, currentPath, [...ancestors, item.id] // 현재 메뉴를 조상 목록에 추가 ); matches.push(...childMatches); } } return matches; }; const matches = findMenuRecursive(menuItems, normalizedPath); if (matches.length > 0) { // 가장 긴 경로(가장 구체적인 매칭) 선택 matches.sort((a, b) => b.pathLength - a.pathLength); const result = matches[0]; // 활성 메뉴 설정 setActiveMenu(result.menuId); // 모든 조상 메뉴를 확장 (3depth 이상 지원) if (result.ancestorIds.length > 0) { setExpandedMenus(prev => { const newExpanded = [...prev]; result.ancestorIds.forEach(id => { if (!newExpanded.includes(id)) { newExpanded.push(id); } }); return newExpanded; }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname, menuItems, setActiveMenu]); const handleMenuClick = (menuId: string, path: string) => { setActiveMenu(menuId); router.push(path); }; // 서브메뉴 토글 함수 const toggleSubmenu = (menuId: string) => { setExpandedMenus(prev => prev.includes(menuId) ? prev.filter(id => id !== menuId) : [...prev, menuId] ); }; const handleLogout = async () => { try { // AuthContext의 logout() 호출 (완전한 캐시 정리 수행) // - Zustand 스토어 초기화 // - sessionStorage 캐시 삭제 (page_config_*, mes-*) // - localStorage 사용자 데이터 삭제 // - 서버 로그아웃 API 호출 (HttpOnly 쿠키 삭제) await logout(); // 로그인 페이지로 리다이렉트 router.push('/login'); } catch (error) { console.error('로그아웃 처리 중 오류:', error); // 에러가 나도 로그인 페이지로 이동 router.push('/login'); } }; // ⚠️ FIXED: Removed hydration check to prevent infinite loading spinner // The hydration check was causing the dashboard to show a loading spinner indefinitely // because Zustand persist rehydration was taking too long or not completing properly. // By removing this check, we allow the component to render immediately with default values // and update once hydration completes through the useEffect above. // 현재 페이지가 대시보드인지 확인 const isDashboard = pathname?.includes('/dashboard') || activeMenu === 'dashboard'; // 이전 페이지로 이동 const handleGoBack = () => { router.back(); }; // 홈(대시보드)으로 이동 const handleGoHome = () => { router.push('/dashboard'); }; // 모바일 레이아웃 if (isMobile) { return (
{/* 모바일 헤더 - sam-design 스타일 */}
{/* 좌측 영역: 햄버거 메뉴 + (대시보드일 때는 로고, 다른 페이지일 때는 이전/홈 버튼) */}
{/* 햄버거 메뉴 - 좌측 맨 앞 */} 메뉴 setIsMobileSidebarOpen(false)} /> {isDashboard ? ( // 대시보드: 로고만 표시
S

SAM

) : ( // 다른 페이지: 이전/홈 버튼 표시
)}
{/* 우측 영역: 종합분석, 품질인정심사, 유저 드롭다운, 메뉴 */}
{/* 종합분석 바로가기 */} {/* 품질인정심사 바로가기 */} {/* 알림 버튼 */} {/* 알림 리스트 */}
{MOCK_NOTIFICATIONS.map((notification) => (
{/* 이미지 플레이스홀더 */}
IMG
{/* 내용 */}
{notification.category} {notification.isNew && ( N )}

{notification.title}

{/* 날짜 */} {notification.date}
))}
{/* 유저 프로필 드롭다운 */} {/* 사용자 정보 헤더 */}

{userName}

{userPosition}

{/* 출근하기 버튼 */}
{/* 회사 선택 (목업) */}

회사 선택

{/* 테마 선택 */}
setTheme('light')} className="rounded-lg"> 일반모드 {theme === 'light' && } setTheme('dark')} className="rounded-lg"> 다크모드 {theme === 'dark' && } setTheme('senior')} className="rounded-lg"> 시니어모드 {theme === 'senior' && } {/* 구분선 */}
{/* 로그아웃 */} 로그아웃
{/* 모바일 콘텐츠 */}
{children}
); } // 데스크톱 레이아웃 return (
{/* 헤더 - 전체 너비 상단 고정 */}
{/* SAM 로고 섹션 - 클릭 시 대시보드로 이동 */}
S

SAM

Smart Automation Management

{/* Menu 버튼 - 사이드바 토글 */} {/* 검색바 */}
{/* 종합분석 바로가기 버튼 */} {/* 품질인정심사 바로가기 버튼 */} {/* 알림 버튼 */} {/* 알림 리스트 */}
{MOCK_NOTIFICATIONS.map((notification) => (
{/* 이미지 플레이스홀더 */}
IMG
{/* 내용 */}
{notification.category} {notification.isNew && ( N )}

{notification.title}

{/* 날짜 */} {notification.date}
))}
{/* 유저 프로필 드롭다운 */} {/* 사용자 정보 헤더 */}

{userName}

{userPosition}

{/* 회사 선택 (목업) */}

회사 선택

{/* 테마 선택 */}
setTheme('light')} className="rounded-lg"> 일반모드 {theme === 'light' && } setTheme('dark')} className="rounded-lg"> 다크모드 {theme === 'dark' && } setTheme('senior')} className="rounded-lg"> 시니어모드 {theme === 'senior' && } {/* 구분선 */}
{/* 로그아웃 */} 로그아웃
{/* Subtle gradient overlay */}
{/* 사이드바 + 메인 콘텐츠 영역 */}
{/* 데스크톱 사이드바 */}
{/* 메인 콘텐츠 */}
{children}
); }