refactor(korea): 작전가이드 → 실전형 순찰 루트 가이드로 변경

- 해경 기지 선택 → 주변 불법어선·어구 자동 탐지
- 탐색 반경 10~100NM 설정 가능
- 중국 선박 대상 위험도 자동 판정 (CRITICAL/HIGH/MEDIUM)
  - 비허가 수역 진입 → CRITICAL
  - 수역I 저인망 의심 → HIGH
  - 다크베셀 (AIS 비정상) → HIGH
  - 어구/어망 AIS 신호 → HIGH
  - 조업 추정 (2~6kn) → MEDIUM
  - 운반선/환적 의심 → MEDIUM
- 우선순위 정렬: 위험도 → 거리순
- 선박 클릭 → 지도 이동 (flyTo)
- 순찰 루트 제안 (가장 가까운 고위험 대상부터)

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

파일 보기

@ -666,7 +666,11 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
<ReportModal ships={koreaData.ships} onClose={() => setShowReport(false)} />
)}
{showOpsGuide && (
<OpsGuideModal onClose={() => setShowOpsGuide(false)} />
<OpsGuideModal
ships={koreaData.ships}
onClose={() => setShowOpsGuide(false)}
onFlyTo={(lat, lng, zoom) => setFlyToTarget({ lat, lng, zoom })}
/>
)}
<KoreaMap
ships={koreaFiltersResult.filteredShips}

파일 보기

@ -1,216 +1,237 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import type { Ship } from '../../types';
import { COAST_GUARD_FACILITIES, CG_TYPE_LABEL } from '../../services/coastGuard';
import type { CoastGuardFacility } from '../../services/coastGuard';
import { getMarineTrafficCategory } from '../../utils/marineTraffic';
import { analyzeFishing, classifyFishingZone } from '../../utils/fishingAnalysis';
interface Props {
ships: Ship[];
onClose: () => void;
onFlyTo?: (lat: number, lng: number, zoom: number) => void;
}
type Tab = 'overview' | 'pt' | 'gn' | 'ps' | 'fc' | 'gear' | 'alert';
interface SuspectVessel {
ship: Ship;
distance: number; // NM from selected KCG
reasons: string[];
riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM';
}
const C = {
bg: '#0a0f1a', card: '#111827', border: '#1e293b',
text: '#e2e8f0', dim: '#64748b', accent: '#3b82f6',
green: '#22c55e', red: '#ef4444', yellow: '#f59e0b', cyan: '#06b6d4',
};
function haversineNM(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 3440.065; // Earth radius in NM
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
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: '🚨' },
];
const RISK_COLOR = { CRITICAL: '#ef4444', HIGH: '#f59e0b', MEDIUM: '#3b82f6' };
const RISK_ICON = { CRITICAL: '🔴', HIGH: '🟡', MEDIUM: '🔵' };
export function OpsGuideModal({ onClose }: Props) {
const [tab, setTab] = useState<Tab>('overview');
export function OpsGuideModal({ ships, onClose, onFlyTo }: Props) {
const [selectedKCG, setSelectedKCG] = useState<CoastGuardFacility | null>(null);
const [searchRadius, setSearchRadius] = useState(30); // NM
// 해경서/지방청만 (파출소 제외)
const kcgBases = useMemo(() =>
COAST_GUARD_FACILITIES.filter(f => ['hq', 'regional', 'station', 'navy'].includes(f.type))
.sort((a, b) => a.name.localeCompare(b.name)),
[]);
// 선택된 해경 기지 주변 의심 선박 탐지
const suspects = useMemo<SuspectVessel[]>(() => {
if (!selectedKCG) return [];
const results: SuspectVessel[] = [];
for (const ship of ships) {
if (ship.flag !== 'CN') continue;
const dist = haversineNM(selectedKCG.lat, selectedKCG.lng, ship.lat, ship.lng);
if (dist > searchRadius) continue;
const cat = getMarineTrafficCategory(ship.typecode, ship.category);
const isFishing = cat === 'fishing' || ship.category === 'fishing' || ship.typecode === '30';
const isGear = /[_]\d+[_]|%$/.test(ship.name);
const analysis = isFishing ? analyzeFishing(ship) : null;
const zone = classifyFishingZone(ship.lat, ship.lng);
const reasons: string[] = [];
let riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM' = 'MEDIUM';
// 수역 외 어선
if (isFishing && zone.zone === 'OUTSIDE') {
reasons.push('비허가 수역 진입');
riskLevel = 'CRITICAL';
}
// 수역 I에 PT/OT 선박
if (isFishing && zone.zone === 'ZONE_I' && ship.speed >= 2 && ship.speed <= 5) {
reasons.push('수역I 저인망 의심 (PT/OT 비허가)');
riskLevel = 'HIGH';
}
// 다크베셀 의심 (속도 0, 방향 0)
if (isFishing && ship.speed === 0 && (!ship.heading || ship.heading === 0)) {
reasons.push('AIS 비정상 (다크베셀 의심)');
if (riskLevel === 'MEDIUM') riskLevel = 'HIGH';
}
// 조업 중 (2-6kn)
if (isFishing && ship.speed >= 2 && ship.speed <= 6) {
reasons.push(`조업 추정 (${ship.speed.toFixed(1)}kn)`);
}
// 어구/어망
if (isGear) {
reasons.push('어구/어망 AIS 신호');
if (riskLevel === 'MEDIUM') riskLevel = 'HIGH';
}
// 대형 선박 (운반선 의심)
if (!isFishing && (cat === 'cargo' || cat === 'unspecified') && ship.speed < 3) {
reasons.push('운반선/환적 의심 (저속 대형)');
}
if (reasons.length > 0) {
results.push({ ship, distance: dist, reasons, riskLevel });
}
}
return results.sort((a, b) => {
const riskOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2 };
return riskOrder[a.riskLevel] - riskOrder[b.riskLevel] || a.distance - b.distance;
});
}, [selectedKCG, ships, searchRadius]);
const criticalCount = suspects.filter(s => s.riskLevel === 'CRITICAL').length;
const highCount = suspects.filter(s => s.riskLevel === 'HIGH').length;
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()}>
<div style={{ width: '92vw', maxWidth: 800, maxHeight: '90vh', overflow: 'hidden', background: '#0a0f1a', borderRadius: 8, border: '1px solid #1e293b', 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 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 16px', borderBottom: '1px solid #1e293b', 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>
<span style={{ fontSize: 14, fontWeight: 700, color: '#e2e8f0' }}> </span>
<span style={{ fontSize: 9, color: '#64748b' }}> · </span>
<button onClick={onClose} style={{ marginLeft: 'auto', background: 'rgba(255,82,82,0.1)', border: '1px solid rgba(255,82,82,0.4)', color: '#ef4444', 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>
))}
{/* Controls */}
<div style={{ display: 'flex', gap: 8, padding: '8px 16px', borderBottom: '1px solid #1e293b', flexShrink: 0, alignItems: 'center', flexWrap: 'wrap' }}>
<label style={{ fontSize: 10, color: '#94a3b8', fontWeight: 700 }}> :</label>
<select
value={selectedKCG?.id ?? ''}
onChange={e => {
const f = kcgBases.find(b => b.id === Number(e.target.value));
setSelectedKCG(f || null);
}}
style={{ background: '#1e293b', border: '1px solid #334155', borderRadius: 4, padding: '4px 8px', fontSize: 10, color: '#e2e8f0', minWidth: 180 }}
>
<option value="">-- --</option>
{kcgBases.map(b => (
<option key={b.id} value={b.id}>[{CG_TYPE_LABEL[b.type]}] {b.name}</option>
))}
</select>
<label style={{ fontSize: 10, color: '#94a3b8', fontWeight: 700, marginLeft: 8 }}> :</label>
<select value={searchRadius} onChange={e => setSearchRadius(Number(e.target.value))} style={{ background: '#1e293b', border: '1px solid #334155', borderRadius: 4, padding: '4px 8px', fontSize: 10, color: '#e2e8f0' }}>
<option value={10}>10 NM</option>
<option value={20}>20 NM</option>
<option value={30}>30 NM</option>
<option value={50}>50 NM</option>
<option value={100}>100 NM</option>
</select>
{selectedKCG && (
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8, fontSize: 10 }}>
<span style={{ color: '#ef4444', fontWeight: 700 }}>🔴 CRITICAL {criticalCount}</span>
<span style={{ color: '#f59e0b', fontWeight: 700 }}>🟡 HIGH {highCount}</span>
<span style={{ color: '#3b82f6', fontWeight: 700 }}>🔵 TOTAL {suspects.length}</span>
</div>
)}
</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 style={{ flex: 1, overflow: 'auto', padding: '8px 16px' }}>
{!selectedKCG ? (
<div style={{ textAlign: 'center', padding: '40px 0', color: '#64748b' }}>
<div style={{ fontSize: 32, marginBottom: 8 }}></div>
<div style={{ fontSize: 12 }}> · </div>
<div style={{ fontSize: 10, marginTop: 4 }}>// </div>
</div>
) : suspects.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px 0', color: '#22c55e' }}>
<div style={{ fontSize: 32, marginBottom: 8 }}></div>
<div style={{ fontSize: 12 }}>{selectedKCG.name} {searchRadius}NM </div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{/* Route summary */}
<div style={{ background: '#111827', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e293b', marginBottom: 4 }}>
<div style={{ fontSize: 11, fontWeight: 700, color: '#60a5fa', marginBottom: 4 }}>
📍 {selectedKCG.name} ({suspects.length})
</div>
<div style={{ fontSize: 9, color: '#94a3b8' }}>
우선순위: CRITICAL HIGH MEDIUM | |
</div>
{criticalCount > 0 && (
<div style={{ fontSize: 10, color: '#ef4444', fontWeight: 700, marginTop: 4 }}>
CRITICAL {criticalCount}
</div>
)}
</div>
{/* Suspect list */}
{suspects.map((s, i) => (
<div
key={s.ship.mmsi}
style={{
background: '#111827', borderRadius: 6, padding: '8px 12px',
border: `1px solid ${RISK_COLOR[s.riskLevel]}30`,
borderLeft: `3px solid ${RISK_COLOR[s.riskLevel]}`,
cursor: onFlyTo ? 'pointer' : 'default',
}}
onClick={() => onFlyTo?.(s.ship.lat, s.ship.lng, 12)}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: '#64748b', minWidth: 20 }}>#{i + 1}</span>
<span style={{ fontSize: 9 }}>{RISK_ICON[s.riskLevel]}</span>
<span style={{
fontSize: 9, fontWeight: 700, padding: '1px 6px', borderRadius: 3,
background: RISK_COLOR[s.riskLevel] + '20', color: RISK_COLOR[s.riskLevel],
}}>{s.riskLevel}</span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#e2e8f0' }}>{s.ship.name || s.ship.mmsi}</span>
<span style={{ fontSize: 9, color: '#64748b' }}>MMSI: {s.ship.mmsi}</span>
<span style={{ marginLeft: 'auto', fontSize: 10, color: '#60a5fa', fontWeight: 700 }}>{s.distance.toFixed(1)} NM</span>
</div>
<div style={{ display: 'flex', gap: 4, marginTop: 4, flexWrap: 'wrap' }}>
{s.reasons.map((r, j) => (
<span key={j} style={{
fontSize: 8, padding: '1px 5px', borderRadius: 2,
background: 'rgba(255,255,255,0.06)', color: '#94a3b8',
}}>{r}</span>
))}
</div>
<div style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 9, color: '#64748b' }}>
<span>SOG: {s.ship.speed?.toFixed(1) ?? '-'} kn</span>
<span>HDG: {s.ship.heading ?? '-'}°</span>
<span>{s.ship.lat.toFixed(4)}°N, {s.ship.lng.toFixed(4)}°E</span>
{onFlyTo && <span style={{ color: '#60a5fa', marginLeft: 'auto' }}> </span>}
</div>
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div style={{ padding: '6px 16px', borderTop: '1px solid #1e293b', fontSize: 8, color: '#475569', flexShrink: 0 }}>
(906) | 판정: Point-in-Polygon | 위험도: 속도··AIS
</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>
</>);
}