diff --git a/frontend/src/features/risk-assessment/EnforcementPlan.tsx b/frontend/src/features/risk-assessment/EnforcementPlan.tsx index b6bae6c..4211262 100644 --- a/frontend/src/features/risk-assessment/EnforcementPlan.tsx +++ b/frontend/src/features/risk-assessment/EnforcementPlan.tsx @@ -7,7 +7,11 @@ import { PageContainer, PageHeader } from '@shared/components/layout'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { getRiskIntent, getStatusIntent } from '@shared/constants/statusIntent'; import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels'; -import { Shield, AlertTriangle, Ship, Plus, Calendar, Users, Loader2 } from 'lucide-react'; +import { + Shield, AlertTriangle, Ship, Plus, Calendar, Users, Loader2, + Navigation, Anchor, Radio, Target, Clock, Compass, Eye, + MapPin, Zap, ChevronRight, CheckCircle, Activity, +} from 'lucide-react'; import { BaseMap, STATIC_LAYERS, createMarkerLayer, createRadiusLayer, useMapLayers, type MapHandle } from '@lib/map'; import type { MarkerData } from '@lib/map'; import { getEnforcementPlans, type EnforcementPlan as EnforcementPlanApi } from '@/services/enforcement'; @@ -17,6 +21,64 @@ import { useSettingsStore } from '@stores/settingsStore'; /* SFR-06: 단속 계획·경보 연계(단속 우선지역 예보) */ +type PlanTab = 'overview' | 'single' | 'multi'; + +// ─── 단일 함정 작전 데이터 ────────────────── + +const SINGLE_OP_TYPES = [ + { type: '정찰 순찰', desc: '특정 구역 내 불법조업 의심 선박 탐색·확인', duration: '4~8시간', crew: '8~12명', risk: '중간', icon: Eye }, + { type: '긴급 출동', desc: 'CRITICAL 경보 발생 시 즉시 현장 투입', duration: '1~3시간', crew: '10~15명', risk: '높음', icon: Zap }, + { type: '감시 초계', desc: '고위험 해역 정기 순찰 및 AIS 모니터링', duration: '6~12시간', crew: '8~10명', risk: '낮음', icon: Compass }, + { type: '근접 차단', desc: '도주 의심 선박 접근·정선명령·검문검색', duration: '2~4시간', crew: '12~18명', risk: '높음', icon: Target }, +]; + +const SINGLE_SCENARIOS = [ + { phase: '① 출항 전', actions: ['작전 브리핑 (위협 분석·해상상태)', '장비 점검 (레이더·AIS·카메라·무선)', '인력 배치 및 역할 분담'], time: '출항 60분 전' }, + { phase: '② 이동·접근', actions: ['최적 경로 항해 (연료·시간 최적화)', 'AIS 실시간 추적 + 레이더 교차확인', '본부 상황실 위치 보고 (15분 주기)'], time: '이동 중' }, + { phase: '③ 현장 작전', actions: ['정선명령 → 임검 또는 감시 유지', '증거 수집 (영상·사진·AIS 로그)', '위반 확인 시 나포 절차 진행'], time: '현장 도착 후' }, + { phase: '④ 복귀·보고', actions: ['작전 결과 보고 (상황실 실시간 전송)', '증거물 봉인 및 인계', '작전 후 회고 (AAR) 기록'], time: '작전 종료 후' }, +]; + +const AVAILABLE_SHIPS = [ + { name: '3009함', type: '1,000톤급', speed: '25kt', equip: '레이더·AIS·FLIR', status: '가용', crew: 18, zone: 'II구역' }, + { name: '3012함', type: '500톤급', speed: '30kt', equip: '레이더·AIS·EO/IR', status: '가용', crew: 12, zone: 'III구역' }, + { name: '1502함', type: '250톤급', speed: '28kt', equip: '레이더·AIS', status: '초계중', crew: 10, zone: 'I구역' }, + { name: '1507함', type: '250톤급', speed: '28kt', equip: '레이더·AIS·소나', status: '가용', crew: 10, zone: 'IV구역' }, + { name: '523정', type: '100톤급', speed: '32kt', equip: '레이더·AIS', status: '정비중', crew: 8, zone: '-' }, + { name: '527정', type: '100톤급', speed: '33kt', equip: '레이더·AIS·드론', status: '가용', crew: 8, zone: 'II구역' }, +]; + +// ─── 다함정 작전 데이터 ────────────────── + +const MULTI_OP_TYPES = [ + { type: '포위 차단 작전', desc: '2~4척이 불법 선단을 포위하여 도주로 차단', ships: '3~4척', formation: '삼각·사각 포위', command: '지휘함 1 + 차단함 2~3', icon: Target }, + { type: '광역 초계 작전', desc: '넓은 해역을 분할하여 동시 순찰', ships: '4~6척', formation: '구역 분할 병렬', command: '지휘함 1 + 순찰함 3~5', icon: Compass }, + { type: '합동 단속 작전', desc: '항공·함정 협동으로 고위험 해역 집중 단속', ships: '3~5척 + 항공 1', formation: '항공 유도 + 수상 차단', command: '합동지휘소', icon: Shield }, + { type: '호위 작전', desc: '아국 어선 보호 및 불법어선 접근 억제', ships: '2~3척', formation: '호위 종대', command: '지휘함 1 + 호위함 1~2', icon: Ship }, +]; + +const MULTI_ROLES = [ + { role: '지휘함', duty: '작전 총괄·의사결정·상황실 보고', requirement: '500톤급 이상', comm: 'VHF Ch.16 + 보안채널', badge: 'critical' as const }, + { role: '차단함', duty: '도주로 차단·정선명령 집행', requirement: '250톤급 이상', comm: 'VHF + AIS 공유', badge: 'high' as const }, + { role: '감시함', duty: '원거리 레이더·FLIR 감시, 증거 촬영', requirement: '100톤급 이상', comm: 'VHF + 영상 전송', badge: 'info' as const }, + { role: '기동함', duty: '고속 접근·소형선박 추적·인력 투입', requirement: '100톤급 고속정', comm: 'VHF + 전술채널', badge: 'warning' as const }, +]; + +const MULTI_COMM_PROTOCOL = [ + { channel: 'VHF Ch.16', purpose: '국제 조난·호출 (공용)', encryption: '비암호', usage: '초기 접촉·정선명령' }, + { channel: 'VHF Ch.22', purpose: '함정 간 전술 통신', encryption: '비암호', usage: '작전 기동·위치 보고' }, + { channel: '보안 채널 (HF)', purpose: '지휘함-상황실 암호 통신', encryption: 'AES-256', usage: '작전 지시·기밀 보고' }, + { channel: 'AIS 공유', purpose: '실시간 위치·속도 동기화', encryption: '-', usage: '함정 간 위치 인식' }, + { channel: '위성 데이터링크', purpose: '원거리 영상·데이터 전송', encryption: 'TLS 1.3', usage: '상황실 실시간 전송' }, +]; + +const MULTI_SCENARIOS = [ + { phase: '① 작전 계획', tasks: ['AI 위험도 분석 기반 작전 구역 선정', '함정 배치 및 역할 분담', '통신 채널·보고 주기 확정', '기상·해황·조류 분석'], commander: '작전지휘관' }, + { phase: '② 전개·배치', tasks: ['각 함정 지정 위치로 이동', '포위망 형성 또는 구역 분할 완료', '지휘함 위치 확인 보고', 'AIS 상호 추적 개시'], commander: '지휘함장' }, + { phase: '③ 작전 수행', tasks: ['감시함 탐지 → 지휘함 판단 → 차단함 투입', '정선명령·임검·나포 절차', '도주 시 기동함 추적 + 차단함 우회', '증거 수집·실시간 상황실 전송'], commander: '현장지휘관' }, + { phase: '④ 철수·평가', tasks: ['나포 선박 호송 또는 감시 해제', '전 함정 귀항 또는 재배치', '합동 작전 보고서(AAR) 작성', '작전 데이터 DB 기록 (AI 학습용)'], commander: '작전지휘관' }, +]; + interface Plan { id: string; zone: string; lat: number; lng: number; risk: number; period: string; ships: string; crew: number; status: string; alert: string; [key: string]: unknown; } /** API 응답 → 화면용 Plan 변환 */ @@ -54,6 +116,7 @@ export function EnforcementPlan() { const { t: tc } = useTranslation('common'); const lang = useSettingsStore((s) => s.language); + const [tab, setTab] = useState('overview'); const [plans, setPlans] = useState([]); const [criticalEvents, setCriticalEvents] = useState([]); const [loading, setLoading] = useState(false); @@ -145,89 +208,333 @@ export function EnforcementPlan() { } /> - {/* 로딩/에러 상태 */} - {loading && ( -
단속 계획을 불러오는 중...
- )} - {error && ( -
로드 실패: {error}
- )} - -
- {[ - { l: '오늘 계획', v: `${todayCount}건`, c: 'text-heading', i: Calendar }, - { l: '경보 발령', v: `${alertCount}건`, c: 'text-red-400', i: AlertTriangle }, - { l: '투입 함정', v: `${totalShips}척`, c: 'text-cyan-400', i: Ship }, - { l: '투입 인력', v: `${totalCrew}명`, c: 'text-green-400', i: Users }, - ].map(k => ( -
- {k.v}{k.l} -
+ {/* 탭 */} +
+ {([ + { key: 'overview' as PlanTab, icon: Calendar, label: '단속 계획' }, + { key: 'single' as PlanTab, icon: Navigation, label: '단일 함정 순찰 작전' }, + { key: 'multi' as PlanTab, icon: Anchor, label: '다함정 순찰 작전' }, + ]).map((t) => ( + ))}
- {/* 미배정 CRITICAL 이벤트 */} - {criticalEvents.length > 0 && ( - - -
- - 미배정 CRITICAL 이벤트 - {criticalEvents.length}건 -
-
- {criticalEvents.map((evt) => ( -
- - {getAlertLevelLabel(evt.level, tc, lang)} - - {evt.title} - {formatDateTime(evt.occurredAt)} - {evt.vesselMmsi ?? '-'} -
- ))} -
-
-
- )} - - -
경보 임계값 설정
-
- {[['위험도 ≥ 80', '상황실 즉시 경보 (알림+SMS)'], ['위험도 ≥ 60', '관련 부서 주의 알림'], ['위험도 ≥ 40', '참고 로그 기록']].map(([k, v]) => ( -
- {k} - {v} + {/* ── ① 단속 계획 (기존) ── */} + {tab === 'overview' && ( +
+ {loading && ( +
단속 계획을 불러오는 중...
+ )} + {error && ( +
로드 실패: {error}
+ )} + +
+ {[ + { l: '오늘 계획', v: `${todayCount}건`, c: 'text-heading', i: Calendar }, + { l: '경보 발령', v: `${alertCount}건`, c: 'text-red-400', i: AlertTriangle }, + { l: '투입 함정', v: `${totalShips}척`, c: 'text-cyan-400', i: Ship }, + { l: '투입 인력', v: `${totalCrew}명`, c: 'text-green-400', i: Users }, + ].map(k => ( +
+ {k.v}{k.l}
))}
- - - + {criticalEvents.length > 0 && ( + + +
+ + 미배정 CRITICAL 이벤트 + {criticalEvents.length}건 +
+
+ {criticalEvents.map((evt) => ( +
+ + {getAlertLevelLabel(evt.level, tc, lang)} + + {evt.title} + {formatDateTime(evt.occurredAt)} + {evt.vesselMmsi ?? '-'} +
+ ))} +
+
+
+ )} - {/* 단속 구역 지도 */} - - - - {/* 범례 */} -
-
단속 구역
-
-
위험도 80+
-
위험도 60~80
-
위험도 40~60
-
-
-
확정
-
계획중
-
+ + +
경보 임계값 설정
+
+ {[['위험도 ≥ 80', '상황실 즉시 경보 (알림+SMS)'], ['위험도 ≥ 60', '관련 부서 주의 알림'], ['위험도 ≥ 40', '참고 로그 기록']].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ + + + + +
+
단속 구역
+
+
위험도 80+
+
위험도 60~80
+
위험도 40~60
+
+
+
확정
+
계획중
+
+
+
+ {PLANS.length}개 + 단속 구역 배치 +
+ + +
+ )} + + {/* ── ② 단일 함정 순찰 작전 ── */} + {tab === 'single' && ( +
+ {/* 작전 유형 */} +
+ {SINGLE_OP_TYPES.map((op) => ( + + +
+ + {op.type} +
+

{op.desc}

+
+ {[ + ['소요시간', op.duration], + ['필요인력', op.crew], + ['위험수준', op.risk], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ ))}
-
- {PLANS.length}개 - 단속 구역 배치 + + {/* 가용 함정 현황 */} + + +
+ + 가용 함정 현황 + {AVAILABLE_SHIPS.filter(s => s.status === '가용').length}척 가용 +
+ + + + + + + + + + + + + + {AVAILABLE_SHIPS.map((s) => ( + + + + + + + + + + ))} + +
함정명함급최대속력장비승조원배치 구역상태
{s.name}{s.type}{s.speed}{s.equip}{s.crew}명{s.zone}{s.status}
+
+
+ + {/* 단일 함정 작전 절차 */} + + +
+ + 단일 함정 작전 절차 (SOP) +
+
+ {SINGLE_SCENARIOS.map((s, i) => ( +
+ {i < SINGLE_SCENARIOS.length - 1 && ( + + )} +
+
+ {s.phase} +
+
{s.time}
+
    + {s.actions.map((a) => ( +
  • + + {a} +
  • + ))} +
+
+
+ ))} +
+
+
+
+ )} + + {/* ── ③ 다함정 순찰 작전 ── */} + {tab === 'multi' && ( +
+ {/* 작전 유형 */} +
+ {MULTI_OP_TYPES.map((op) => ( + + +
+ + {op.type} +
+

{op.desc}

+
+ {[ + ['투입 함정', op.ships], + ['대형', op.formation], + ['지휘 체계', op.command], + ].map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+
+
+ ))}
- - + + {/* 함정 역할 분담 */} + + +
+ + 함정별 역할 분담 +
+
+ {MULTI_ROLES.map((r) => ( +
+
+ {r.role} +
+
+
{r.duty}
+
+ 요구 사양 + {r.requirement} +
+
+ 통신 + {r.comm} +
+
+
+ ))} +
+
+
+ + {/* 통신 프로토콜 */} + + +
+ + 함정 간 통신 프로토콜 +
+ + + + + + + + + + + {MULTI_COMM_PROTOCOL.map((c) => ( + + + + + + + ))} + +
채널용도암호화사용 시점
{c.channel}{c.purpose} + {c.encryption} + {c.usage}
+
+
+ + {/* 다함정 합동 작전 절차 */} + + +
+ + 다함정 합동 작전 절차 (SOP) +
+
+ {MULTI_SCENARIOS.map((s, i) => ( +
+ {i < MULTI_SCENARIOS.length - 1 && ( + + )} +
+
+ {s.phase} + {s.commander} +
+
    + {s.tasks.map((t) => ( +
  • + + {t} +
  • + ))} +
+
+
+ ))} +
+
+
+
+ )} ); }