feat: 단속 계획에 단일 함정 순찰 작전·다함정 순찰 작전 탭 추가

- 단일 함정 순찰 작전: 4가지 작전 유형(정찰 순찰/긴급 출동/감시 초계/근접 차단), 가용 함정 현황, SOP 절차
- 다함정 순찰 작전: 4가지 작전 유형(포위 차단/광역 초계/합동 단속/호위), 역할 분담, 통신 프로토콜, SOP 절차

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-04-14 11:36:33 +09:00
부모 a238887322
커밋 8fe55474d9

파일 보기

@ -7,7 +7,11 @@ import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { getRiskIntent, getStatusIntent } from '@shared/constants/statusIntent'; import { getRiskIntent, getStatusIntent } from '@shared/constants/statusIntent';
import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels'; 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 { BaseMap, STATIC_LAYERS, createMarkerLayer, createRadiusLayer, useMapLayers, type MapHandle } from '@lib/map';
import type { MarkerData } from '@lib/map'; import type { MarkerData } from '@lib/map';
import { getEnforcementPlans, type EnforcementPlan as EnforcementPlanApi } from '@/services/enforcement'; import { getEnforcementPlans, type EnforcementPlan as EnforcementPlanApi } from '@/services/enforcement';
@ -17,6 +21,64 @@ import { useSettingsStore } from '@stores/settingsStore';
/* SFR-06: 단속 계획·경보 연계(단속 우선지역 예보) */ /* 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; } 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 변환 */ /** API 응답 → 화면용 Plan 변환 */
@ -54,6 +116,7 @@ export function EnforcementPlan() {
const { t: tc } = useTranslation('common'); const { t: tc } = useTranslation('common');
const lang = useSettingsStore((s) => s.language); const lang = useSettingsStore((s) => s.language);
const [tab, setTab] = useState<PlanTab>('overview');
const [plans, setPlans] = useState<Plan[]>([]); const [plans, setPlans] = useState<Plan[]>([]);
const [criticalEvents, setCriticalEvents] = useState<PredictionEvent[]>([]); const [criticalEvents, setCriticalEvents] = useState<PredictionEvent[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -145,89 +208,333 @@ export function EnforcementPlan() {
} }
/> />
{/* 로딩/에러 상태 */} {/* 탭 */}
{loading && ( <div className="flex gap-0 border-b border-border">
<div className="text-center text-muted-foreground text-sm py-4"> ...</div> {([
)} { key: 'overview' as PlanTab, icon: Calendar, label: '단속 계획' },
{error && ( { key: 'single' as PlanTab, icon: Navigation, label: '단일 함정 순찰 작전' },
<div className="text-center text-red-400 text-sm py-4"> : {error}</div> { key: 'multi' as PlanTab, icon: Anchor, label: '다함정 순찰 작전' },
)} ]).map((t) => (
<button type="button" key={t.key} onClick={() => setTab(t.key)}
<div className="flex gap-2"> className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-orange-400 border-orange-400' : 'text-hint border-transparent hover:text-label'}`}>
{[ <t.icon className="w-3.5 h-3.5" />{t.label}
{ l: '오늘 계획', v: `${todayCount}`, c: 'text-heading', i: Calendar }, </button>
{ 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 => (
<div key={k.l} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
<k.i className={`w-4 h-4 ${k.c}`} /><span className={`text-base font-bold ${k.c}`}>{k.v}</span><span className="text-[9px] text-hint">{k.l}</span>
</div>
))} ))}
</div> </div>
{/* 미배정 CRITICAL 이벤트 */}
{criticalEvents.length > 0 && (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<AlertTriangle className="w-4 h-4 text-red-400" />
<span className="text-[12px] font-bold text-heading"> CRITICAL </span>
<Badge intent="critical" size="xs">{criticalEvents.length}</Badge>
</div>
<div className="space-y-1.5 max-h-48 overflow-y-auto">
{criticalEvents.map((evt) => (
<div key={evt.id} className="flex items-center gap-2 px-3 py-2 bg-red-500/5 border border-red-500/20 rounded-lg">
<Badge intent={getAlertLevelIntent(evt.level)} size="xs">
{getAlertLevelLabel(evt.level, tc, lang)}
</Badge>
<span className="text-[10px] text-heading font-medium flex-1 truncate">{evt.title}</span>
<span className="text-[9px] text-hint shrink-0">{formatDateTime(evt.occurredAt)}</span>
<span className="text-[9px] text-cyan-400 font-mono shrink-0">{evt.vesselMmsi ?? '-'}</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
<Card> {/* ── ① 단속 계획 (기존) ── */}
<CardContent className="p-4"> {tab === 'overview' && (
<div className="text-[12px] font-bold text-heading mb-3"> </div> <div className="space-y-3">
<div className="flex gap-4 text-[10px]"> {loading && (
{[['위험도 ≥ 80', '상황실 즉시 경보 (알림+SMS)'], ['위험도 ≥ 60', '관련 부서 주의 알림'], ['위험도 ≥ 40', '참고 로그 기록']].map(([k, v]) => ( <div className="text-center text-muted-foreground text-sm py-4"> ...</div>
<div key={k} className="flex items-center gap-2 px-3 py-2 bg-surface-overlay rounded-lg flex-1"> )}
<Badge intent="critical" size="sm">{k}</Badge> {error && (
<span className="text-muted-foreground">{v}</span> <div className="text-center text-red-400 text-sm py-4"> : {error}</div>
)}
<div className="flex gap-2">
{[
{ 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 => (
<div key={k.l} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
<k.i className={`w-4 h-4 ${k.c}`} /><span className={`text-base font-bold ${k.c}`}>{k.v}</span><span className="text-[9px] text-hint">{k.l}</span>
</div> </div>
))} ))}
</div> </div>
</CardContent> {criticalEvents.length > 0 && (
</Card> <Card>
<DataTable data={PLANS} columns={cols} pageSize={10} searchPlaceholder="구역, 함정명 검색..." searchKeys={['zone', 'ships']} exportFilename="단속계획" /> <CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<AlertTriangle className="w-4 h-4 text-red-400" />
<span className="text-[12px] font-bold text-heading"> CRITICAL </span>
<Badge intent="critical" size="xs">{criticalEvents.length}</Badge>
</div>
<div className="space-y-1.5 max-h-48 overflow-y-auto">
{criticalEvents.map((evt) => (
<div key={evt.id} className="flex items-center gap-2 px-3 py-2 bg-red-500/5 border border-red-500/20 rounded-lg">
<Badge intent={getAlertLevelIntent(evt.level)} size="xs">
{getAlertLevelLabel(evt.level, tc, lang)}
</Badge>
<span className="text-[10px] text-heading font-medium flex-1 truncate">{evt.title}</span>
<span className="text-[9px] text-hint shrink-0">{formatDateTime(evt.occurredAt)}</span>
<span className="text-[9px] text-cyan-400 font-mono shrink-0">{evt.vesselMmsi ?? '-'}</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* 단속 구역 지도 */} <Card>
<Card> <CardContent className="p-4">
<CardContent className="p-0 relative"> <div className="text-[12px] font-bold text-heading mb-3"> </div>
<BaseMap ref={mapRef} center={[36.2, 126.0]} zoom={7} height={420} className="rounded-lg overflow-hidden" /> <div className="flex gap-4 text-[10px]">
{/* 범례 */} {[['위험도 ≥ 80', '상황실 즉시 경보 (알림+SMS)'], ['위험도 ≥ 60', '관련 부서 주의 알림'], ['위험도 ≥ 40', '참고 로그 기록']].map(([k, v]) => (
<div className="absolute bottom-3 left-3 z-[1000] bg-background/90 backdrop-blur-sm border border-border rounded-lg px-3 py-2"> <div key={k} className="flex items-center gap-2 px-3 py-2 bg-surface-overlay rounded-lg flex-1">
<div className="text-[9px] text-muted-foreground font-bold mb-1.5"> </div> <Badge intent="critical" size="sm">{k}</Badge>
<div className="space-y-1"> <span className="text-muted-foreground">{v}</span>
<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded-full bg-red-500/70 border border-red-500" /><span className="text-[8px] text-muted-foreground"> 80+</span></div> </div>
<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded-full bg-orange-500/70 border border-orange-500" /><span className="text-[8px] text-muted-foreground"> 60~80</span></div> ))}
<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded-full bg-yellow-500/70 border border-yellow-500" /><span className="text-[8px] text-muted-foreground"> 40~60</span></div> </div>
</div> </CardContent>
<div className="flex items-center gap-3 mt-1.5 pt-1.5 border-t border-border"> </Card>
<div className="flex items-center gap-1"><div className="w-6 h-3 rounded-sm border-2 border-orange-500/50 bg-orange-500/15" /><span className="text-[7px] text-hint"></span></div> <DataTable data={PLANS} columns={cols} pageSize={10} searchPlaceholder="구역, 함정명 검색..." searchKeys={['zone', 'ships']} exportFilename="단속계획" />
<div className="flex items-center gap-1"><div className="w-6 h-3 rounded-sm border border-dashed border-orange-500/40 bg-orange-500/5" /><span className="text-[7px] text-hint"></span></div>
</div> <Card>
<CardContent className="p-0 relative">
<BaseMap ref={mapRef} center={[36.2, 126.0]} zoom={7} height={420} className="rounded-lg overflow-hidden" />
<div className="absolute bottom-3 left-3 z-[1000] bg-background/90 backdrop-blur-sm border border-border rounded-lg px-3 py-2">
<div className="text-[9px] text-muted-foreground font-bold mb-1.5"> </div>
<div className="space-y-1">
<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded-full bg-red-500/70 border border-red-500" /><span className="text-[8px] text-muted-foreground"> 80+</span></div>
<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded-full bg-orange-500/70 border border-orange-500" /><span className="text-[8px] text-muted-foreground"> 60~80</span></div>
<div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded-full bg-yellow-500/70 border border-yellow-500" /><span className="text-[8px] text-muted-foreground"> 40~60</span></div>
</div>
<div className="flex items-center gap-3 mt-1.5 pt-1.5 border-t border-border">
<div className="flex items-center gap-1"><div className="w-6 h-3 rounded-sm border-2 border-orange-500/50 bg-orange-500/15" /><span className="text-[7px] text-hint"></span></div>
<div className="flex items-center gap-1"><div className="w-6 h-3 rounded-sm border border-dashed border-orange-500/40 bg-orange-500/5" /><span className="text-[7px] text-hint"></span></div>
</div>
</div>
<div className="absolute top-3 left-3 z-[1000] bg-background/90 backdrop-blur-sm border border-border rounded-lg px-3 py-1.5">
<span className="text-[10px] text-orange-400 font-bold">{PLANS.length}</span>
<span className="text-[9px] text-hint ml-1"> </span>
</div>
</CardContent>
</Card>
</div>
)}
{/* ── ② 단일 함정 순찰 작전 ── */}
{tab === 'single' && (
<div className="space-y-3">
{/* 작전 유형 */}
<div className="grid grid-cols-4 gap-2">
{SINGLE_OP_TYPES.map((op) => (
<Card key={op.type} className="bg-surface-raised border-border">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<op.icon className="w-4 h-4 text-orange-400" />
<span className="text-[12px] font-bold text-heading">{op.type}</span>
</div>
<p className="text-[9px] text-hint mb-3">{op.desc}</p>
<div className="space-y-1">
{[
['소요시간', op.duration],
['필요인력', op.crew],
['위험수준', op.risk],
].map(([k, v]) => (
<div key={k} className="flex justify-between text-[10px] px-2 py-1 bg-surface-overlay rounded">
<span className="text-muted-foreground">{k}</span>
<span className="text-heading font-medium">{v}</span>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div> </div>
<div className="absolute top-3 left-3 z-[1000] bg-background/90 backdrop-blur-sm border border-border rounded-lg px-3 py-1.5">
<span className="text-[10px] text-orange-400 font-bold">{PLANS.length}</span> {/* 가용 함정 현황 */}
<span className="text-[9px] text-hint ml-1"> </span> <Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Ship className="w-4 h-4 text-cyan-400" />
<span className="text-[12px] font-bold text-heading"> </span>
<Badge intent="info" size="xs">{AVAILABLE_SHIPS.filter(s => s.status === '가용').length} </Badge>
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th>
<th className="text-center py-2"></th>
<th className="text-center py-2"></th>
<th className="text-left py-2"></th>
<th className="text-center py-2"></th>
<th className="text-center py-2"> </th>
<th className="text-center py-2"></th>
</tr>
</thead>
<tbody>
{AVAILABLE_SHIPS.map((s) => (
<tr key={s.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2 px-2 text-heading font-bold">{s.name}</td>
<td className="py-2 text-center text-muted-foreground">{s.type}</td>
<td className="py-2 text-center text-cyan-400 font-mono">{s.speed}</td>
<td className="py-2 text-muted-foreground text-[9px]">{s.equip}</td>
<td className="py-2 text-center text-heading">{s.crew}</td>
<td className="py-2 text-center text-muted-foreground">{s.zone}</td>
<td className="py-2 text-center"><Badge intent={getStatusIntent(s.status)} size="xs">{s.status}</Badge></td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
{/* 단일 함정 작전 절차 */}
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-blue-400" />
<span className="text-[12px] font-bold text-heading"> (SOP)</span>
</div>
<div className="grid grid-cols-4 gap-3">
{SINGLE_SCENARIOS.map((s, i) => (
<div key={s.phase} className="relative">
{i < SINGLE_SCENARIOS.length - 1 && (
<ChevronRight className="absolute -right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-border z-10" />
)}
<div className="bg-surface-overlay rounded-lg p-3 h-full">
<div className="flex items-center gap-2 mb-2">
<span className="text-[11px] font-bold text-orange-400">{s.phase}</span>
</div>
<div className="text-[9px] text-cyan-400 mb-2">{s.time}</div>
<ul className="space-y-1.5">
{s.actions.map((a) => (
<li key={a} className="flex items-start gap-1.5 text-[9px] text-muted-foreground">
<CheckCircle className="w-3 h-3 text-green-500 shrink-0 mt-0.5" />
<span>{a}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
{/* ── ③ 다함정 순찰 작전 ── */}
{tab === 'multi' && (
<div className="space-y-3">
{/* 작전 유형 */}
<div className="grid grid-cols-4 gap-2">
{MULTI_OP_TYPES.map((op) => (
<Card key={op.type} className="bg-surface-raised border-border">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<op.icon className="w-4 h-4 text-blue-400" />
<span className="text-[11px] font-bold text-heading">{op.type}</span>
</div>
<p className="text-[9px] text-hint mb-3">{op.desc}</p>
<div className="space-y-1">
{[
['투입 함정', op.ships],
['대형', op.formation],
['지휘 체계', op.command],
].map(([k, v]) => (
<div key={k} className="flex justify-between text-[10px] px-2 py-1 bg-surface-overlay rounded">
<span className="text-muted-foreground">{k}</span>
<span className="text-heading font-medium text-[9px]">{v}</span>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div> </div>
</CardContent>
</Card> {/* 함정 역할 분담 */}
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Users className="w-4 h-4 text-purple-400" />
<span className="text-[12px] font-bold text-heading"> </span>
</div>
<div className="grid grid-cols-4 gap-3">
{MULTI_ROLES.map((r) => (
<div key={r.role} className="bg-surface-overlay rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Badge intent={r.badge} size="sm">{r.role}</Badge>
</div>
<div className="space-y-1.5">
<div className="text-[10px] text-heading">{r.duty}</div>
<div className="flex justify-between text-[9px] px-2 py-1 bg-surface-raised rounded">
<span className="text-muted-foreground"> </span>
<span className="text-label">{r.requirement}</span>
</div>
<div className="flex justify-between text-[9px] px-2 py-1 bg-surface-raised rounded">
<span className="text-muted-foreground"></span>
<span className="text-label">{r.comm}</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 통신 프로토콜 */}
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Radio className="w-4 h-4 text-green-400" />
<span className="text-[12px] font-bold text-heading"> </span>
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th>
<th className="text-left py-2"></th>
<th className="text-center py-2"></th>
<th className="text-left py-2"> </th>
</tr>
</thead>
<tbody>
{MULTI_COMM_PROTOCOL.map((c) => (
<tr key={c.channel} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2 px-2 text-heading font-medium">{c.channel}</td>
<td className="py-2 text-muted-foreground">{c.purpose}</td>
<td className="py-2 text-center">
<Badge intent={c.encryption === 'AES-256' || c.encryption === 'TLS 1.3' ? 'success' : 'muted'} size="xs">{c.encryption}</Badge>
</td>
<td className="py-2 text-muted-foreground">{c.usage}</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
{/* 다함정 합동 작전 절차 */}
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Shield className="w-4 h-4 text-orange-400" />
<span className="text-[12px] font-bold text-heading"> (SOP)</span>
</div>
<div className="grid grid-cols-4 gap-3">
{MULTI_SCENARIOS.map((s, i) => (
<div key={s.phase} className="relative">
{i < MULTI_SCENARIOS.length - 1 && (
<ChevronRight className="absolute -right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-border z-10" />
)}
<div className="bg-surface-overlay rounded-lg p-3 h-full">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-blue-400">{s.phase}</span>
<Badge intent="muted" size="xs">{s.commander}</Badge>
</div>
<ul className="space-y-1.5">
{s.tasks.map((t) => (
<li key={t} className="flex items-start gap-1.5 text-[9px] text-muted-foreground">
<CheckCircle className="w-3 h-3 text-green-500 shrink-0 mt-0.5" />
<span>{t}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
</PageContainer> </PageContainer>
); );
} }