import { useEffect, useState, type ReactNode, type ElementType } from 'react'; import { PageType } from '@/types'; import { useAuth } from '@/features/auth/useAuth'; import { useBranding } from '@/features/settings/useCompanySettings'; import { ProfileSheet } from '@/features/auth/components/ProfileSheet'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Typography } from '@/components/ui/typography'; import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { useNavigate } from 'react-router'; import { cn } from '@/lib/utils'; import { NotificationBell } from './NotificationBell'; import { GUIDE_TABS, TAB_LABEL, type GuideTab } from '@/features/onboarding/OnboardingGuideModal'; import { SETTINGS_TABS, SETTINGS_TAB_LABEL, type SettingsTab } from '@/features/settings/SettingsView'; import { LayoutDashboard, BarChart3, Briefcase, Users, UserCog, UserPen, FileSpreadsheet, Layers, LogOut, Search, Sun, Moon, Building, Palette, ChevronRight, Menu, X, BookOpen, } from 'lucide-react'; interface LayoutProps { children: ReactNode; currentPage: PageType; setPage: (page: PageType) => void; onLogout: () => void; } type SidebarUser = ReturnType['user']; type MenuItem = { type: PageType; label: string; icon: ElementType; id: string; ownerOnly?: boolean; devOnly?: boolean }; // ownerOnly = 최고관리자 이상, devOnly = 개발자(내부 운영)만. 렌더 시 user.role 로 필터한다. // 그룹 라벨은 사이드바 섹션 헤더로 노출(접힘 상태에선 숨김). const menuGroups: { label?: string; items: MenuItem[] }[] = [ { items: [ { type: 'DASHBOARD', label: '대시보드', icon: LayoutDashboard, id: 'sidebar-dashboard' }, { type: 'STATISTICS', label: '통계', icon: BarChart3, id: 'sidebar-statistics' }, ], }, { label: '업무', items: [ { type: 'PRODUCTS', label: '상품관리', icon: Briefcase, id: 'sidebar-products' }, { type: 'PARTNERS', label: '협력사관리', icon: Users, id: 'sidebar-partners' }, { type: 'QUOTATION', label: '견적관리', icon: FileSpreadsheet, id: 'sidebar-quotation' }, { type: 'CARDS', label: '협상카드관리', icon: Layers, id: 'sidebar-cards' }, ], }, { label: '관리', items: [ { type: 'MEMBERS', label: '회원관리', icon: UserCog, id: 'sidebar-members', ownerOnly: true }, ], }, { label: '개발자', items: [ { type: 'SETTINGS', label: '회사 설정', icon: Building, id: 'sidebar-settings', devOnly: true }, { type: 'DESIGN', label: '디자인 시스템', icon: Palette, id: 'sidebar-design', devOnly: true }, ], }, ]; const menuItems: MenuItem[] = menuGroups.flatMap((g) => g.items); // 메뉴 노출 판정 — devOnly 는 개발자만, ownerOnly 는 최고관리자 이상. const canSee = (item: MenuItem, role?: string): boolean => { if (item.devOnly) return role === '개발자'; if (item.ownerOnly) return role === '최고관리자' || role === '개발자'; return true; }; const pageLabelMap: Record = { DASHBOARD: '대시보드', STATISTICS: '통계', PRODUCTS: '상품관리', PARTNERS: '협력사관리', QUOTATION: '견적관리', CARDS: '협상카드관리', MEMBERS: '회원관리', SETTINGS: '회사 설정', DESIGN: '디자인 시스템', NOTIFICATIONS: '알림', }; export default function Layout({ children, currentPage, setPage, onLogout }: LayoutProps) { const { user } = useAuth(); const navigate = useNavigate(); const branding = useBranding(); // 회사 설정 브랜딩(서비스명/로고). 미설정 시 기본 NegoData // 기준일시(오늘) — 로컬 타임존 기준 YYYY-MM-DD const today = new Date().toLocaleDateString('sv-SE'); const [isDark, setIsDark] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(true); // 데스크톱 접기 토글 const [isMobileOpen, setIsMobileOpen] = useState(false); // 모바일 드로어 열림 const [isProfileOpen, setIsProfileOpen] = useState(false); // 내 정보 수정 시트 const [isCmdOpen, setIsCmdOpen] = useState(false); // ⌘K 빠른 이동 // 사이드바 펼침 여부(라벨/프로필 노출 기준). 모바일 드로어는 항상 펼친 상태로 본다. const expanded = isMobileOpen || isSidebarOpen; const visibleItems = menuItems.filter((item) => canSee(item, user?.role)); // ⌘K / Ctrl+K 로 빠른 이동 열기 useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setIsCmdOpen((v) => !v); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); const toggleDarkMode = () => { setIsDark(!isDark); if (!isDark) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }; return (
{/* 토스트는 sonner (main.tsx)가 전역으로 처리한다 */} {/* 모바일 드로어 백드롭 */} {isMobileOpen && (
setIsMobileOpen(false)} aria-hidden /> )} {/* Main Sidebar */} {/* Main Container Wrapper */}
{/* Global Header — 좌: 페이지명 / 중: 빠른 이동(⌘K) / 우: 알림·계정 메타 */}
{/* 모바일 햄버거 */} {/* 현재 페이지 — 모바일·데스크톱 모두 표시(로고는 사이드바/드로어 몫) */} {pageLabelMap[currentPage]}
{/* 빠른 이동 트리거 */}
{/* Content Body Area */}
{children}
{/* Compact Admin footer info */}
Copyright © O2O Inc. All rights reserved
{ setPage(type); setIsCmdOpen(false); }} onSelectGuide={(tab) => { navigate(`/dashboard?guide=${tab}`); setIsCmdOpen(false); }} onSelectSettings={(tab) => { navigate(`/settings?tab=${tab}`); setIsCmdOpen(false); }} canSeeSettings={visibleItems.some((i) => i.type === 'SETTINGS')} /> {isProfileOpen && setIsProfileOpen(false)} />}
); } /** ⌘K 빠른 이동 — 메뉴 검색·이동. 검색 대상 확장(견적명 등)은 추후 단계 */ function CommandMenu({ open, onOpenChange, items, currentPage, onSelect, onSelectGuide, onSelectSettings, canSeeSettings, }: { open: boolean; onOpenChange: (open: boolean) => void; items: MenuItem[]; currentPage: PageType; onSelect: (type: PageType) => void; onSelectGuide: (tab: GuideTab) => void; onSelectSettings: (tab: SettingsTab) => void; canSeeSettings: boolean; }) { const [query, setQuery] = useState(''); useEffect(() => { if (!open) setQuery(''); }, [open]); const q = query.trim().toLowerCase(); const filtered = q ? items.filter((i) => i.label.toLowerCase().includes(q)) : items; // 이용안내 탭도 이동 대상 — 대시보드로 가면서 ?guide=<탭> 을 붙여 해당 탭으로 바로 연다. const guideEntries = GUIDE_TABS.map((t) => ({ tab: t, label: `이용안내 · ${TAB_LABEL[t]}` })); const filteredGuides = q ? guideEntries.filter((g) => g.label.toLowerCase().includes(q)) : guideEntries; // 회사 설정 탭도 이동 대상(최고관리자만 — 메뉴와 같은 게이팅). const settingsEntries = canSeeSettings ? SETTINGS_TABS.map((t) => ({ tab: t, label: `회사 설정 · ${SETTINGS_TAB_LABEL[t]}` })) : []; const filteredSettings = q ? settingsEntries.filter((e) => e.label.toLowerCase().includes(q)) : settingsEntries; return ( 메뉴 빠른 이동
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && filtered.length > 0) onSelect(filtered[0].type); }} placeholder="이동할 메뉴 검색…" className="h-11 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground" />
{filtered.length === 0 && filteredGuides.length === 0 && filteredSettings.length === 0 ? ( 일치하는 메뉴가 없습니다 ) : ( filtered.map((item) => { const Icon = item.icon; return ( ); }) )} {filteredSettings.map((e) => ( ))} {filteredGuides.map((g) => ( ))}
); } /** 라벨:값 한 줄 (사이드바 프로필 카드용) */ function InfoRow({ label, value, valueClassName, title, }: { label: string; value?: string; valueClassName?: string; title?: string; }) { return (
{label} {value}
); } /** 사이드바 사용자 프로필 카드 */ function SidebarProfileCard({ user }: { user: SidebarUser }) { return (
{user?.company}
권한등급: {user?.role}
); } /** 사이드바 메뉴 항목 */ function NavItem({ item, isActive, isOpen, onClick, }: { item: MenuItem; isActive: boolean; isOpen: boolean; onClick: () => void; }) { const Icon = item.icon; return ( ); } /** 글로벌 헤더 우측 메타 정보 */ function HeaderMeta({ user, today }: { user: SidebarUser; today: string }) { return (
기준일시: {today} {/* 회사명은 모바일에서만 — 데스크톱은 사이드바 브랜드에 이미 노출 */}
회사: {user?.company}
관리자: {user?.name} ({user?.loginId}) {user?.role}
); }