import { Suspense, useMemo, lazy } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider, useAuth } from '@/app/auth/AuthContext'; import { MainLayout } from '@/app/layout/MainLayout'; import { LoginPage } from '@features/auth'; import { useMenuStore } from '@stores/menuStore'; import { COMPONENT_REGISTRY } from '@/app/componentRegistry'; // 권한 노드 없는 드릴다운 라우트 (인증만 체크) const VesselDetail = lazy(() => import('@features/vessel').then((m) => ({ default: m.VesselDetail }))); /** * 권한 가드. * - user 미인증 시 /login으로 리다이렉트 * - resource 지정 시 hasPermission 체크 → 거부 시 403 표시 */ function ProtectedRoute({ children, resource, operation = 'READ', }: { children: React.ReactNode; resource?: string; operation?: string; }) { const { user, loading, hasPermission } = useAuth(); if (loading) return null; if (!user) return ; if (resource && !hasPermission(resource, operation)) { return (
🚫

접근 권한이 없습니다

이 페이지에 접근하려면 {resource}::{operation} 권한이 필요합니다.

); } return <>{children}; } function LoadingFallback() { return (
로딩 중...
); } /** * DB menu_config 기반 동적 라우트를 Route 배열로 반환. * React Router v6는 직계 자식으로 만 허용하므로 컴포넌트가 아닌 함수로 생성. */ function useDynamicRoutes() { const items = useMenuStore((s) => s.items); const routableItems = useMemo( () => items.filter((i) => i.menuType === 'ITEM' && i.urlPath), [items], ); return routableItems.map((item) => { const Comp = item.componentKey ? COMPONENT_REGISTRY[item.componentKey] : null; if (!Comp || !item.urlPath) return null; const path = item.urlPath.replace(/^\//, ''); return ( }> } /> ); }); } function AppRoutes() { const dynamicRoutes = useDynamicRoutes(); return ( } /> }> } /> {dynamicRoutes} {/* 드릴다운 전용 라우트 — 메뉴/권한 노드 없음, 인증만 체크 */} }>} /> ); } export default function App() { return ( ); }