import { useState, useMemo, useRef, useCallback } 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'; export interface OpsRoute { from: { lat: number; lng: number; name: string }; to: { lat: number; lng: number; name: string; mmsi: string }; distanceNM: number; riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM'; } interface Props { ships: Ship[]; onClose: () => void; onFlyTo?: (lat: number, lng: number, zoom: number) => void; onRouteSelect?: (route: OpsRoute | null) => void; } interface SuspectVessel { ship: Ship; distance: number; reasons: string[]; riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM'; estimatedType: 'PT' | 'GN' | 'PS' | 'FC' | 'GEAR' | 'UNKNOWN'; } function haversineNM(lat1: number, lng1: number, lat2: number, lng2: number): number { const R = 3440.065; 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 RISK_COLOR = { CRITICAL: '#ef4444', HIGH: '#f59e0b', MEDIUM: '#3b82f6' }; const RISK_ICON = { CRITICAL: '๐Ÿ”ด', HIGH: '๐ŸŸก', MEDIUM: '๐Ÿ”ต' }; type Tab = 'detect' | 'procedure' | 'alert'; // โ”€โ”€ ์ค‘๊ตญ์–ด ๊ฒฝ๊ณ ๋ฌธ โ”€โ”€ const CN_WARNINGS: Record = { PT: [ { zh: '่ฏท็ซ‹ๅณๅœ่ˆนๆŽฅๅ—ๆฃ€ๆŸฅ', ko: '์ฆ‰์‹œ ์ •์„ ํ•˜์—ฌ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค', usage: 'VHF Ch.16 + ํ™•์„ฑ๊ธฐ' }, { zh: '่ฏทๅ‡บ็คบๆ•ๆž่ฎธๅฏ่ฏ', ko: '์–ด์—…ํ—ˆ๊ฐ€์ฆ์„ ์ œ์‹œํ•˜์‹œ์˜ค', usage: '์Šน์„  ๊ฒ€์‚ฌ ์‹œ' }, { zh: '่ฏทๅ‡บ็คบไฝœไธšๆ—ฅๅฟ—', ko: '์กฐ์—…์ผ์ง€๋ฅผ ์ œ์‹œํ•˜์‹œ์˜ค', usage: '์–ดํš๋Ÿ‰ ํ™•์ธ' }, { zh: 'ไฝ ็š„็ฝ‘็›ฎไธ็ฌฆๅˆ่ง„ๅฎš', ko: '๋ง๋ชฉ์ด ๊ทœ์ •์— ๋ฏธ๋‹ฌํ•ฉ๋‹ˆ๋‹ค', usage: '์–ด๊ตฌ ๊ฒ€์‚ฌ (54mm ๋ฏธ๋งŒ)' }, ], GN: [ { zh: '่ฏทๆ‰“ๅผ€AIS', ko: 'AIS๋ฅผ ์ผœ์‹œ์˜ค', usage: '๋‹คํฌ๋ฒ ์…€ ๋Œ€์‘' }, { zh: '่ฏท็ซ‹ๅณๅœ่ˆนๆŽฅๅ—ๆฃ€ๆŸฅ', ko: '์ฆ‰์‹œ ์ •์„ ํ•˜์—ฌ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค', usage: 'VHF + ํ™•์„ฑ๊ธฐ' }, { zh: 'ไฝ ๅœจ้ž่ฎธๅฏๅŒบๅŸŸไฝœไธš', ko: '๋น„ํ—ˆ๊ฐ€ ๊ตฌ์—ญ์—์„œ ์กฐ์—… ์ค‘์ž…๋‹ˆ๋‹ค', usage: '์ˆ˜์—ญ ์ดํƒˆ ์‹œ' }, { zh: '่ฏท็ซ‹ๅณๆ”ถๅ›žๆธ”็ฝ‘', ko: '์–ด๋ง์„ ์ฆ‰์‹œ ํšŒ์ˆ˜ํ•˜์‹œ์˜ค', usage: '๋ถˆ๋ฒ• ์ž๋ง ๋ฐœ๊ฒฌ' }, ], PS: [ { zh: 'ๆ‰€ๆœ‰่ˆนๅช็ซ‹ๅณๅœๆญขไฝœไธš', ko: '๋ชจ๋“  ์„ ๋ฐ• ์ฆ‰์‹œ ์กฐ์—… ์ค‘๋‹จ', usage: '์„ ๋‹จ ์ œ์•• ์‹œ' }, { zh: '่ฏท็ซ‹ๅณๅœ่ˆนๆŽฅๅ—ๆฃ€ๆŸฅ', ko: '์ฆ‰์‹œ ์ •์„ ํ•˜์—ฌ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค', usage: 'VHF + ํ™•์„ฑ๊ธฐ' }, { zh: 'ๅ…ณ้—ญ้›†้ฑผ็ฏ', ko: '์ง‘์–ด๋“ฑ์„ ๋„์‹œ์˜ค', usage: '์กฐ๋ช…์„  ๋Œ€์‘' }, { zh: 'ไธ่ฆ่ฏ•ๅ›พ้€ƒ่ท‘', ko: '๋„์ฃผ๋ฅผ ์‹œ๋„ํ•˜์ง€ ๋งˆ์‹œ์˜ค', usage: '๋„์ฃผ ์‹œ' }, ], FC: [ { zh: '่ฏท็ซ‹ๅณๅœ่ˆนๆŽฅๅ—ๆฃ€ๆŸฅ', ko: '์ฆ‰์‹œ ์ •์„ ํ•˜์—ฌ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค', usage: 'VHF + ํ™•์„ฑ๊ธฐ' }, { zh: '่ฏทๅ‡บ็คบ่ดง็‰ฉๆธ…ๅ•', ko: 'ํ™”๋ฌผ ๋ชฉ๋ก์„ ์ œ์‹œํ•˜์‹œ์˜ค', usage: 'ํ™˜์  ๊ฒ€์‚ฌ' }, { zh: '็ฆๆญข่ฝฌ่ฟๆธ”่Žท็‰ฉ', ko: '์–ดํš๋ฌผ ํ™˜์ ์„ ๊ธˆ์ง€ํ•ฉ๋‹ˆ๋‹ค', usage: 'ํ™˜์  ํ˜„์žฅ' }, ], GEAR: [ { zh: '่ฟ™ไบ›ๆธ”ๅ…ทๅฑžไบŽ้žๆณ•่ฎพ็ฝฎ', ko: '์ด ์–ด๊ตฌ๋Š” ๋ถˆ๋ฒ• ์„ค์น˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค', usage: '์–ด๊ตฌ ์ˆ˜๊ฑฐ ์‹œ' }, ], UNKNOWN: [ { zh: '่ฏท็ซ‹ๅณๅœ่ˆนๆŽฅๅ—ๆฃ€ๆŸฅ', ko: '์ฆ‰์‹œ ์ •์„ ํ•˜์—ฌ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ›์œผ์‹œ์˜ค', usage: '๊ธฐ๋ณธ ๊ฒฝ๊ณ ' }, { zh: '่ฏทๆ‰“ๅผ€AIS', ko: 'AIS๋ฅผ ์ผœ์‹œ์˜ค', usage: '๋‹คํฌ๋ฒ ์…€' }, ], }; function estimateVesselType(ship: Ship): 'PT' | 'GN' | 'PS' | 'FC' | 'GEAR' | 'UNKNOWN' { const cat = getMarineTrafficCategory(ship.typecode, ship.category); const isGear = /[_]\d+[_]|%$/.test(ship.name); if (isGear) return 'GEAR'; if (cat === 'cargo' || (cat === 'unspecified' && (ship.length || 0) > 50)) return 'FC'; if (cat !== 'fishing' && ship.category !== 'fishing' && ship.typecode !== '30') return 'UNKNOWN'; const spd = ship.speed || 0; if (spd >= 7) return 'PS'; if (spd < 1.5) return 'GN'; return 'PT'; } export function OpsGuideModal({ ships, onClose, onFlyTo, onRouteSelect }: Props) { const [selectedKCG, setSelectedKCG] = useState(null); const [searchRadius, setSearchRadius] = useState(30); const [pos, setPos] = useState({ x: 60, y: 60 }); const [tab, setTab] = useState('detect'); const [selectedSuspect, setSelectedSuspect] = useState(null); const [copiedIdx, setCopiedIdx] = useState(null); const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null); const onDragStart = useCallback((e: React.MouseEvent) => { e.preventDefault(); dragRef.current = { startX: e.clientX, startY: e.clientY, origX: pos.x, origY: pos.y }; const onMove = (ev: MouseEvent) => { if (!dragRef.current) return; setPos({ x: dragRef.current.origX + (ev.clientX - dragRef.current.startX), y: dragRef.current.origY + (ev.clientY - dragRef.current.startY) }); }; const onUp = () => { dragRef.current = null; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); }, [pos]); 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(() => { 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 zone = classifyFishingZone(ship.lat, ship.lng); const reasons: string[] = []; let riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM' = 'MEDIUM'; if (isFishing && zone.zone === 'OUTSIDE') { reasons.push('๋น„ํ—ˆ๊ฐ€ ์ˆ˜์—ญ ์ง„์ž…'); riskLevel = 'CRITICAL'; } if (isFishing && zone.zone === 'ZONE_I' && ship.speed >= 2 && ship.speed <= 5) { reasons.push('์ˆ˜์—ญI ์ €์ธ๋ง ์˜์‹ฌ'); riskLevel = 'HIGH'; } if (isFishing && ship.speed === 0 && (!ship.heading || ship.heading === 0)) { reasons.push('๋‹คํฌ๋ฒ ์…€ ์˜์‹ฌ'); if (riskLevel === 'MEDIUM') riskLevel = 'HIGH'; } 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, estimatedType: estimateVesselType(ship) }); } return results.sort((a, b) => ({ CRITICAL: 0, HIGH: 1, MEDIUM: 2 }[a.riskLevel] - { CRITICAL: 0, HIGH: 1, MEDIUM: 2 }[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; const [speakingIdx, setSpeakingIdx] = useState(null); const copyToClipboard = (text: string, idx: number) => { navigator.clipboard.writeText(text).then(() => { setCopiedIdx(idx); setTimeout(() => setCopiedIdx(null), 1500); }); }; const speakChinese = useCallback((text: string, idx: number) => { if (typeof window === 'undefined' || !window.speechSynthesis) return; window.speechSynthesis.cancel(); const utter = new SpeechSynthesisUtterance(text); utter.lang = 'zh-CN'; utter.rate = 0.85; utter.volume = 1; // Try to find a Chinese voice const voices = window.speechSynthesis.getVoices(); const zhVoice = voices.find(v => v.lang.startsWith('zh')) || voices.find(v => v.lang.includes('CN')); if (zhVoice) utter.voice = zhVoice; utter.onstart = () => setSpeakingIdx(idx); utter.onend = () => setSpeakingIdx(null); utter.onerror = () => setSpeakingIdx(null); window.speechSynthesis.speak(utter); }, []); const handleSuspectClick = (s: SuspectVessel) => { setSelectedSuspect(s); setTab('procedure'); onFlyTo?.(s.ship.lat, s.ship.lng, 10); if (selectedKCG) { onRouteSelect?.({ from: { lat: selectedKCG.lat, lng: selectedKCG.lng, name: selectedKCG.name }, to: { lat: s.ship.lat, lng: s.ship.lng, name: s.ship.name || s.ship.mmsi, mmsi: s.ship.mmsi }, distanceNM: s.distance, riskLevel: s.riskLevel }); } }; const TYPE_LABEL: Record = { PT: '์ €์ธ๋ง(PT)', GN: '์œ ์ž๋ง(GN)', PS: '์œ„๋ง(PS)', FC: '์šด๋ฐ˜์„ (FC)', GEAR: '์–ด๊ตฌ/์–ด๋ง', UNKNOWN: '๋ฏธ๋ถ„๋ฅ˜' }; return (
{/* Header */}
โ ฟ โš“ ๊ฒฝ๋น„ํ•จ์ • ์ž‘์ „ ๊ฐ€์ด๋“œ
{/* Tabs */}
{([['detect', '๐Ÿ” ์‹ค์‹œ๊ฐ„ ํƒ์ง€'], ['procedure', '๐Ÿ“‹ ๋Œ€์‘ ์ ˆ์ฐจ'], ['alert', '๐Ÿšจ ์กฐ์น˜ ๊ธฐ์ค€']] as [Tab, string][]).map(([k, l]) => ( ))}
{/* Controls (detect tab) */} {tab === 'detect' && (
{selectedKCG &&
๐Ÿ”ด {criticalCount} ๐ŸŸก {highCount} ๐Ÿ”ต {suspects.length}
}
)} {/* Content */}
{/* โ”€โ”€ TAB: ์‹ค์‹œ๊ฐ„ ํƒ์ง€ โ”€โ”€ */} {tab === 'detect' && (<> {!selectedKCG ? (
โš“ ์ถœ๋™ ๊ธฐ์ง€๋ฅผ ์„ ํƒํ•˜๋ฉด ์ฃผ๋ณ€ ๋ถˆ๋ฒ•์–ด์„ ยท์–ด๊ตฌ๋ฅผ ์ž๋™ ํƒ์ง€ํ•ฉ๋‹ˆ๋‹ค
) : suspects.length === 0 ? (
โœ… {selectedKCG.name} ๋ฐ˜๊ฒฝ {searchRadius}NM ๋‚ด ์˜์‹ฌ ์„ ๋ฐ• ์—†์Œ
) : (
{suspects.map((s, i) => (
handleSuspectClick(s)} style={{ background: '#111827', borderRadius: 4, padding: '6px 10px', borderLeft: `3px solid ${RISK_COLOR[s.riskLevel]}`, cursor: 'pointer', }}>
#{i + 1} {RISK_ICON[s.riskLevel]} {s.riskLevel} {s.ship.name || s.ship.mmsi} [{TYPE_LABEL[s.estimatedType]}] {s.distance.toFixed(1)} NM
{s.reasons.map((r, j) => {r})}
))}
)} )} {/* โ”€โ”€ TAB: ๋Œ€์‘ ์ ˆ์ฐจ โ”€โ”€ */} {tab === 'procedure' && (<> {selectedSuspect ? (
{/* ์„ ๋ฐ• ์ •๋ณด */}
{RISK_ICON[selectedSuspect.riskLevel]} {selectedSuspect.ship.name || selectedSuspect.ship.mmsi} {selectedSuspect.riskLevel} ์ถ”์ •: {TYPE_LABEL[selectedSuspect.estimatedType]}
MMSI: {selectedSuspect.ship.mmsi} | SOG: {selectedSuspect.ship.speed?.toFixed(1)}kn | {selectedSuspect.distance.toFixed(1)} NM
{/* ์—…์ข…๋ณ„ ๋Œ€์‘ ์ ˆ์ฐจ */} {/* ์ค‘๊ตญ์–ด ๊ฒฝ๊ณ ๋ฌธ */}
๐Ÿ“ข ์ค‘๊ตญ์–ด ๊ฒฝ๊ณ ๋ฌธ (ํด๋ฆญ: ๋ณต์‚ฌ | ๐Ÿ”Š: ์Œ์„ฑ)
{(CN_WARNINGS[selectedSuspect.estimatedType] || CN_WARNINGS.UNKNOWN).map((w, i) => (
copyToClipboard(w.zh, i)}>
{w.zh}
{w.ko}
์‚ฌ์šฉ: {w.usage} {copiedIdx === i && โœ“ ๋ณต์‚ฌ๋จ}
))}
) : (
์‹ค์‹œ๊ฐ„ ํƒ์ง€ ํƒญ์—์„œ ์˜์‹ฌ ์„ ๋ฐ•์„ ํด๋ฆญํ•˜๋ฉด
ํ•ด๋‹น ์—…์ข…๋ณ„ ๋Œ€์‘ ์ ˆ์ฐจ๊ฐ€ ์ž๋™ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
)} )} {/* โ”€โ”€ TAB: ์กฐ์น˜ ๊ธฐ์ค€ โ”€โ”€ */} {tab === 'alert' && ()}
{/* Footer */}
GC-KCG-2026-001 ๊ธฐ๋ฐ˜ | ํ—ˆ๊ฐ€ํ˜„ํ™ฉ 906์ฒ™ | ์ˆ˜์—ญ: Point-in-Polygon | ์ค‘๊ตญ์–ด ๊ฒฝ๊ณ ๋ฌธ ํด๋ฆญ ์‹œ ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ
); } // โ”€โ”€ ์—…์ข…๋ณ„ ๋Œ€์‘ ์ ˆ์ฐจ ์ปดํฌ๋„ŒํŠธ โ”€โ”€ const step: React.CSSProperties = { background: '#1e293b', borderRadius: 4, padding: '6px 10px', margin: '4px 0' }; const stepN: React.CSSProperties = { display: 'inline-block', background: '#3b82f6', color: '#fff', borderRadius: 3, padding: '0 5px', fontSize: 8, fontWeight: 700, marginRight: 4 }; const warn: React.CSSProperties = { background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)', borderRadius: 4, padding: '4px 8px', margin: '4px 0', fontSize: 9, color: '#fca5a5' }; function ProcedureSteps({ type }: { type: string }) { switch (type) { case 'PT': return (<>
๐Ÿ”ด 2์ฒ™์‹ ์ €์ธ๋ง (PT) ๋Œ€์‘ ์ ˆ์ฐจ
โš  ์„ ๋ฏธ(่ˆนๅฐพ) ๋ฐฉํ–ฅ ์ ‘๊ทผ ์ ˆ๋Œ€ ๊ธˆ์ง€ โ€” ์˜ˆ์ธ์‚ญ ์Šคํฌ๋ฃจ ๊ฐ๊น€ ์œ„ํ—˜
1ํƒ์ง€/์‹๋ณ„ โ€” AIS MMSI โ†’ ํ—ˆ๊ฐ€DB ๋Œ€์กฐ. ๋ณธ์„ ยท๋ถ€์†์„  ์Œ ํ™•์ธ, ์ด๊ฒฉ๊ฑฐ๋ฆฌ ์ธก์ •
2์ ‘๊ทผ/๊ฒฝ๊ณ  โ€” ์„ ์ˆ˜ 45ยฐ ์ธก๋ฉด ์ ‘๊ทผ. VHF Ch.16 ๊ฒฝ๊ณ  3ํšŒ. ์ค‘๊ตญ์–ด ๋ฐฉ์†ก ๋ณ‘ํ–‰
3์Šน์„  ๊ฒ€์‚ฌ โ€” โ‘ ํ—ˆ๊ฐ€์ฆ(C21-xxxxx) โ‘ก์กฐ์—…์ผ์ง€(ํ• ๋‹น๋Ÿ‰ 100ํ†ค/์ฒ™) โ‘ข๋ง๋ชฉ ์‹ค์ธก(54mm)
4์œ„๋ฐ˜ ํŒ์ • โ€” ํœด์–ด๊ธฐ(4/16~10/15)โ†’๋‚˜ํฌ | ํ• ๋‹น์ดˆ๊ณผโ†’์••์ˆ˜ | ๋ถ€์†์„  ๋ถ„๋ฆฌโ†’์–‘์„  ๋‚˜ํฌ
5๋‚˜ํฌ/๋ฐฉ๋ฉด โ€” ์œ„๋ฐ˜: ๋ชฉํฌยท์—ฌ์ˆ˜ยท์ œ์ฃผยทํƒœ์•ˆ ์ž…ํ•ญ. ๊ฒฝ๋ฏธ: ๊ฒฝ๊ณ  ํ›„ ๋ฐฉ๋ฉด. ์•Œ๋žŒ ๊ธฐ๋ก ๋“ฑ๋ก
); case 'GN': return (<>
๐ŸŸก ์œ ์ž๋ง (GN) ๋Œ€์‘ โ€” ๋‹คํฌ๋ฒ ์…€ ์ฃผ์˜
โš  ๋ถ€ํ‘œ ์œ„์น˜ ๋จผ์ € ํ™•์ธ โ†’ ๊ทธ๋ฌผ ๋ฒ”์œ„ ์™ธ๊ณฝ์œผ๋กœ ์ ‘๊ทผ (์Šคํฌ๋ฃจ ๊ฐ๊น€ ๋ฐฉ์ง€)
1๋‹คํฌ๋ฒ ์…€ ํƒ์ง€ โ€” ๋ ˆ์ด๋” ํƒ์ƒ‰ + SAR ์š”์ฒญ. ๋ถ€ํ‘œ ๋‹ค์ˆ˜ ๋ฐœ๊ฒฌ โ†’ 1NM ์ด๋‚ด ์ง‘์ค‘ ์ˆ˜์ƒ‰
2๊ทธ๋ฌผ ํ™•์ธ ํ›„ ์ ‘๊ทผ โ€” ๋ถ€ํ‘œ ๋ฐฐ์น˜๋ฐฉํ–ฅ โ†’ ์ž๋ง ์—ฐ์žฅ์„  ์ถ”์ • โ†’ ์ˆ˜์ง 90ยฐ ์™ธ๊ณฝ ์ ‘๊ทผ
3AIS ์žฌ๊ฐ€๋™ โ€” "่ฏทๆ‰“ๅผ€AIS" ๊ฒฝ๊ณ . ์žฌ๊ฐœ ํ™•์ธ ํ›„ MMSI ๊ธฐ๋ก. ๊ฑฐ๋ถ€ ์‹œ ๊ฐ•์ œ ์ž„๊ฒ€
4์Šน์„  ๊ฒ€์‚ฌ โ€” โ‘ ํ—ˆ๊ฐ€์ฆ(C25-xxxxx) โ‘ก์ˆ˜์—ญํ™•์ธ(I๋ฐœ๊ฒฌโ†’์œ„๋ฐ˜) โ‘ข์–ดํš๋Ÿ‰(28ํ†ค/์ฒ™) โ‘ฃ๋ง๋ชฉยท๊ทœ๋ชจ
5์–ด๊ตฌ ํŒ์ • โ€” ํ—ˆ๊ฐ€์™ธ ์ž๋งโ†’์ˆ˜๊ฑฐ/์ ˆ๋‹จ. ๋ง๋ชฉ๋ฏธ๋‹ฌโ†’์ „๋Ÿ‰์••์ˆ˜. GPSยท์‚ฌ์ง„ ๊ธฐ๋ก
); case 'PS': return (<>
๐ŸŸฃ ์œ„๋ง (PS) ์„ ๋‹จ ๋Œ€์‘ โ€” ์„ ๋‹จ ๋ถ„์‚ฐ ์ฃผ์˜
โš  ๋‹จ๋… ์ ‘๊ทผ ๊ธˆ์ง€ โ€” ์กฐ๋ช…์„  ์‹œ์•ผ๊ต๋ž€, ๋ถ„์‚ฐ๋„์ฃผ ์ „์ˆ  ๋Œ€๋น„. ๋Œ€ํ˜• ํ•จ์ • ์ง€์› ํ›„ ๋™์‹œ ์ œ์••
1์„ ๋‹จ ํ™•์ธ/๋ณด๊ณ  โ€” ์›ํ˜•๊ถค์  + ๊ณ ์†โ†’์ €์† ํŒจํ„ด. 3์ฒ™+ ํด๋Ÿฌ์Šคํ„ฐ. ์ฆ‰์‹œ ์ƒ๊ธ‰ ๋ณด๊ณ 
2์ง‘์–ด๋“ฑ ์‹๋ณ„ โ€” ์•ผ๊ฐ„ EO/์œก์•ˆ. ์กฐ๋ช…์„  MMSI ๊ธฐ๋ก. ์ฐจ๋‹จ์€ ์ตœํ›„ ๋‹จ๊ณ„
3์„ ๋‹จ ํฌ์œ„ โ€” ๋ชจ์„ ยท์šด๋ฐ˜์„ ยท์กฐ๋ช…์„  ๋™์‹œ ํฌ์œ„. ์„œ๋ฐฉ(์ค‘๊ตญ์ธก) ํƒˆ์ฃผ ์ฐจ๋‹จ ์šฐ์„ 
4์ผ์ œ ์ž„๊ฒ€ โ€” ๋ชจ์„ : C23-xxxxx, 1,500ํ†ค/์ฒ™. ์šด๋ฐ˜์„ /์กฐ๋ช…์„ : 0ํ†คโ†’์ ์žฌ ์‹œ ๋ถˆ๋ฒ•
5๋‚˜ํฌ/์ฆ๊ฑฐ โ€” ์–ดํš๋ฌผยท๋ƒ‰๋™์„ค๋น„ ์ดฌ์˜. ๅฎๆณขๆตท่ฃ• VHF ๊ต์‹  ํ™•๋ณด. ๋ชฉํฌยท์—ฌ์ˆ˜ํ•ญ ์ธ๊ณ„
); case 'FC': return (<>
๐ŸŸ  ์šด๋ฐ˜์„  (FC) ํ™˜์  ๋Œ€์‘
1ํ™˜์  ์•Œ๋žŒ โ€” FC+์กฐ์—…์„  0.5NM + ์–‘์ชฝ 2kn + 30๋ถ„ โ†’ HIGH. ์ขŒํ‘œ ์ฆ‰์‹œ ์ด๋™
2์ฆ๊ฑฐ ์ดฌ์˜ โ€” ์ ‘ํ˜„/๊ณ ๋ฌด๋ณดํŠธ ํ™•์ธ. ๋“œ๋ก  ํ•ญ๊ณต์ดฌ์˜. MMSIยท์„ ๋ช…ยท์ ‘ํ˜„ ํ”์  ๊ธฐ๋ก
3์–‘์„  ์ž„๊ฒ€ โ€” ์šด๋ฐ˜์„ : ํ™”๋ฌผยท์ถœ๋ฐœ์ง€ยท๋„์ฐฉ์ง€. ์กฐ์—…์„ : ํ—ˆ๊ฐ€๋Ÿ‰ ๋Œ€๋น„ ์‹ค์–ดํš๋Ÿ‰
4์ฆ๊ฑฐ/์กฐ์น˜ โ€” ์‚ฌ์ง„ยท์ค‘๋Ÿ‰ ํ™•๋ณด. ํ•„์š”์‹œ ์ „๋Ÿ‰ ์••์ˆ˜. ๋„์ฃผ์‹œ ๊ฒฝ๊ณ ์‚ฌ๊ฒฉ. ์ตœ๊ทผ์ ‘ ํ•ญ๊ตฌ ์ž…ํ•ญ
); case 'GEAR': return (<>
๐Ÿชค ๋ถˆ๋ฒ• ์–ด๊ตฌ ์ˆ˜๊ฑฐ ์ ˆ์ฐจ
โš  ๋ฐฉ์น˜ ์ž๋ง ์Šคํฌ๋ฃจ ๊ฐ๊น€ ์ฃผ์˜ โ€” ์—”์ง„ ์ •์ง€/์ €์† ์ƒํƒœ์—์„œ ์ˆ˜๋™ ํšŒ์ˆ˜. ์•ผ๊ฐ„ ์ˆ˜๊ฑฐ ์›์น™์  ์—ฐ๊ธฐ
1๋ฐœ๊ฒฌ/๊ธฐ๋ก โ€” GPS(WGS84), ์ข…๋ฅ˜ ์ถ”์ •, ์‚ฌ์ง„, ์†Œ์œ ์ž๋ฒˆํ˜ธ, ๊ทœ๋ชจ(๊ธธ์ดยทํญยท๊ทธ๋ฌผ์ฝ”)
2์ค‘๊ตญ์–ด๊ตฌ ํŒ๋‹จ โ€” ์ค‘๊ตญ์–ด ๋ถ€ํ‘œ, ๊ด‘ํญยท์žฅํ˜• ๊ตฌ์กฐ. ์ธ๊ทผ ์ค‘๊ตญ์–ด์„  ํ™•์ธ. ๋ถˆ๊ฐ€โ†’ํ•ญ๊ตฌ ๊ฐ์‹
3์ˆ˜๊ฑฐ ์‹คํ–‰ โ€” RIB/ํฌ๋ ˆ์ธ. ์–ดํš๋ฌผโ†’์ „๋Ÿ‰ ์••์ˆ˜. ์ ˆ๋‹จ ์‹œ ์œ„์น˜ยท์ž”์กด ๊ธฐ๋ก
4์ˆ˜๊ฑฐ ๋ณด๊ณ  โ€” ๊ฐ์‹œ ์‹œ์Šคํ…œ ๋“ฑ๋ก. ํ•ญ๊ตฌ ๊ฐ์‹ยท์ฆ๊ฑฐ ๋ณด์กด. ๋ฐ˜๋ณต ๋ฐœ๊ฒฌโ†’์ง‘์ค‘ ๊ฐ์‹œ ์ง€์ •
); default: return (
์„ ๋ฐ• ์œ ํ˜•์„ ์‹๋ณ„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์ž„๊ฒ€ ์ ˆ์ฐจ๋ฅผ ์ ์šฉํ•˜์„ธ์š”.
); } } function AlertTable() { const rows = [ { type: '๋ฏธ๋“ฑ๋ก ์„ ๋ฐ•', criteria: 'MMSI ํ—ˆ๊ฐ€DB ๋ฏธ๋“ฑ๋ก', action: '์ฆ‰์‹œ ์ •์„ ยท๋‚˜ํฌ', level: 'CRITICAL', note: 'ํ—ˆ๊ฐ€์ฆ ๋ถˆ์†Œ์ง€ ์ถ”๊ฐ€ ํ™•์ธ' }, { type: 'ํœด์–ด๊ธฐ ์กฐ์—…', criteria: 'C21ยทC22: 4/16~10/15\nC25: 6/2~8/31', action: '์ฆ‰์‹œ ๋‚˜ํฌ', level: 'CRITICAL', note: '๋‚ ์งœ ์ž๋™ ํŒ๋ณ„' }, { type: 'ํ—ˆ๊ฐ€ ์ˆ˜์—ญ ์ดํƒˆ', criteria: '๋น„ํ—ˆ๊ฐ€ ์ˆ˜์—ญ ์ง„์ž…', action: '๊ฒฝ๊ณ  ํ›„ ๋‚˜ํฌ', level: 'HIGH', note: 'PT: IยทIV์ดํƒˆ GN: I์ดํƒˆ' }, { type: 'PT ๋ถ€์†์„  ๋ถ„๋ฆฌ', criteria: '๋ณธ์„  ์ด๊ฒฉ 3NM+', action: '์–‘์„  ๋™์‹œ ๋‚˜ํฌ', level: 'HIGHโ†’CRIT', note: '311์Œ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง' }, { type: 'ํ™˜์  ํ˜„์žฅ ํฌ์ฐฉ', criteria: 'FC+์กฐ์—…์„  0.5NM+2kn+30๋ถ„', action: '์ดฌ์˜ ํ›„ ์–‘์„  ๋‚˜ํฌ', level: 'HIGH', note: '์ฆ๊ฑฐ ์ดฌ์˜ ์ตœ์šฐ์„ ' }, { type: '๋ถˆ๋ฒ• ์–ด๊ตฌ ๋ฐœ๊ฒฌ', criteria: 'ํ‘œ์ง€ ์—†์Œ/๋ฏธํ—ˆ๊ฐ€', action: '์ฆ‰์‹œ ์ˆ˜๊ฑฐยท๊ธฐ๋ก', level: '์ž์ฒดํŒ๋‹จ', note: 'GPS ๋“ฑ๋ก, ๋ฐ˜๋ณต ์š”์ฃผ์˜' }, { type: 'ํ• ๋‹น๋Ÿ‰ ์ดˆ๊ณผ', criteria: '80~100%+ ์ดˆ๊ณผ', action: '๊ณ„๋Ÿ‰ยท์ดˆ๊ณผ ์‹œ ์••์ˆ˜', level: 'CRITICAL', note: 'GN 28ํ†ค ํ˜„์žฅ ๊ณ„๋Ÿ‰' }, { type: '๋‹คํฌ๋ฒ ์…€', criteria: 'AIS ๊ณต๋ฐฑ 6์‹œ๊ฐ„+', action: '์ ‘๊ทผยท์ž„๊ฒ€', level: 'HIGH', note: 'SAR ๊ต์ฐจ ํ™•์ธ' }, ]; const lc = (l: string) => l.includes('CRIT') ? '#ef4444' : l === 'HIGH' ? '#f59e0b' : '#64748b'; return (
๐Ÿšจ ๋‹จ์† ์ƒํ™ฉ๋ณ„ ์กฐ์น˜ ๊ธฐ์ค€
{rows.map((r, i) => ( ))}
์œ„๋ฐ˜ ์œ ํ˜•ํŒ์ • ๊ธฐ์ค€์ฆ‰์‹œ ์กฐ์น˜์•Œ๋žŒ๋น„๊ณ 
{r.type}{r.criteria}{r.action} {r.level}{r.note}
๐Ÿ“… ๊ฐ์‹œ ๊ฐ•ํ™” ์‹œ๊ธฐ
์‹œ๊ธฐ์ƒํ™ฉ๋Œ€์‘
7~8์›”PS 16์ฒ™๋งŒ ํ—ˆ๊ฐ€C21ยทC22ยทC25 ์ „์› ๋น„ํ—ˆ๊ฐ€
5์›”GN๋งŒ ํ—ˆ๊ฐ€์ €์ธ๋ง(C21ยทC22) ์ฆ‰์‹œ ์œ„๋ฐ˜
4์›”ยท10์›”๊ธฐ๊ฐ„ ๊ฒฝ๊ณ„4/16, 10/16 ์ง‘์ค‘ ๋ชจ๋‹ˆํ„ฐ๋ง
1~3์›”์ „ ์—…์ข… ๊ฐ€๋Šฅ์ˆ˜์—ญ์ดํƒˆยทํ• ๋‹น์ดˆ๊ณผ ์ค‘์‹ฌ
); } const th: React.CSSProperties = { border: '1px solid #1e293b', padding: '3px 6px', textAlign: 'left', color: '#e2e8f0', fontSize: 8, fontWeight: 700 }; const td: React.CSSProperties = { border: '1px solid #1e293b', padding: '2px 6px', fontSize: 9 };