+ {/* ═══════════════════════════════════════════════
+ IRAN TAB
+ ═══════════════════════════════════════════════ */}
+ {dashboardTab === 'iran' && (
+ <>
+ {/* Breaking News Section (replay) */}
+ {visibleNews.length > 0 && (
+
+
+ BREAKING
+ {t('events:news.breakingTitle')}
+
+
+ {visibleNews.map(n => {
+ const catColor = NEWS_CATEGORY_COLORS[n.category];
+ const catIcon = NEWS_CATEGORY_ICONS[n.category];
+ const isRecent = currentTime - n.timestamp < 2 * HOUR_MS;
+ return (
+
+
+
+ {catIcon} {t(`events:news.categoryLabel.${n.category}`)}
+
+
+ {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 && (
+
+
+ {'\u{1F1F0}\u{1F1F7}'}
+ {t('ships:shipStatus.koreanTitle')}
+ {koreanShips.length}{t('common:units.vessels')}
+
+
+ {koreanShips.slice(0, 30).map(s => {
+ const cat = getShipMTCategory(s.typecode, s.category);
+ const mtColor = MT_CATEGORY_COLORS[cat] || '#888';
+ const mtLabel = t(`ships:mtType.${cat}`, { defaultValue: t('ships:mtType.unspecified') });
+ return (
+
+ {'\u{1F1F0}\u{1F1F7}'}
+ {s.name}
+
+ {mtLabel}
+
+ {s.speed != null && s.speed > 0.5 ? (
+ {s.speed.toFixed(1)}kn
+ ) : (
+ {t('ships:status.anchored')}
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ {/* OSINT Live Feed (live mode) */}
+ {isLive && osintFeed.length > 0 && (
+ <>
+
+
+ {t('events:osint.liveTitle')}
+ {osintFeed.length}
+
+
+ >
+ )}
+ {isLive && osintFeed.length === 0 && (
+
+
+ {t('events:osint.liveTitle')}
+ {t('events:osint.loading')}
+
+ )}
+
+ {/* Event Log (replay mode) */}
+ {!isLive && (
+ <>
+
{t('events:log.title')}
+
+ {visibleEvents.length === 0 && (
+
{t('events:log.noEvents')}
+ )}
+ {visibleEvents.map(e => {
+ const isNew = currentTime - e.timestamp < 86_400_000;
+ return (
+
+
+ {TYPE_LABELS[e.type]}
+
+
+
+ {isNew && (
+ {t('events:log.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 && (
+
+
+ {t('events:news.breaking')}
+ {'\u{1F1F0}\u{1F1F7}'} {t('events:news.koreaTitle')}
+
+
+ {visibleNewsKR.map(n => {
+ const catColor = NEWS_CATEGORY_COLORS[n.category];
+ const catIcon = NEWS_CATEGORY_ICONS[n.category];
+ const isRecent = currentTime - n.timestamp < 2 * HOUR_MS;
+ return (
+
+
+
+ {catIcon} {t(`events:news.categoryLabel.${n.category}`)}
+
+
+ {new Date(n.timestamp + 9 * 3600_000).toISOString().slice(5, 16).replace('T', ' ')}
+
+
+
{n.headline}
+ {n.detail &&
{n.detail}
}
+
+ );
+ })}
+
+
+ )}
+
+ {/* 한국 선박 현황 — 선종별 분류 */}
+
+
+ {'\u{1F1F0}\u{1F1F7}'}
+ {t('ships:shipStatus.koreanTitle')}
+ {koreanShips.length}{t('common:units.vessels')}
+
+ {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 mtColor = MT_CATEGORY_COLORS[cat] || MT_CATEGORY_COLORS.unspecified;
+ const mtLabel = t(`ships:mtType.${cat}`, { defaultValue: t('ships:mtType.unspecified') });
+ const list = groups[cat];
+ const moving = list.filter(s => s.speed > 0.5).length;
+ const anchored = list.length - moving;
+ return (
+
+
+ {mtLabel}
+
+ {list.length}{t('common:units.vessels')}
+
+
+ {moving > 0 && {t('ships:status.underway')} {moving}}
+ {anchored > 0 && {t('ships:status.anchored')} {anchored}}
+
+
+ );
+ })}
+
+ );
+ })()}
+
+
+ {/* 중국 선박 현황 */}
+
+
+ {'\u{1F1E8}\u{1F1F3}'}
+ {t('ships:shipStatus.chineseTitle')}
+ {chineseShips.length}{t('common:units.vessels')}
+
+ {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 && (
+
+ {'\u{1F6A8}'}
+
+ {t('ships:shipStatus.chineseFishingAlert', { count: fishingCount })}
+
+
+ )}
+ {sorted.map(cat => {
+ const mtColor = MT_CATEGORY_COLORS[cat] || MT_CATEGORY_COLORS.unspecified;
+ const mtLabel = t(`ships:mtType.${cat}`, { defaultValue: t('ships:mtType.unspecified') });
+ const list = groups[cat];
+ const moving = list.filter(s => s.speed > 0.5).length;
+ const anchored = list.length - moving;
+ return (
+
+
+ {mtLabel}
+
+ {list.length}{t('common:units.vessels')}
+
+
+ {moving > 0 && {t('ships:status.underway')} {moving}}
+ {anchored > 0 && {t('ships:status.anchored')} {anchored}}
+
+
+ );
+ })}
+
+ );
+ })()}
+
+
+ {/* OSINT 피드 — 한국: 일반(general) 제외, 해양 관련만 표시 (라이브/리플레이 모두) */}
+ {osintFeed.length > 0 && (
+ <>
+
+
+ {'\u{1F1F0}\u{1F1F7}'} {t('events:osint.koreaLiveTitle')}
+ {(() => {
+ 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;
+ })()}
+
+
+ >
+ )}
+ {osintFeed.length === 0 && (
+
+
+ {'\u{1F1F0}\u{1F1F7}'} {t('events:osint.koreaLiveTitle')}
+ {t('events:osint.loading')}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/components/EventStrip.tsx b/frontend/src/components/EventStrip.tsx
new file mode 100644
index 0000000..8a72c87
--- /dev/null
+++ b/frontend/src/components/EventStrip.tsx
@@ -0,0 +1,142 @@
+import { useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import type { GeoEvent } from '../types';
+
+interface Props {
+ events: GeoEvent[];
+ currentTime: number;
+ startTime: number;
+ endTime: number;
+ onEventClick: (event: GeoEvent) => void;
+}
+
+const KST_OFFSET = 9 * 3600_000;
+
+const TYPE_COLORS: Record