## 핵심 변경
- 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>
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { create } from 'zustand';
|
|
|
|
export interface MenuConfigItem {
|
|
menuCd: string;
|
|
parentMenuCd: string | null;
|
|
menuType: 'ITEM' | 'GROUP' | 'DIVIDER';
|
|
urlPath: string | null;
|
|
rsrcCd: string | null;
|
|
componentKey: string | null;
|
|
icon: string | null;
|
|
labelKey: string | null;
|
|
dividerLabel: string | null;
|
|
menuLevel: number;
|
|
sortOrd: number;
|
|
useYn: string;
|
|
labels: Record<string, string>;
|
|
}
|
|
|
|
/** DB labels에서 현재 언어의 라벨을 반환 */
|
|
export function getMenuLabel(item: MenuConfigItem, lang: string): string {
|
|
return item.labels?.[lang] || item.labels?.ko || item.menuCd;
|
|
}
|
|
|
|
interface MenuStore {
|
|
items: MenuConfigItem[];
|
|
loaded: boolean;
|
|
setMenuConfig: (items: MenuConfigItem[]) => void;
|
|
clear: () => void;
|
|
|
|
/** path → rsrcCd (longest-match, PATH_TO_RESOURCE 대체) */
|
|
getResourceForPath: (path: string) => string | undefined;
|
|
/** 최상위 항목 (menu_level=0, 사이드바 표시용) */
|
|
getTopLevelEntries: () => MenuConfigItem[];
|
|
/** 그룹 하위 항목 */
|
|
getChildren: (parentMenuCd: string) => MenuConfigItem[];
|
|
/** 라우팅 가능 항목 (ITEM + urlPath 보유) */
|
|
getRoutableItems: () => MenuConfigItem[];
|
|
}
|
|
|
|
export const useMenuStore = create<MenuStore>((set, get) => ({
|
|
items: [],
|
|
loaded: false,
|
|
|
|
setMenuConfig: (items) => set({ items, loaded: true }),
|
|
clear: () => set({ items: [], loaded: false }),
|
|
|
|
getResourceForPath: (path) => {
|
|
const { items } = get();
|
|
// longest-match: 가장 구체적인 경로 우선 (삽입 순서 의존 버그 해결)
|
|
const candidates = items.filter((i) => i.urlPath && i.rsrcCd);
|
|
candidates.sort((a, b) => b.urlPath!.length - a.urlPath!.length);
|
|
const match = candidates.find((i) => path.startsWith(i.urlPath!));
|
|
return match?.rsrcCd ?? undefined;
|
|
},
|
|
|
|
getTopLevelEntries: () =>
|
|
get().items.filter(
|
|
(i) => i.menuLevel === 0 && i.parentMenuCd === null && i.useYn !== 'H',
|
|
),
|
|
|
|
getChildren: (parentMenuCd) =>
|
|
get().items.filter((i) => i.parentMenuCd === parentMenuCd),
|
|
|
|
getRoutableItems: () =>
|
|
get().items.filter((i) => i.menuType === 'ITEM' && i.urlPath),
|
|
}));
|