feat(korea): 경비함정 작전 가이드 모달 추가 (GC-KCG-2026-001 제7장)

- 탑메뉴 '작전가이드' 버튼 추가 (현장분석 옆)
- OpsGuideModal: 7개 탭 구성
  1. 작전 개요 (톤급별 구역/기간/임무 + 7일 스케줄)
  2. PT 저인망 대응 5단계 (접근 금지구역, 중국어 경고문)
  3. GN 유자망 대응 5단계 (다크베셀 탐지, AIS 재가동)
  4. PS 위망 선단 대응 5단계 (단독접근 금지, 宁波海裕)
  5. FC 운반선 환적 대응 4단계 (환적 신뢰도 판정)
  6. 어구 수거 절차 4단계 (자망/정치망/통발 식별)
  7. 조치 기준 (8대 위반유형 알람 등급 + 감시 강화 시기)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-24 15:25:32 +09:00
부모 c98d6ba353
커밋 f4ec6dd0f5
2개의 변경된 파일230개의 추가작업 그리고 0개의 파일을 삭제

파일 보기

@ -25,6 +25,7 @@ import LoginPage from './components/auth/LoginPage';
import CollectorMonitor from './components/common/CollectorMonitor';
import { FieldAnalysisModal } from './components/korea/FieldAnalysisModal';
import { ReportModal } from './components/korea/ReportModal';
import { OpsGuideModal } from './components/korea/OpsGuideModal';
import { filterFacilities } from './data/meEnergyHazardFacilities';
// 정적 데이터 카운트용 import (이미 useStaticDeckLayers에서 번들 포함됨)
import { EAST_ASIA_PORTS } from './data/ports';
@ -194,6 +195,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
const [showCollectorMonitor, setShowCollectorMonitor] = useState(false);
const [showFieldAnalysis, setShowFieldAnalysis] = useState(false);
const [showReport, setShowReport] = useState(false);
const [showOpsGuide, setShowOpsGuide] = useState(false);
const [timeZone, setTimeZone] = useState<'KST' | 'UTC'>('KST');
const [hoveredShipMmsi, setHoveredShipMmsi] = useState<string | null>(null);
const [focusShipMmsi, setFocusShipMmsi] = useState<string | null>(null);
@ -377,6 +379,15 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
<span className="text-[11px]">📊</span>
</button>
<button
type="button"
className={`mode-btn ${showOpsGuide ? 'active' : ''}`}
onClick={() => setShowOpsGuide(v => !v)}
title="경비함정 작전 가이드"
>
<span className="text-[11px]"></span>
</button>
</div>
)}
@ -654,6 +665,9 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
{showReport && (
<ReportModal ships={koreaData.ships} onClose={() => setShowReport(false)} />
)}
{showOpsGuide && (
<OpsGuideModal onClose={() => setShowOpsGuide(false)} />
)}
<KoreaMap
ships={koreaFiltersResult.filteredShips}
allShips={koreaData.visibleShips}

파일 보기

@ -0,0 +1,216 @@
import { useState } from 'react';
interface Props {
onClose: () => void;
}
type Tab = 'overview' | 'pt' | 'gn' | 'ps' | 'fc' | 'gear' | 'alert';
const C = {
bg: '#0a0f1a', card: '#111827', border: '#1e293b',
text: '#e2e8f0', dim: '#64748b', accent: '#3b82f6',
green: '#22c55e', red: '#ef4444', yellow: '#f59e0b', cyan: '#06b6d4',
};
const TABS: { key: Tab; label: string; icon: string }[] = [
{ key: 'overview', label: '작전 개요', icon: '🗺' },
{ key: 'pt', label: 'PT 저인망', icon: '🔴' },
{ key: 'gn', label: 'GN 유자망', icon: '🟡' },
{ key: 'ps', label: 'PS 위망', icon: '🟣' },
{ key: 'fc', label: 'FC 운반선', icon: '🟠' },
{ key: 'gear', label: '어구 수거', icon: '🪤' },
{ key: 'alert', label: '조치 기준', icon: '🚨' },
];
export function OpsGuideModal({ onClose }: Props) {
const [tab, setTab] = useState<Tab>('overview');
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={onClose}>
<div style={{ width: '92vw', maxWidth: 960, maxHeight: '90vh', overflow: 'hidden', background: C.bg, borderRadius: 8, border: `1px solid ${C.border}`, display: 'flex', flexDirection: 'column' }} onClick={e => e.stopPropagation()}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 16px', borderBottom: `1px solid ${C.border}`, background: 'rgba(30,58,95,0.5)', flexShrink: 0 }}>
<span style={{ fontSize: 14 }}></span>
<span style={{ fontSize: 14, fontWeight: 700, color: C.text }}> </span>
<span style={{ fontSize: 9, color: C.dim }}>GC-KCG-2026-001 7 </span>
<button onClick={onClose} style={{ marginLeft: 'auto', background: 'rgba(255,82,82,0.1)', border: '1px solid rgba(255,82,82,0.4)', color: C.red, padding: '4px 14px', cursor: 'pointer', fontSize: 11, borderRadius: 2 }}> </button>
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: 2, padding: '6px 16px', borderBottom: `1px solid ${C.border}`, flexShrink: 0, overflowX: 'auto' }}>
{TABS.map(t => (
<button key={t.key} onClick={() => setTab(t.key)} style={{
padding: '4px 10px', fontSize: 10, fontWeight: 700, borderRadius: 3, cursor: 'pointer', whiteSpace: 'nowrap',
background: tab === t.key ? 'rgba(59,130,246,0.2)' : 'transparent',
border: tab === t.key ? '1px solid rgba(59,130,246,0.4)' : '1px solid transparent',
color: tab === t.key ? '#60a5fa' : C.dim,
}}>
{t.icon} {t.label}
</button>
))}
</div>
{/* Content */}
<div style={{ flex: 1, overflow: 'auto', padding: '12px 20px', color: C.text, fontSize: 11, lineHeight: 1.8 }}>
{tab === 'overview' && <OverviewTab />}
{tab === 'pt' && <PTTab />}
{tab === 'gn' && <GNTab />}
{tab === 'ps' && <PSTab />}
{tab === 'fc' && <FCTab />}
{tab === 'gear' && <GearTab />}
{tab === 'alert' && <AlertTab />}
</div>
</div>
</div>
);
}
// ── Styles ──
const h2: React.CSSProperties = { fontSize: 13, color: '#60a5fa', borderLeft: '3px solid #3b82f6', paddingLeft: 8, margin: '16px 0 8px' };
const tbl: React.CSSProperties = { borderCollapse: 'collapse', width: '100%', fontSize: 10, margin: '6px 0' };
const th: React.CSSProperties = { border: '1px solid #1e293b', padding: '4px 8px', background: '#1e293b', color: '#e2e8f0', fontSize: 9, fontWeight: 700, textAlign: 'left' };
const td: React.CSSProperties = { border: '1px solid #1e293b', padding: '3px 8px', fontSize: 10 };
const tdB: React.CSSProperties = { ...td, fontWeight: 700, color: '#e2e8f0' };
const step: React.CSSProperties = { background: '#1e293b', borderRadius: 4, padding: '8px 12px', margin: '6px 0' };
const stepNum: React.CSSProperties = { display: 'inline-block', background: '#3b82f6', color: '#fff', borderRadius: 3, padding: '1px 6px', fontSize: 9, fontWeight: 700, marginRight: 6 };
const warn: React.CSSProperties = { background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)', borderRadius: 4, padding: '6px 10px', margin: '8px 0', fontSize: 10, color: '#fca5a5' };
function OverviewTab() {
return (<>
<h2 style={h2}> </h2>
<table style={tbl}>
<thead><tr><th style={th}> </th><th style={th}> </th><th style={th}> </th><th style={th}> </th></tr></thead>
<tbody>
<tr><td style={tdB}> (500 )</td><td style={td}>5~7</td><td style={td}>~ </td><td style={td}> . . IV .</td></tr>
<tr><td style={tdB}> (1,000)</td><td style={td}>7~10</td><td style={td}> (2 )</td><td style={td}> · . GN . .</td></tr>
<tr><td style={tdB}> (3,000)</td><td style={td}>10~14</td><td style={td}> ()</td><td style={td}>PT · ·. PS . EO .</td></tr>
<tr><td style={tdB}> (5,000)</td><td style={td}>10~14</td><td style={td}> </td><td style={td}> . . . · .</td></tr>
</tbody>
</table>
<h2 style={h2}> ( 7~10)</h2>
<table style={tbl}>
<thead><tr><th style={th}></th><th style={th}> </th><th style={th}> </th></tr></thead>
<tbody>
<tr><td style={tdB}>D+1</td><td style={td}> · </td><td style={td}>AIS , , </td></tr>
<tr><td style={tdB}>D+2~3 ()</td><td style={td}> </td><td style={td}>PT , GN , </td></tr>
<tr><td style={tdB}>D+2~3 ()</td><td style={td}> </td><td style={td}>EO· , PS , AIS SAR </td></tr>
<tr><td style={tdB}>D+4~5</td><td style={td}> </td><td style={td}> · ·, GPS ·</td></tr>
<tr><td style={tdB}>D+5~6</td><td style={td}>·</td><td style={td}> , · , </td></tr>
<tr><td style={tdB}>D+7</td><td style={td}> ·</td><td style={td}> , , , </td></tr>
</tbody>
</table>
</>);
}
function PTTab() {
return (<>
<h2 style={h2}>2 (PT) </h2>
<div style={warn}> () </div>
<div style={step}><span style={stepNum}>STEP 1</span><b> </b><br/>AIS MMSI DB . ·· . (PT)·(PT-S) , .</div>
<div style={step}><span style={stepNum}>STEP 2</span><b> </b><br/> ( 45° ) . VHF Ch.16 3. : "请立即停船接受检查"</div>
<div style={step}><span style={stepNum}>STEP 3</span><b> </b><br/>() (C21-xxxxx) <br/> (100/) <br/> 54mm </div>
<div style={step}><span style={stepNum}>STEP 4</span><b> </b><br/> (4/16~10/15) <br/> 100 <br/> </div>
<div style={step}><span style={stepNum}>STEP 5</span><b> </b><br/> 확정: 나포 ··· <br/> 위반: 현장 · . </div>
</>);
}
function GNTab() {
return (<>
<h2 style={h2}> (GN) </h2>
<div style={warn}> () </div>
<div style={step}><span style={stepNum}>STEP 1</span><b> </b><br/>AIS . SAR . 1NM </div>
<div style={step}><span style={stepNum}>STEP 2</span><b> </b><br/> . (90°) </div>
<div style={step}><span style={stepNum}>STEP 3</span><b>AIS </b><br/>"请打开AIS" . MMSI . </div>
<div style={step}><span style={stepNum}>STEP 4</span><b> </b><br/>(C25-xxxxx)<br/> GN은 IV까지 , I <br/> 28/<br/> (·)</div>
<div style={step}><span style={stepNum}>STEP 5</span><b> </b><br/> 자망: 즉시 /<br/> 미달: 어구 <br/> (GPS)· </div>
</>);
}
function PSTab() {
return (<>
<h2 style={h2}> (PS) </h2>
<div style={warn}> , . </div>
<div style={step}><span style={stepNum}>STEP 1</span><b> </b><br/> + (8~10kn)({'<'}3kn) . 3+ AIS . · . </div>
<div style={step}><span style={stepNum}>STEP 2</span><b> </b><br/>EO . MMSI . </div>
<div style={step}><span style={stepNum}>STEP 3</span><b> ( )</b><br/>·· . ( ) . VHF </div>
<div style={step}><span style={stepNum}>STEP 4</span><b> </b><br/>모선: 허가증(C23-xxxxx)·· . 1,500/(4)<br/>·조명선: 할당량 0 </div>
<div style={step}><span style={stepNum}>STEP 5</span><b> </b><br/> · . · . VHF . · </div>
<h2 style={h2}> </h2>
<table style={tbl}>
<thead><tr><th style={th}></th><th style={th}></th><th style={th}></th></tr></thead>
<tbody>
<tr><td style={tdB}> 22</td><td style={td}>490</td><td style={td}> 0 · () </td></tr>
<tr><td style={tdB}> 23</td><td style={td}>541</td><td style={td}> 0 · () </td></tr>
</tbody>
</table>
</>);
}
function FCTab() {
return (<>
<h2 style={h2}> (FC) </h2>
<div style={step}><span style={stepNum}>STEP 1</span><b> </b><br/> 시스템: FC + 0.5NM + 2kn + 30 HIGH . . · </div>
<div style={step}><span style={stepNum}>STEP 2</span><b> </b><br/> () . . MMSI·· . (· )</div>
<div style={step}><span style={stepNum}>STEP 3</span><b> ·</b><br/>운반선: 어획물 ··. ·<br/>조업선: 허가량 . PT-S </div>
<div style={step}><span style={stepNum}>STEP 4</span><b> </b><br/> · . . . </div>
<h2 style={h2}> </h2>
<table style={tbl}>
<thead><tr><th style={th}> </th><th style={th}></th><th style={th}></th></tr></thead>
<tbody>
<tr><td style={td}>30</td><td style={td}>60%</td><td style={tdB}>HIGH</td></tr>
<tr><td style={td}>60</td><td style={td}>80%</td><td style={tdB}>HIGH</td></tr>
<tr><td style={td}>120+</td><td style={td}>95%</td><td style={{ ...tdB, color: C.red }}>CRITICAL</td></tr>
</tbody>
</table>
</>);
}
function GearTab() {
return (<>
<h2 style={h2}> </h2>
<div style={warn}> . . </div>
<div style={step}><span style={stepNum}>STEP 1</span><b> </b><br/>GPS (WGS84). · . · . (··) </div>
<div style={step}><span style={stepNum}>STEP 2</span><b> </b><br/> · . . </div>
<div style={step}><span style={stepNum}>STEP 3</span><b> </b><br/>RIB . · . · </div>
<div style={step}><span style={stepNum}>STEP 4</span><b> </b><br/>···· . · . </div>
<h2 style={h2}> </h2>
<table style={tbl}>
<thead><tr><th style={th}></th><th style={th}></th><th style={th}> </th><th style={th}> </th></tr></thead>
<tbody>
<tr><td style={tdB}> ()</td><td style={td}> , , ·</td><td style={td}> ~ </td><td style={td}>RIB + </td></tr>
<tr><td style={tdB}> ()</td><td style={td}> , </td><td style={td}> , </td><td style={td}>·</td></tr>
<tr><td style={tdB}> ()</td><td style={td}>/, </td><td style={td}>, </td><td style={td}>RIB + </td></tr>
</tbody>
</table>
</>);
}
function AlertTab() {
return (<>
<h2 style={h2}> </h2>
<table style={tbl}>
<thead><tr><th style={th}> </th><th style={th}> </th><th style={th}> </th><th style={th}></th></tr></thead>
<tbody>
<tr><td style={tdB}> </td><td style={td}>MMSI DB </td><td style={td}> ·</td><td style={{ ...td, color: C.red }}> CRITICAL</td></tr>
<tr><td style={tdB}> </td><td style={td}>C21·C22: 4/16~10/15<br/>C25: 6/2~8/31</td><td style={td}> </td><td style={{ ...td, color: C.red }}> CRITICAL</td></tr>
<tr><td style={tdB}> </td><td style={td}> </td><td style={td}> </td><td style={{ ...td, color: C.yellow }}> HIGH</td></tr>
<tr><td style={tdB}>PT </td><td style={td}> 3NM+</td><td style={td}> </td><td style={{ ...td, color: C.yellow }}> HIGHCRITICAL</td></tr>
<tr><td style={tdB}> </td><td style={td}>FC+ 0.5NM+2kn+30</td><td style={td}> </td><td style={{ ...td, color: C.yellow }}> HIGH</td></tr>
<tr><td style={tdB}> </td><td style={td}> </td><td style={td}> ·</td><td style={td}> </td></tr>
<tr><td style={tdB}> </td><td style={td}>80~100% </td><td style={td}> ·</td><td style={{ ...td, color: C.red }}> CRITICAL</td></tr>
<tr><td style={tdB}></td><td style={td}>AIS 6+</td><td style={td}>· </td><td style={{ ...td, color: C.yellow }}> HIGH</td></tr>
</tbody>
</table>
<h2 style={h2}> </h2>
<table style={tbl}>
<thead><tr><th style={th}></th><th style={th}></th><th style={th}></th></tr></thead>
<tbody>
<tr><td style={tdB}>7~8</td><td style={td}>PS 16 </td><td style={{ ...td, color: C.red }}>C21·C22·C25 . </td></tr>
<tr><td style={tdB}>5</td><td style={td}>GN만 </td><td style={td}> (C21·C22) </td></tr>
<tr><td style={tdB}>4·10</td><td style={td}> </td><td style={td}>4/16, 10/16 </td></tr>
<tr><td style={tdB}>1~3·11~12</td><td style={td}> </td><td style={td}> </td></tr>
</tbody>
</table>
</>);
}