import { useMemo } from 'react'; import type { GeoEvent, Ship } from '../types'; import type { OsintItem } from '../services/osint'; type DashboardTab = 'iran' | 'korea'; interface Props { events: GeoEvent[]; currentTime: number; totalShipCount: number; koreanShips: Ship[]; koreanShipsByCategory: Record; chineseShips?: Ship[]; osintFeed?: OsintItem[]; isLive?: boolean; dashboardTab?: DashboardTab; onTabChange?: (tab: DashboardTab) => void; ships?: Ship[]; } // ═══ 속보 / 트럼프 발언 + 유가·에너지 뉴스 ═══ interface BreakingNews { id: string; timestamp: number; category: 'trump' | 'oil' | 'diplomacy' | 'economy'; headline: string; detail?: string; } const T0_NEWS = new Date('2026-03-01T12:01:00Z').getTime(); const HOUR_MS = 3600_000; const DAY_MS = 24 * HOUR_MS; const _MIN_MS = 60_000; const BREAKING_NEWS: BreakingNews[] = [ // DAY 1 { id: 'bn1', timestamp: T0_NEWS - 11 * HOUR_MS, category: 'trump', headline: '트럼프: "이란 정권 제거 작전 개시"', detail: '백악관 긴급 브리핑. "미국은 이란의 핵위협을 더 이상 용납하지 않겠다."', }, { id: 'bn2', timestamp: T0_NEWS - 8 * HOUR_MS, category: 'oil', headline: 'WTI 원유 $140 돌파 — 호르무즈 해협 봉쇄 우려', detail: '브렌트유 $145, 아시아 선물시장 급등. 호르무즈 해협 통과 원유 일일 2,100만 배럴.', }, { id: 'bn3', timestamp: T0_NEWS - 3 * HOUR_MS, category: 'oil', headline: '호르무즈 해협 봉쇄 선언 — 유가 40% 급등', detail: 'IRGC 해군 해협 봉쇄. WTI $165, 브렌트 $170. 글로벌 공급망 마비 우려.', }, { id: 'bn4', timestamp: T0_NEWS + 2 * HOUR_MS, category: 'trump', headline: '트럼프: "이란은 매우 큰 대가를 치를 것"', detail: '알우데이드 미군 3명 전사 확인 후 성명. "미국 군인에 대한 공격은 10배로 갚겠다."', }, { id: 'bn5', timestamp: T0_NEWS + 4 * HOUR_MS, category: 'oil', headline: 'WTI $180 돌파 — 사상 최고가 경신', detail: '이란 보복 공격으로 걸프 원유 수출 완전 중단. S&P 500 -7% 서킷브레이커.', }, { id: 'bn6', timestamp: T0_NEWS + 6 * HOUR_MS, category: 'economy', headline: '미 국방부: "2,000명 추가 병력 중동 긴급 배치"', detail: '제82공수사단 신속대응여단 카타르행. 추가 패트리어트 포대 배치.', }, { id: 'bn7', timestamp: T0_NEWS + 10 * HOUR_MS, category: 'economy', headline: '한국 비상 에너지 대책 — 전략비축유 방출 검토', detail: '산업부, 유류비 급등 대응 비상대책 발표. 걸프 한국 교민 대피 명령.', }, // DAY 2 { id: 'bn8', timestamp: T0_NEWS + 1 * DAY_MS, category: 'oil', headline: 'WTI $185 — 호르무즈 기뢰 추가 배치', detail: 'IRGC 해협 기뢰 추가 설치. 보험료 1,000% 급등, 유조선 통행 사실상 중단.', }, { id: 'bn9', timestamp: T0_NEWS + 1 * DAY_MS + 6 * HOUR_MS, category: 'trump', headline: '트럼프: "이란 석유시설 전면 타격 승인"', detail: '"이란이 해협을 닫으면 우리는 이란의 모든 석유시설을 파괴할 것."', }, { id: 'bn10', timestamp: T0_NEWS + 1 * DAY_MS + 10 * HOUR_MS, category: 'economy', headline: 'IEA 긴급 비축유 방출 — 6,000만 배럴', detail: 'IEA 회원국 전략비축유 협조 방출 합의. 미국 3,000만 배럴 선도 방출.', }, // DAY 3 { id: 'bn11', timestamp: T0_NEWS + 2 * DAY_MS, category: 'oil', headline: '호르무즈 유조선 기뢰 접촉 — 원유 유출', detail: '그리스 VLCC "아테나 글로리" 기뢰 접촉. 200만 배럴 유출 위기. WTI $190.', }, { id: 'bn12', timestamp: T0_NEWS + 2 * DAY_MS + 6 * HOUR_MS, category: 'trump', headline: '트럼프: "해군에 호르무즈 기뢰 제거 명령"', detail: '"미 해군 소해정 부대 투입. 해협 72시간 내 재개방 목표."', }, { id: 'bn13', timestamp: T0_NEWS + 2 * DAY_MS + 10 * HOUR_MS, category: 'economy', headline: '한국 선박 12척 오만만 긴급 대피', detail: '청해부대 호위 하 호르무즈 인근 한국 선박 대피. 해운업계 손실 하루 2,000억원.', }, // DAY 4 { id: 'bn14', timestamp: T0_NEWS + 3 * DAY_MS, category: 'oil', headline: 'WTI $195 — 헤즈볼라 하이파 정유시설 타격', detail: '이스라엘 하이파 정유시설 화재. 중동 전면전 우려 극대화.', }, { id: 'bn15', timestamp: T0_NEWS + 3 * DAY_MS + 8 * HOUR_MS, category: 'trump', headline: '트럼프: "디모나 공격은 레드라인 — 핵옵션 배제 안 해"', detail: '디모나 핵시설 인근 피격 후 강경 성명. 세계 핵전쟁 공포 확산.', }, // DAY 5 { id: 'bn16', timestamp: T0_NEWS + 4 * DAY_MS, category: 'economy', headline: '이란 사이버공격 — 이스라엘 전력망 마비', detail: '이란 APT, 이스라엘 전력망 해킹. 텔아비브 일대 12시간 정전.', }, { id: 'bn17', timestamp: T0_NEWS + 4 * DAY_MS + 4 * HOUR_MS, category: 'oil', headline: 'WTI $200 돌파 — 사우디 라스타누라 드론 피격', detail: '사우디 최대 석유수출터미널 피격. 글로벌 석유 공급 일 500만 배럴 감소.', }, { id: 'bn18', timestamp: T0_NEWS + 4 * DAY_MS + 8 * HOUR_MS, category: 'economy', headline: '한국 비상경제대책 발동 — 전략비축유 방출 개시', detail: '유류비 급등 대응 비상대책. 비축유 500만 배럴 방출. 주유소 가격 L당 2,800원 돌파.', }, // DAY 6 { id: 'bn19', timestamp: T0_NEWS + 5 * DAY_MS, category: 'oil', headline: '이란 항모전단 공격 — WTI $210', detail: 'IRGC 대함미사일 발사, 이지스 전탄 요격. 해상보험료 역사적 최고치.', }, { id: 'bn20', timestamp: T0_NEWS + 5 * DAY_MS + 4 * HOUR_MS, category: 'trump', headline: '트럼프: "이란 해군 완전히 소탕하겠다"', detail: '"페르시아만에서 이란 함정이 하나도 남지 않을 때까지 작전 지속."', }, // DAY 7 { id: 'bn21', timestamp: T0_NEWS + 6 * DAY_MS + 4 * HOUR_MS, category: 'trump', headline: '트럼프: "48시간 최후통첩 — 정권교체 불사"', detail: '"이란이 48시간 내 미사일 발사를 중단하지 않으면 정권 교체 작전을 개시하겠다."', }, { id: 'bn22', timestamp: T0_NEWS + 6 * DAY_MS + 8 * HOUR_MS, category: 'oil', headline: 'WTI $195 소폭 하락 — 휴전 기대감', detail: '트럼프 최후통첩 후 휴전 기대감. 그러나 IRGC 거부 성명으로 다시 반등.', }, // DAY 8 { id: 'bn23', timestamp: T0_NEWS + 7 * DAY_MS, category: 'diplomacy', headline: 'ICRC: "중동 인도적 위기 — 이란 의약품 고갈"', detail: '이란 내 의약품·식수 부족 심각. 이스라엘·바레인 민간인 사상자 수천 명.', }, { id: 'bn24', timestamp: T0_NEWS + 7 * DAY_MS + 6 * HOUR_MS, category: 'trump', headline: '트럼프: "이란 정보부 본부도 파괴 — 끝까지 간다"', detail: 'B-2 이란 정보부(VAJA) 타격 후 성명. "이란에 남은 건 항복뿐."', }, { id: 'bn25', timestamp: T0_NEWS + 7 * DAY_MS + 10 * HOUR_MS, category: 'economy', headline: '한국 교민 350명 두바이 경유 긴급 귀국', detail: '군 수송기 투입. 청해부대 한국 선박 호위 지속. 해운업계 일 3,000억원 손실.', }, // DAY 9 { id: 'bn26', timestamp: T0_NEWS + 8 * DAY_MS, category: 'diplomacy', headline: '러시아, 이란에 휴전 수용 비공식 권고', detail: '푸틴, 추가 무기 지원 거부. 이란 고립 심화.', }, { id: 'bn27', timestamp: T0_NEWS + 8 * DAY_MS + 4 * HOUR_MS, category: 'oil', headline: '이란 미사일 재고 80% 소진 — WTI $180으로 하락', detail: '미 정보기관 분석 공개. 이란 잔존 이동식 발사대 10기 이하.', }, { id: 'bn28', timestamp: T0_NEWS + 8 * DAY_MS + 8 * HOUR_MS, category: 'trump', headline: '트럼프: "나탄즈 완전 파괴 — 이란 핵프로그램 종식"', detail: '"이란의 핵 야망은 영원히 끝났다. 역사가 나를 기억할 것."', }, { id: 'bn29', timestamp: T0_NEWS + 8 * DAY_MS + 10 * HOUR_MS, category: 'diplomacy', headline: 'UN 72시간 인도적 휴전 결의안 채택', detail: '안보리 찬성 13, 기권 2. 미국·이란 모두 입장 미정.', }, { id: 'bn30', timestamp: T0_NEWS + 8 * DAY_MS + 12 * HOUR_MS, category: 'economy', headline: '한국 NSC: "에너지 비상계획 수립 — 비축유 90일분"', detail: '호르무즈 봉쇄 장기화 대비. LNG 대체수입선 확보 논의.', }, ]; // ═══ 한국 전용 속보 (리플레이) ═══ const BREAKING_NEWS_KR: BreakingNews[] = [ // DAY 1 { id: 'kr1', timestamp: T0_NEWS - 6 * HOUR_MS, category: 'economy', headline: '한국 NSC 긴급소집 — 호르무즈 사태 대응 논의', detail: '외교·국방·산업부 장관 참석. 교민 보호·에너지 수급 점검.' }, { id: 'kr2', timestamp: T0_NEWS + 2 * HOUR_MS, category: 'economy', headline: '코스피 -4.2% 급락 — 유가 폭등 충격', detail: '한국 원유 수입의 70% 호르무즈 해협 경유. 정유·항공·운송주 급락.' }, { id: 'kr3', timestamp: T0_NEWS + 6 * HOUR_MS, category: 'oil', headline: '국내 유가 L당 2,200원 돌파 — 주유소 대란 시작', detail: '수도권 주유소 재고 부족 속출. 산업부 긴급 유류 배급 체계 가동 검토.' }, { id: 'kr4', timestamp: T0_NEWS + 10 * HOUR_MS, category: 'economy', headline: '한국 전략비축유 방출 검토 — 산업부 비상대책', detail: '걸프 한국 교민 대피 명령. 청해부대 한국 선박 호위 태세.' }, // DAY 2 { id: 'kr5', timestamp: T0_NEWS + 1 * DAY_MS, category: 'oil', headline: '한국행 원유 탱커 5척 오만만 대기 — 통행 불가', detail: 'VLCC 5척(250만 배럴) 호르무즈 해협 진입 불가. 정유사 원유 재고 2주분.' }, { id: 'kr6', timestamp: T0_NEWS + 1 * DAY_MS + 8 * HOUR_MS, category: 'economy', headline: '현대·삼성중공업 조선 수주 취소 우려 — 해운보험료 급등', detail: '걸프 향 선박 보험료 1,000% 인상. 해운업계 일 2,000억원 손실.' }, // DAY 3 { id: 'kr7', timestamp: T0_NEWS + 2 * DAY_MS, category: 'economy', headline: '한국 교민 1,200명 UAE·카타르 대피 중', detail: '외교부 특별기 2대 투입. 청해부대 ROKS 최영함 한국 선박 호위.' }, { id: 'kr8', timestamp: T0_NEWS + 2 * DAY_MS + 10 * HOUR_MS, category: 'oil', headline: '한국 정유사 가동률 70% 감축 — 원유 부족', detail: 'SK에너지·GS칼텍스·에쓰오일 감산. LPG·석유화학 제품 공급 차질.' }, // DAY 4 { id: 'kr9', timestamp: T0_NEWS + 3 * DAY_MS, category: 'economy', headline: '코스피 -8.5% — 서킷브레이커 발동', detail: '외국인 6조원 순매도. 원/달러 1,550원 돌파. 한국 CDS 급등.' }, { id: 'kr10', timestamp: T0_NEWS + 3 * DAY_MS + 6 * HOUR_MS, category: 'oil', headline: '한국, 미국·캐나다 긴급 원유 도입 협상', detail: '비(非)호르무즈 경유 원유 확보. 미 전략비축유 한국 우선배분 요청.' }, // DAY 5 { id: 'kr11', timestamp: T0_NEWS + 4 * DAY_MS, category: 'economy', headline: '한국 비상경제대책 발동 — 전략비축유 500만 배럴 방출', detail: '주유소 가격 L당 2,800원 돌파. 택시·화물차 운행 감축 논의.' }, { id: 'kr12', timestamp: T0_NEWS + 4 * DAY_MS + 8 * HOUR_MS, category: 'diplomacy', headline: '한국 외교부, 이란에 교민 안전 보장 요청', detail: '이란 주재 한국대사관 최소 인원 운영. 한국인 체류자 150명 잔류.' }, // DAY 6 { id: 'kr13', timestamp: T0_NEWS + 5 * DAY_MS, category: 'oil', headline: '한국 LNG 긴급 수입 — 호주·카타르 장기계약 가동', detail: 'LNG 스팟 가격 MMBtu $35 돌파. 가스공사 비축량 2주분.' }, { id: 'kr14', timestamp: T0_NEWS + 5 * DAY_MS + 6 * HOUR_MS, category: 'economy', headline: '한국 해운 3사 호르무즈 회항 — 희망봉 우회', detail: 'HMM·팬오션·대한해운 전 선박 희망봉 우회. 운항 일수 +14일, 비용 +40%.' }, // DAY 7 { id: 'kr15', timestamp: T0_NEWS + 6 * DAY_MS, category: 'economy', headline: '한국 제조업 PMI 42.1 — 3년 최저', detail: '석유화학·철강·자동차 부품 공급 차질. 수출 전년비 -15% 전망.' }, { id: 'kr16', timestamp: T0_NEWS + 6 * DAY_MS + 8 * HOUR_MS, category: 'diplomacy', headline: '한미 정상 긴급통화 — 에너지 안보 협력 강화', detail: '미국, 한국에 전략비축유 500만 배럴 추가 배분. 원유 수송 해군 호위 합의.' }, // DAY 8 { id: 'kr17', timestamp: T0_NEWS + 7 * DAY_MS, category: 'economy', headline: '한국 교민 350명 두바이 경유 긴급 귀국', detail: '군 수송기 C-130J 2대 투입. 청해부대 한국 선박 호위 지속.' }, { id: 'kr18', timestamp: T0_NEWS + 7 * DAY_MS + 8 * HOUR_MS, category: 'oil', headline: '한국 원유 비축 45일분으로 감소 — 경고 수준', detail: 'IEA 권고 90일 대비 절반. 추가 긴축 조치 불가피.' }, // DAY 9 { id: 'kr19', timestamp: T0_NEWS + 8 * DAY_MS, category: 'diplomacy', headline: '한국, UN 휴전 결의안 공동 발의', detail: '인도적 위기 해소와 호르무즈 재개방을 위한 72시간 휴전 촉구.' }, { id: 'kr20', timestamp: T0_NEWS + 8 * DAY_MS + 12 * HOUR_MS, category: 'economy', headline: '한국 NSC: "에너지 비상계획 수립 — 비축유 90일분 목표"', detail: '호르무즈 봉쇄 장기화 대비. LNG 대체수입선 확보 논의.' }, ]; const TYPE_LABELS: Record = { airstrike: 'STRIKE', explosion: 'EXPLOSION', missile_launch: 'LAUNCH', intercept: 'INTERCEPT', alert: 'ALERT', impact: 'IMPACT', }; const TYPE_COLORS: Record = { airstrike: '#ef4444', explosion: '#f97316', missile_launch: '#eab308', intercept: '#3b82f6', alert: '#a855f7', impact: '#ff0000', osint: '#06b6d4', }; // 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'; } // MarineTraffic-style category labels and colors const MT_CATEGORIES: Record = { cargo: { label: '화물선', color: '#8bc34a' }, // green tanker: { label: '유조선', color: '#e91e63' }, // red/pink passenger: { label: '여객선', color: '#2196f3' }, // blue high_speed: { label: '고속선', color: '#ff9800' }, // orange tug_special: { label: '예인선/특수선', color: '#00bcd4' }, // teal fishing: { label: '어선', color: '#ff5722' }, // deep orange pleasure: { label: '레저선', color: '#9c27b0' }, // purple military: { label: '군함', color: '#607d8b' }, // blue-grey unspecified: { label: '미분류', color: '#9e9e9e' }, // grey }; const NEWS_CATEGORY_STYLE: Record = { trump: { icon: '🇺🇸', color: '#ef4444', label: '트럼프' }, oil: { icon: '🛢️', color: '#f59e0b', label: '유가' }, diplomacy: { icon: '🌐', color: '#8b5cf6', label: '외교' }, economy: { icon: '📊', color: '#3b82f6', label: '경제' }, }; // OSINT category styles const OSINT_CAT_STYLE: Record = { military: { icon: '🎯', color: '#ef4444', label: '군사' }, oil: { icon: '🛢', color: '#f59e0b', label: '에너지' }, diplomacy: { icon: '🌐', color: '#8b5cf6', label: '외교' }, shipping: { icon: '🚢', color: '#06b6d4', label: '해운' }, nuclear: { icon: '☢', color: '#f97316', label: '핵' }, maritime_accident: { icon: '🚨', color: '#ef4444', label: '해양사고' }, fishing: { icon: '🐟', color: '#22c55e', label: '어선/수산' }, maritime_traffic: { icon: '🚢', color: '#3b82f6', label: '해상교통' }, general: { icon: '📰', color: '#6b7280', label: '일반' }, }; const EMPTY_OSINT: OsintItem[] = []; const EMPTY_SHIPS: import('../types').Ship[] = []; function timeAgo(ts: number): string { const diff = Date.now() - ts; const mins = Math.floor(diff / 60000); if (mins < 1) return '방금'; if (mins < 60) return `${mins}분 전`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}시간 전`; const days = Math.floor(hours / 24); return `${days}일 전`; } export function EventLog({ events, currentTime, totalShipCount: _totalShipCount, koreanShips, koreanShipsByCategory: _koreanShipsByCategory, chineseShips = [], osintFeed = EMPTY_OSINT, isLive = false, dashboardTab = 'iran', onTabChange: _onTabChange, ships = EMPTY_SHIPS }: Props) { const visibleEvents = useMemo( () => events.filter(e => e.timestamp <= currentTime).reverse(), [events, currentTime], ); const visibleNews = useMemo( () => events.length > 0 ? BREAKING_NEWS.filter(n => n.timestamp <= currentTime).reverse() : [], [events.length, currentTime], ); const visibleNewsKR = useMemo( () => events.length > 0 ? BREAKING_NEWS_KR.filter(n => n.timestamp <= currentTime).reverse() : [], [events.length, currentTime], ); // Iran-related ships (military + Iranian flag) const _iranMilitaryShips = useMemo(() => ships.filter(s => s.flag === 'IR' || s.category === 'carrier' || s.category === 'destroyer' || s.category === 'warship' || s.category === 'patrol' ).sort((a, b) => { const order: Record = { carrier: 0, destroyer: 1, warship: 2, patrol: 3, tanker: 4, cargo: 5, civilian: 6, unknown: 7 }; return (order[a.category] ?? 9) - (order[b.category] ?? 9); }), [ships], ); return (
{/* ═══════════════════════════════════════════════ IRAN TAB ═══════════════════════════════════════════════ */} {dashboardTab === 'iran' && ( <> {/* Breaking News Section (replay) */} {visibleNews.length > 0 && (
BREAKING 속보 / 주요 뉴스
{visibleNews.map(n => { const style = NEWS_CATEGORY_STYLE[n.category]; const isRecent = currentTime - n.timestamp < 2 * HOUR_MS; return (
{style.icon} {style.label} {new Date(n.timestamp + 9 * 3600_000).toISOString().slice(5, 16).replace('T', ' ')}
{n.headline}
{n.detail &&
{n.detail}
}
); })}
)} {/* Korean Ship Overview (Iran dashboard) */} {koreanShips.length > 0 && (
🇰🇷 한국 선박 현황 {koreanShips.length}척
{koreanShips.slice(0, 30).map(s => { const mt = MT_CATEGORIES[getShipMTCategory(s.typecode, s.category)] || { label: '기타', color: '#888' }; return (
🇰🇷 {s.name} {mt.label} {s.speed != null && s.speed > 0.5 ? ( {s.speed.toFixed(1)}kn ) : ( 정박 )}
); })}
)} {/* OSINT Live Feed (live mode) */} {isLive && osintFeed.length > 0 && ( <>
OSINT LIVE FEED {osintFeed.length}
{osintFeed.map(item => { const style = OSINT_CAT_STYLE[item.category] || OSINT_CAT_STYLE.general; const isRecent = Date.now() - item.timestamp < 3600_000; return (
{style.icon} {style.label} {item.language === 'ko' && KR} {timeAgo(item.timestamp)}
{item.title}
{item.source}
); })}
)} {isLive && osintFeed.length === 0 && (
OSINT LIVE FEED Loading...
)} {/* Event Log (replay mode) */} {!isLive && ( <>

Event Log

{visibleEvents.length === 0 && (
No events yet. Press play to start replay.
)} {visibleEvents.map(e => { const isNew = currentTime - e.timestamp < 86_400_000; return (
{TYPE_LABELS[e.type]}
{isNew && ( NEW )} {e.label}
{new Date(e.timestamp + 9 * 3600_000).toISOString().slice(5, 16).replace('T', ' ')} KST
{e.description && (
{e.description}
)}
); })}
)} )} {/* ═══════════════════════════════════════════════ KOREA TAB ═══════════════════════════════════════════════ */} {dashboardTab === 'korea' && ( <> {/* 한국 속보 (replay) */} {visibleNewsKR.length > 0 && (
속보 🇰🇷 한국 주요 뉴스
{visibleNewsKR.map(n => { const style = NEWS_CATEGORY_STYLE[n.category]; const isRecent = currentTime - n.timestamp < 2 * HOUR_MS; return (
{style.icon} {style.label} {new Date(n.timestamp + 9 * 3600_000).toISOString().slice(5, 16).replace('T', ' ')}
{n.headline}
{n.detail &&
{n.detail}
}
); })}
)} {/* 한국 선박 현황 — 선종별 분류 */}
🇰🇷 한국 선박 현황 {koreanShips.length}척
{koreanShips.length > 0 && (() => { // 선종별 그룹핑 const groups: Record = {}; for (const s of koreanShips) { const cat = getShipMTCategory(s.typecode, s.category); if (!groups[cat]) groups[cat] = []; groups[cat].push(s); } // 정렬 순서: 군함 → 유조선 → 화물선 → 여객선 → 어선 → 예인선 → 기타 const order = ['military', 'tanker', 'cargo', 'passenger', 'fishing', 'tug_special', 'high_speed', 'pleasure', 'unspecified']; const sorted = order.filter(k => groups[k]?.length); return (
{sorted.map(cat => { const mt = MT_CATEGORIES[cat] || MT_CATEGORIES.unspecified; const list = groups[cat]; const moving = list.filter(s => s.speed > 0.5).length; const anchored = list.length - moving; return (
{mt.label} {list.length} {moving > 0 && 항해 {moving}} {anchored > 0 && 정박 {anchored}}
); })}
); })()}
{/* 중국 선박 현황 */}
🇨🇳 중국 선박 현황 {chineseShips.length}척
{chineseShips.length > 0 && (() => { const groups: Record = {}; for (const s of chineseShips) { const cat = getShipMTCategory(s.typecode, s.category); if (!groups[cat]) groups[cat] = []; groups[cat].push(s); } const order = ['military', 'tanker', 'cargo', 'passenger', 'fishing', 'tug_special', 'high_speed', 'pleasure', 'unspecified']; const sorted = order.filter(k => groups[k]?.length); const fishingCount = groups['fishing']?.length || 0; return (
{fishingCount > 0 && (
🚨 중국어선 {fishingCount}척 우리 해역 근접
)} {sorted.map(cat => { const mt = MT_CATEGORIES[cat] || MT_CATEGORIES.unspecified; const list = groups[cat]; const moving = list.filter(s => s.speed > 0.5).length; const anchored = list.length - moving; return (
{mt.label} {list.length} {moving > 0 && 항해 {moving}} {anchored > 0 && 정박 {anchored}}
); })}
); })()}
{/* OSINT 피드 — 한국: 일반(general) 제외, 해양 관련만 표시 (라이브/리플레이 모두) */} {osintFeed.length > 0 && ( <>
🇰🇷 OSINT LIVE {(() => { const filtered = osintFeed.filter(i => i.category !== 'general' && i.category !== 'oil'); const seen = new Set(); return filtered.filter(i => { const key = i.title.replace(/\s+/g, '').slice(0, 30).toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }).length; })()}
{(() => { const filtered = osintFeed.filter(item => item.category !== 'general' && item.category !== 'oil'); const seen = new Set(); return filtered.filter(item => { const key = item.title.replace(/\s+/g, '').slice(0, 30).toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }); })().map(item => { const style = OSINT_CAT_STYLE[item.category] || OSINT_CAT_STYLE.general; const isRecent = Date.now() - item.timestamp < 3600_000; return (
{style.icon} {style.label} {item.language === 'ko' && KR} {timeAgo(item.timestamp)}
{item.title}
{item.source}
); })}
)} {osintFeed.length === 0 && (
🇰🇷 OSINT LIVE Loading...
)} )}
); }