fix: hotfix 동기화 — history/detail candidate_count 안전 처리 #225

병합
htlee hotfix/sync-candidate-count 에서 develop 로 69 commits 를 머지했습니다 2026-04-04 11:05:43 +09:00
2개의 변경된 파일35개의 추가작업 그리고 52개의 파일을 삭제
Showing only changes of commit 9c091d1052 - Show all commits

파일 보기

@ -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<GeoEvent['type'], string> = {
};
// 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<string, string> = {
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<BreakingNews['category'], string> = {
@ -443,7 +428,7 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
</div>
<div className="iran-mil-list">
{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<string, Ship[]> = {};
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<string, Ship[]> = {};
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);
}

파일 보기

@ -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';
}