From 387c4e42c8e19192c419614a51e008e4b3c291f5 Mon Sep 17 00:00:00 2001 From: htlee Date: Wed, 18 Mar 2026 11:58:32 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=84=A0=EB=B0=95=20=EB=B6=84=EB=A5=98?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20+=20=EB=B0=B0?= =?UTF-8?q?=EC=A7=80=20=EC=83=89=EC=83=81=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getMarineTrafficCategory: VesselType 문자열 매칭을 STAT5CODE 접두사보다 우선 적용 - STAT5CODE 매칭: 2번째 문자가 숫자인 경우만 적용 ("Cargo" → fishing 오분류 방지) - EventLog 로컬 getShipMTCategory 제거 → 공통 getMarineTrafficCategory 통합 - EventLog 배지 색상: 하드코딩 hex → CSS 변수 (LayerPanel/ShipLayer와 동일) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/common/EventLog.tsx | 45 +++++++-------------- frontend/src/utils/marineTraffic.ts | 42 +++++++++---------- 2 files changed, 35 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/common/EventLog.tsx b/frontend/src/components/common/EventLog.tsx index 50fe4d6..0481666 100644 --- a/frontend/src/components/common/EventLog.tsx +++ b/frontend/src/components/common/EventLog.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { GeoEvent, Ship } from '../../types'; import type { OsintItem } from '../../services/osint'; +import { getMarineTrafficCategory } from '../../utils/marineTraffic'; type DashboardTab = 'iran' | 'korea'; @@ -268,36 +269,20 @@ const TYPE_COLORS: Record = { }; // MarineTraffic-style ship type classification -function getShipMTCategory(typecode?: string, category?: string): string { - if (!typecode) { - if (category === 'tanker') return 'tanker'; - if (category === 'cargo') return 'cargo'; - if (category === 'destroyer' || category === 'warship' || category === 'carrier' || category === 'patrol') return 'military'; - return 'unspecified'; - } - const code = typecode.toUpperCase(); - if (code === 'VLCC' || code === 'LNG' || code === 'LPG') return 'tanker'; - if (code === 'CONT' || code === 'BULK') return 'cargo'; - if (code === 'DDH' || code === 'DDG' || code === 'CVN' || code === 'FFG' || code === 'LCS' || code === 'MCM' || code === 'PC') return 'military'; - if (code.startsWith('A1')) return 'tanker'; - if (code.startsWith('A2') || code.startsWith('A3')) return 'cargo'; - if (code.startsWith('B')) return 'passenger'; - if (code.startsWith('C')) return 'fishing'; - if (code.startsWith('D') || code.startsWith('E')) return 'tug_special'; - return 'unspecified'; -} +// getMarineTrafficCategory → getMarineTrafficCategory (utils/marineTraffic.ts)로 통합 // MarineTraffic-style category colors (labels come from i18n) const MT_CATEGORY_COLORS: Record = { - cargo: '#8bc34a', - tanker: '#e91e63', - passenger: '#2196f3', - high_speed: '#ff9800', - tug_special: '#00bcd4', - fishing: '#ff5722', - pleasure: '#9c27b0', - military: '#607d8b', - unspecified: '#9e9e9e', + cargo: 'var(--kcg-ship-cargo)', + tanker: 'var(--kcg-ship-tanker)', + passenger: 'var(--kcg-ship-passenger)', + high_speed: 'var(--kcg-ship-highspeed)', + tug_special: 'var(--kcg-ship-tug)', + fishing: 'var(--kcg-ship-fishing)', + pleasure: 'var(--kcg-ship-pleasure)', + military: 'var(--kcg-ship-military)', + other: 'var(--kcg-ship-other)', + unspecified: 'var(--kcg-ship-unknown)', }; const NEWS_CATEGORY_ICONS: Record = { @@ -443,7 +428,7 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
{koreanShips.slice(0, 30).map(s => { - const cat = getShipMTCategory(s.typecode, s.category); + const cat = getMarineTrafficCategory(s.typecode, s.category); const mtColor = MT_CATEGORY_COLORS[cat] || '#888'; const mtLabel = t(`ships:mtType.${cat}`, { defaultValue: t('ships:mtType.unspecified') }); return ( @@ -612,7 +597,7 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount, {koreanShips.length > 0 && (() => { const groups: Record = {}; for (const s of koreanShips) { - const cat = getShipMTCategory(s.typecode, s.category); + const cat = getMarineTrafficCategory(s.typecode, s.category); if (!groups[cat]) groups[cat] = []; groups[cat].push(s); } @@ -659,7 +644,7 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount, {chineseShips.length > 0 && (() => { const groups: Record = {}; for (const s of chineseShips) { - const cat = getShipMTCategory(s.typecode, s.category); + const cat = getMarineTrafficCategory(s.typecode, s.category); if (!groups[cat]) groups[cat] = []; groups[cat].push(s); } diff --git a/frontend/src/utils/marineTraffic.ts b/frontend/src/utils/marineTraffic.ts index ece0199..e10b374 100644 --- a/frontend/src/utils/marineTraffic.ts +++ b/frontend/src/utils/marineTraffic.ts @@ -1,5 +1,5 @@ // MarineTraffic-style ship classification -// Maps S&P STAT5CODE prefixes and our custom typecodes to MT categories +// Maps S&P STAT5CODE prefixes, VesselType strings, and custom typecodes to MT categories export function getMarineTrafficCategory(typecode?: string, category?: string): string { if (!typecode) { // Fallback to our internal category @@ -10,30 +10,14 @@ export function getMarineTrafficCategory(typecode?: string, category?: string): } const code = typecode.toUpperCase(); - // Our custom typecodes + // Our custom typecodes (exact match) if (code === 'VLCC' || code === 'LNG' || code === 'LPG') return 'tanker'; if (code === 'CONT' || code === 'BULK') return 'cargo'; - if (code === 'DDH' || code === 'DDG' || code === 'CVN' || code === 'FFG' || code === 'LCS' || code === 'MCM' || code === 'PC') return 'military'; + if (code === 'DDH' || code === 'DDG' || code === 'CVN' || code === 'FFG' || code === 'LCS' || code === 'MCM' || code === 'PC' || code === 'LPH') return 'military'; + if (code === 'PASS') return 'passenger'; - // S&P STAT5CODE (IHS StatCode5) — first 2 chars determine main category - // A1x = Tankers (crude, products, chemical, LPG, LNG) - if (code.startsWith('A1')) return 'tanker'; - // A2x = Bulk carriers - if (code.startsWith('A2')) return 'cargo'; - // A3x = General cargo / Container / Reefer / Ro-Ro - if (code.startsWith('A3')) return 'cargo'; - // B1x / B2x = Passenger / Cruise / Ferry - if (code.startsWith('B')) return 'passenger'; - // C1x = Fishing - if (code.startsWith('C')) return 'fishing'; - // D1x = Offshore (tugs, supply, etc.) - if (code.startsWith('D')) return 'tug_special'; - // E = Other activities (research, cable layers, dredgers) - if (code.startsWith('E')) return 'tug_special'; - // X = Non-propelled (barges) - if (code.startsWith('X')) return 'unspecified'; - - // S&P VesselType strings + // VesselType strings (e.g. "Cargo", "Tanker", "Passenger") — match BEFORE STAT5CODE + // to avoid "Cargo" matching STAT5CODE prefix "C" → fishing const lower = code.toLowerCase(); if (lower.includes('tanker')) return 'tanker'; if (lower.includes('cargo') || lower.includes('container') || lower.includes('bulk')) return 'cargo'; @@ -42,6 +26,20 @@ export function getMarineTrafficCategory(typecode?: string, category?: string): if (lower.includes('tug') || lower.includes('supply') || lower.includes('offshore')) return 'tug_special'; if (lower.includes('high speed')) return 'high_speed'; if (lower.includes('pleasure') || lower.includes('yacht') || lower.includes('sailing')) return 'pleasure'; + if (lower.includes('pilot') || lower.includes('search') || lower.includes('law enforcement')) return 'tug_special'; + if (lower.includes('naval') || lower.includes('military')) return 'military'; + + // S&P STAT5CODE (IHS StatCode5) — 2nd char is digit (e.g. "A1xxxx", "B2xxxx") + if (code.length >= 2 && /\d/.test(code[1])) { + if (code.startsWith('A1')) return 'tanker'; + if (code.startsWith('A2')) return 'cargo'; + if (code.startsWith('A3')) return 'cargo'; + if (code.startsWith('B')) return 'passenger'; + if (code.startsWith('C')) return 'fishing'; + if (code.startsWith('D')) return 'tug_special'; + if (code.startsWith('E')) return 'tug_special'; + if (code.startsWith('X')) return 'unspecified'; + } return 'unspecified'; }