diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index fa0618f..1310703 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -12,7 +12,7 @@ import { LoginPage } from '@features/auth'; /* SFR-08 */ import { FleetOptimization } from '@features/patrol'; /* SFR-09 */ import { DarkVesselDetection } from '@features/detection'; /* SFR-10 */ import { GearDetection } from '@features/detection'; -/* SFR-11 */ import { EnforcementHistory } from '@features/enforcement'; +/* SFR-11 */ import { EnforcementHistory, CaseList, MatchDashboard, MatchVerify, LabelList, HistorySearch, VesselHistory, TypeStats, ReportGen, ChangeHistory } from '@features/enforcement'; /* SFR-12 */ import { MonitoringDashboard } from '@features/monitoring'; /* SFR-13 */ import { Statistics } from '@features/statistics'; /* SFR-14 */ import { ExternalService } from '@features/statistics'; @@ -82,6 +82,16 @@ export default function App() { {/* SFR-05~06 위험도·단속계획 */} } /> } /> + {/* SFR-11 단속 사건 관리 (리스트·등록·상세·수정 통합) */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* SFR-09~10 탐지 */} } /> } /> diff --git a/frontend/src/app/auth/AuthContext.tsx b/frontend/src/app/auth/AuthContext.tsx index 02b01a9..a38d93a 100644 --- a/frontend/src/app/auth/AuthContext.tsx +++ b/frontend/src/app/auth/AuthContext.tsx @@ -44,6 +44,15 @@ const PATH_TO_RESOURCE: Record = { '/china-fishing': 'detection:china-fishing', '/vessel': 'vessel', '/risk-map': 'risk-assessment:risk-map', + '/enforcement-plan/cases': 'enforcement:enforcement-history', + '/enforcement-plan/match-dashboard': 'enforcement:enforcement-history', + '/enforcement-plan/match-verify': 'enforcement:enforcement-history', + '/enforcement-plan/label-list': 'enforcement:enforcement-history', + '/enforcement-plan/history-search': 'enforcement:enforcement-history', + '/enforcement-plan/vessel-history': 'enforcement:enforcement-history', + '/enforcement-plan/type-stats': 'enforcement:enforcement-history', + '/enforcement-plan/report-gen': 'enforcement:enforcement-history', + '/enforcement-plan/change-history': 'enforcement:enforcement-history', '/enforcement-plan': 'risk-assessment:enforcement-plan', '/patrol-route': 'patrol:patrol-route', '/fleet-optimization': 'patrol:fleet-optimization', diff --git a/frontend/src/app/layout/MainLayout.tsx b/frontend/src/app/layout/MainLayout.tsx index 65eff20..21858af 100644 --- a/frontend/src/app/layout/MainLayout.tsx +++ b/frontend/src/app/layout/MainLayout.tsx @@ -10,6 +10,7 @@ import { Navigation, Users, EyeOff, BarChart3, Globe, Smartphone, Monitor, Send, Cpu, MessageSquare, GitBranch, CheckSquare, Ban, Tag, ScrollText, History, KeyRound, + Plus, Edit3, } from 'lucide-react'; import { useAuth, type UserRole } from '@/app/auth/AuthContext'; import { NotificationBanner, NotificationPopup, type SystemNotice } from '@shared/components/common/NotificationBanner'; @@ -42,10 +43,17 @@ const AUTH_METHOD_LABELS: Record = { }; interface NavItem { to: string; icon: React.ElementType; labelKey: string; } -interface NavGroup { groupKey: string; icon: React.ElementType; items: NavItem[]; } +interface NavSubGroup { subGroupKey: string; icon: React.ElementType; items: NavItem[]; } +interface NavGroup { groupKey: string; icon: React.ElementType; items: (NavItem | NavSubGroup)[]; } type NavEntry = NavItem | NavGroup; const isGroup = (entry: NavEntry): entry is NavGroup => 'groupKey' in entry; +const isSubGroup = (entry: NavItem | NavSubGroup): entry is NavSubGroup => 'subGroupKey' in entry; + +/** 서브그룹 포함 그룹에서 모든 NavItem을 플랫하게 추출 */ +function flatGroupItems(items: (NavItem | NavSubGroup)[]): NavItem[] { + return items.flatMap(item => isSubGroup(item) ? item.items : [item]); +} const NAV_ENTRIES: NavEntry[] = [ // ── 상황판·감시 ── @@ -55,7 +63,42 @@ const NAV_ENTRIES: NavEntry[] = [ { to: '/map-control', icon: Map, labelKey: 'nav.riskMap' }, // ── 위험도·단속 ── { to: '/risk-map', icon: Layers, labelKey: 'nav.riskMap' }, - { to: '/enforcement-plan', icon: Shield, labelKey: 'nav.enforcementPlan' }, + // ── SFR-11 단속계획 (3단 메뉴) ── + { + groupKey: 'group.enforcementPlan', icon: Shield, + items: [ + { to: '/enforcement-plan', icon: Shield, labelKey: 'nav.enforcementPlan' }, + { + subGroupKey: 'subGroup.caseManagement', icon: FileText, + items: [ + { to: '/enforcement-plan/cases', icon: List, labelKey: 'nav.caseManagement' }, + ], + }, + { + subGroupKey: 'subGroup.aiMatch', icon: Activity, + items: [ + { to: '/enforcement-plan/match-dashboard', icon: Activity, labelKey: 'nav.matchDashboard' }, + { to: '/enforcement-plan/match-verify', icon: CheckSquare, labelKey: 'nav.matchVerify' }, + { to: '/enforcement-plan/label-list', icon: Tag, labelKey: 'nav.labelList' }, + ], + }, + { + subGroupKey: 'subGroup.historyAnalysis', icon: Search, + items: [ + { to: '/enforcement-plan/history-search', icon: Search, labelKey: 'nav.historySearch' }, + { to: '/enforcement-plan/vessel-history', icon: Ship, labelKey: 'nav.vesselHistory' }, + { to: '/enforcement-plan/type-stats', icon: BarChart3, labelKey: 'nav.typeStats' }, + { to: '/enforcement-plan/report-gen', icon: FileSpreadsheet, labelKey: 'nav.reportGen' }, + ], + }, + { + subGroupKey: 'subGroup.auditTrail', icon: History, + items: [ + { to: '/enforcement-plan/change-history', icon: History, labelKey: 'nav.changeHistory' }, + ], + }, + ], + }, // ── 탐지 ── { to: '/dark-vessel', icon: EyeOff, labelKey: 'nav.darkVessel' }, { to: '/gear-detection', icon: Anchor, labelKey: 'nav.gearDetection' }, @@ -106,7 +149,7 @@ const NAV_ENTRIES: NavEntry[] = [ ]; // getPageLabel용 flat 목록 -const NAV_ITEMS = NAV_ENTRIES.flatMap(e => isGroup(e) ? e.items : [e]); +const NAV_ITEMS = NAV_ENTRIES.flatMap(e => isGroup(e) ? flatGroupItems(e.items) : [e]); function formatRemaining(seconds: number) { const m = Math.floor(seconds / 60); @@ -322,15 +365,17 @@ export function MainLayout() {