refactor: Phase 4-2 — shipClassification 유틸 추출 (ShipLayer 862줄→769줄)

- utils/shipClassification.ts: MT 색상/타입매핑/국기/사이즈 추출 (87줄)
- ShipLayer: 로컬 상수/함수 93줄 → import 1줄로 교체
This commit is contained in:
htlee 2026-03-23 10:56:47 +09:00
부모 c6c3b5ffb9
커밋 2b009ca81a
2개의 변경된 파일86개의 추가작업 그리고 95개의 파일을 삭제

파일 보기

@ -1,8 +1,9 @@
import { memo, useMemo, useState, useEffect, useRef, useCallback } from 'react';
import { Marker, Popup, Source, Layer, useMap } from 'react-map-gl/maplibre';
import { useTranslation } from 'react-i18next';
import type { Ship, ShipCategory, VesselAnalysisDto } from '../../types';
import type { Ship, VesselAnalysisDto } from '../../types';
import maplibregl from 'maplibre-gl';
import { MT_TYPE_HEX, getMTType, NAVY_COLORS, FLAG_EMOJI, SIZE_MAP, isMilitary, getShipColor } from '../../utils/shipClassification';
interface Props {
ships: Ship[];
@ -14,100 +15,6 @@ interface Props {
analysisMap?: Map<string, VesselAnalysisDto>;
}
// ── MarineTraffic-style vessel type colors (CSS variable references) ──
const MT_TYPE_COLORS: Record<string, string> = {
cargo: 'var(--kcg-ship-cargo)',
tanker: 'var(--kcg-ship-tanker)',
passenger: 'var(--kcg-ship-passenger)',
fishing: 'var(--kcg-ship-fishing)',
fishing_gear: '#f97316',
pleasure: 'var(--kcg-ship-pleasure)',
military: 'var(--kcg-ship-military)',
tug_special: 'var(--kcg-ship-tug)',
other: 'var(--kcg-ship-other)',
unknown: 'var(--kcg-ship-unknown)',
};
// Resolved hex colors for MapLibre paint (which cannot use CSS vars)
const MT_TYPE_HEX: Record<string, string> = {
cargo: '#f0a830',
tanker: '#e74c3c',
passenger: '#4caf50',
fishing: '#42a5f5',
fishing_gear: '#f97316',
pleasure: '#e91e8c',
military: '#d32f2f',
tug_special: '#2e7d32',
other: '#5c6bc0',
unknown: '#9e9e9e',
};
// Map our internal ShipCategory + typecode → MT visual type
function getMTType(ship: Ship): string {
const tc = (ship.typecode || '').toUpperCase();
const cat = ship.category;
// Military first
if (cat === 'carrier' || cat === 'destroyer' || cat === 'warship' || cat === 'submarine' || cat === 'patrol') return 'military';
if (tc === 'DDG' || tc === 'DDH' || tc === 'CVN' || tc === 'FFG' || tc === 'LCS' || tc === 'MCM' || tc === 'PC' || tc === 'LPH') return 'military';
// Tanker
if (cat === 'tanker') return 'tanker';
if (tc === 'VLCC' || tc === 'LNG' || tc === 'LPG') return 'tanker';
if (tc.startsWith('A1')) return 'tanker';
// Cargo
if (cat === 'cargo') return 'cargo';
if (tc === 'CONT' || tc === 'BULK') return 'cargo';
if (tc.startsWith('A2') || tc.startsWith('A3')) return 'cargo';
// Passenger
if (tc === 'PASS' || tc.startsWith('B')) return 'passenger';
// Fishing
if (tc.startsWith('C')) return 'fishing';
// Tug / Special
if (tc.startsWith('D') || tc.startsWith('E')) return 'tug_special';
// Pleasure
if (tc === 'SAIL' || tc === 'YACHT') return 'pleasure';
if (cat === 'civilian') return 'other';
return 'unknown';
}
// Legacy navy flag colors (for popup header accent only)
const NAVY_COLORS: Record<string, string> = {
US: '#1e90ff', UK: '#e63946', FR: '#ffd60a', KR: '#00e5ff',
IR: '#2ecc40', JP: '#ff6b6b', AU: '#f4a261', DE: '#b5b5b5', IN: '#ff9f43',
};
const FLAG_EMOJI: Record<string, string> = {
US: '\u{1F1FA}\u{1F1F8}', UK: '\u{1F1EC}\u{1F1E7}', FR: '\u{1F1EB}\u{1F1F7}',
KR: '\u{1F1F0}\u{1F1F7}', IR: '\u{1F1EE}\u{1F1F7}', JP: '\u{1F1EF}\u{1F1F5}',
AU: '\u{1F1E6}\u{1F1FA}', DE: '\u{1F1E9}\u{1F1EA}', IN: '\u{1F1EE}\u{1F1F3}',
CN: '\u{1F1E8}\u{1F1F3}', PA: '\u{1F1F5}\u{1F1E6}', LR: '\u{1F1F1}\u{1F1F7}',
MH: '\u{1F1F2}\u{1F1ED}', HK: '\u{1F1ED}\u{1F1F0}', SG: '\u{1F1F8}\u{1F1EC}',
BZ: '\u{1F1E7}\u{1F1FF}', OM: '\u{1F1F4}\u{1F1F2}', AE: '\u{1F1E6}\u{1F1EA}',
SA: '\u{1F1F8}\u{1F1E6}', BH: '\u{1F1E7}\u{1F1ED}', QA: '\u{1F1F6}\u{1F1E6}',
};
// icon-size multiplier (symbol layer, base=64px)
const SIZE_MAP: Record<ShipCategory, number> = {
carrier: 0.32, destroyer: 0.22, warship: 0.22, submarine: 0.18, patrol: 0.16,
tanker: 0.16, cargo: 0.16, fishing: 0.14, civilian: 0.14, unknown: 0.12,
};
const MIL_CATEGORIES: ShipCategory[] = ['carrier', 'destroyer', 'warship', 'submarine', 'patrol'];
function isMilitary(category: ShipCategory): boolean {
return MIL_CATEGORIES.includes(category);
}
function getShipColor(ship: Ship): string {
return MT_TYPE_COLORS[getMTType(ship)] || MT_TYPE_COLORS.unknown;
}
function getShipHex(ship: Ship): string {
return MT_TYPE_HEX[getMTType(ship)] || MT_TYPE_HEX.unknown;

파일 보기

@ -0,0 +1,84 @@
import type { Ship, ShipCategory } from '../types';
// ── MarineTraffic-style vessel type colors (CSS variable references) ──
export const MT_TYPE_COLORS: Record<string, string> = {
cargo: 'var(--kcg-ship-cargo)',
tanker: 'var(--kcg-ship-tanker)',
passenger: 'var(--kcg-ship-passenger)',
fishing: 'var(--kcg-ship-fishing)',
fishing_gear: '#f97316',
pleasure: 'var(--kcg-ship-pleasure)',
military: 'var(--kcg-ship-military)',
tug_special: 'var(--kcg-ship-tug)',
other: 'var(--kcg-ship-other)',
unknown: 'var(--kcg-ship-unknown)',
};
// Resolved hex colors for MapLibre paint (which cannot use CSS vars)
export const MT_TYPE_HEX: Record<string, string> = {
cargo: '#f0a830',
tanker: '#e74c3c',
passenger: '#4caf50',
fishing: '#42a5f5',
fishing_gear: '#f97316',
pleasure: '#e91e8c',
military: '#d32f2f',
tug_special: '#2e7d32',
other: '#5c6bc0',
unknown: '#9e9e9e',
};
export function getMTType(ship: Ship): string {
const tc = (ship.typecode || '').toUpperCase();
const cat = ship.category;
if (cat === 'carrier' || cat === 'destroyer' || cat === 'warship' || cat === 'submarine' || cat === 'patrol') return 'military';
if (tc === 'DDG' || tc === 'DDH' || tc === 'CVN' || tc === 'FFG' || tc === 'LCS' || tc === 'MCM' || tc === 'PC' || tc === 'LPH') return 'military';
if (cat === 'tanker') return 'tanker';
if (tc === 'VLCC' || tc === 'LNG' || tc === 'LPG') return 'tanker';
if (tc.startsWith('A1')) return 'tanker';
if (cat === 'cargo') return 'cargo';
if (tc === 'CONT' || tc === 'BULK') return 'cargo';
if (tc.startsWith('A2') || tc.startsWith('A3')) return 'cargo';
if (tc === 'PASS' || tc.startsWith('B')) return 'passenger';
if (tc.startsWith('C')) return 'fishing';
if (tc.startsWith('D') || tc.startsWith('E')) return 'tug_special';
if (tc === 'SAIL' || tc === 'YACHT') return 'pleasure';
if (cat === 'civilian') return 'other';
return 'unknown';
}
// Legacy navy flag colors (for popup header accent only)
export const NAVY_COLORS: Record<string, string> = {
US: '#1e90ff', UK: '#e63946', FR: '#ffd60a', KR: '#00e5ff',
IR: '#2ecc40', JP: '#ff6b6b', AU: '#f4a261', DE: '#b5b5b5', IN: '#ff9f43',
};
export const FLAG_EMOJI: Record<string, string> = {
US: '\u{1F1FA}\u{1F1F8}', UK: '\u{1F1EC}\u{1F1E7}', FR: '\u{1F1EB}\u{1F1F7}',
KR: '\u{1F1F0}\u{1F1F7}', IR: '\u{1F1EE}\u{1F1F7}', JP: '\u{1F1EF}\u{1F1F5}',
AU: '\u{1F1E6}\u{1F1FA}', DE: '\u{1F1E9}\u{1F1EA}', IN: '\u{1F1EE}\u{1F1F3}',
CN: '\u{1F1E8}\u{1F1F3}', PA: '\u{1F1F5}\u{1F1E6}', LR: '\u{1F1F1}\u{1F1F7}',
MH: '\u{1F1F2}\u{1F1ED}', HK: '\u{1F1ED}\u{1F1F0}', SG: '\u{1F1F8}\u{1F1EC}',
BZ: '\u{1F1E7}\u{1F1FF}', OM: '\u{1F1F4}\u{1F1F2}', AE: '\u{1F1E6}\u{1F1EA}',
SA: '\u{1F1F8}\u{1F1E6}', BH: '\u{1F1E7}\u{1F1ED}', QA: '\u{1F1F6}\u{1F1E6}',
};
export const SIZE_MAP: Record<ShipCategory, number> = {
carrier: 0.32, destroyer: 0.22, warship: 0.22, submarine: 0.18, patrol: 0.16,
tanker: 0.16, cargo: 0.16, fishing: 0.14, civilian: 0.14, unknown: 0.12,
};
export const MIL_CATEGORIES: ShipCategory[] = ['carrier', 'destroyer', 'warship', 'submarine', 'patrol'];
export function isMilitary(category: ShipCategory): boolean {
return MIL_CATEGORIES.includes(category);
}
export function getShipColor(ship: Ship): string {
return MT_TYPE_COLORS[getMTType(ship)] || MT_TYPE_COLORS.unknown;
}