## 핵심 변경
- auth_perm_tree를 메뉴 SSOT로 확장 (V020~V024)
- url_path, label_key, component_key, nav_group, nav_sub_group, nav_sort 컬럼
- labels JSONB (다국어: {"ko":"...", "en":"..."})
- 보이지 않는 도메인 그룹 8개 삭제 (surveillance, detection, risk-assessment 등)
- 권한 트리 = 메뉴 트리 완전 동기화
- 그룹 레벨 권한 → 개별 자식 권한으로 확장 후 그룹 삭제
- 패널 노드 parent_cd를 실제 소속 페이지로 수정
(어구식별→어구탐지, 전역제외→후보제외, 역할관리→권한관리)
- vessel:vessel-detail 권한 노드 제거 (드릴다운 전용, 인증만 체크)
## 백엔드
- MenuConfigService: auth_perm_tree에서 menuConfig DTO 생성
- /api/auth/me 응답에 menuConfig 포함 (로그인 시 프리로드)
- @RequirePermission 12곳 수정 (삭제된 그룹명 → 구체적 자식 리소스)
- Caffeine 캐시 menuConfig 추가
## 프론트엔드
- NAV_ENTRIES 하드코딩 제거 → menuStore(Zustand) 동적 렌더링
- PATH_TO_RESOURCE 하드코딩 제거 → DB 기반 longest-match
- App.tsx 36개 정적 import/33개 Route → DynamicRoutes + componentRegistry
- PermissionsPanel: DB labels JSONB 기반 표시명 + 페이지/패널 아이콘 구분
- DB migration README.md 전면 재작성 (V001~V024, 49테이블, 149인덱스)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
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 <Navigate to="/login" replace />;
|
|
if (resource && !hasPermission(resource, operation)) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center px-6">
|
|
<div className="text-4xl mb-4">🚫</div>
|
|
<h1 className="text-xl font-bold text-heading mb-2">접근 권한이 없습니다</h1>
|
|
<p className="text-sm text-hint">
|
|
이 페이지에 접근하려면 <code className="bg-surface-overlay px-1.5 py-0.5 rounded">{resource}</code>::{operation} 권한이 필요합니다.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
return <>{children}</>;
|
|
}
|
|
|
|
function LoadingFallback() {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[40vh]">
|
|
<div className="text-sm text-hint">로딩 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* DB menu_config 기반 동적 라우트를 Route 배열로 반환.
|
|
* React Router v6는 <Routes> 직계 자식으로 <Route>만 허용하므로 컴포넌트가 아닌 함수로 생성.
|
|
*/
|
|
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 (
|
|
<Route
|
|
key={item.menuCd}
|
|
path={path}
|
|
element={
|
|
<ProtectedRoute resource={item.rsrcCd ?? undefined}>
|
|
<Suspense fallback={<LoadingFallback />}>
|
|
<Comp />
|
|
</Suspense>
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
);
|
|
});
|
|
}
|
|
|
|
function AppRoutes() {
|
|
const dynamicRoutes = useDynamicRoutes();
|
|
|
|
return (
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route path="/" element={<ProtectedRoute><MainLayout /></ProtectedRoute>}>
|
|
<Route index element={<Navigate to="/dashboard" replace />} />
|
|
{dynamicRoutes}
|
|
{/* 드릴다운 전용 라우트 — 메뉴/권한 노드 없음, 인증만 체크 */}
|
|
<Route path="vessel/:id" element={<Suspense fallback={<LoadingFallback />}><VesselDetail /></Suspense>} />
|
|
</Route>
|
|
</Routes>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<BrowserRouter>
|
|
<AuthProvider>
|
|
<AppRoutes />
|
|
</AuthProvider>
|
|
</BrowserRouter>
|
|
);
|
|
}
|