+
+ {formatTime(startTime)}
+ {formatTime(currentTime)}
+ {formatTime(endTime)}
+
+
+
+
+ {eventMarkers.map(m => {
+ const ev = events.find(e => e.id === m.id)!;
+ const isSelected = selectedId === m.id;
+ const isInCluster = selectedCluster.some(c => c.id === m.id);
+ return (
+
handleMarkerClick(e, ev)}
+ />
+ );
+ })}
+
+
+ {/* Event detail strip — shown when a marker is selected */}
+ {selectedCluster.length > 0 && (
+
+ {selectedCluster.map(ev => {
+ const color = TYPE_COLORS[ev.type] || '#888';
+ const isPast = ev.timestamp <= currentTime;
+ const isActive = ev.id === selectedId;
+ const source = ev.source ? SOURCE_LABELS_KO[ev.source] : '';
+ const typeLabel = TYPE_LABELS_KO[ev.type] || ev.type;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/data/airports.ts b/src/data/airports.ts
new file mode 100644
index 0000000..761436d
--- /dev/null
+++ b/src/data/airports.ts
@@ -0,0 +1,112 @@
+// Major airports in the Middle East / Horn of Africa region
+// Reference: Flightradar24, OurAirports
+
+export interface Airport {
+ iata: string; // IATA code (e.g. "IKA")
+ icao: string; // ICAO code (e.g. "OIIE")
+ name: string;
+ nameKo?: string; // Korean name
+ lat: number;
+ lng: number;
+ type: 'large' | 'medium' | 'small' | 'military';
+ country: string; // ISO 2-letter
+ city?: string;
+}
+
+export const middleEastAirports: Airport[] = [
+ // ── 이란 (Iran) ──
+ { iata: 'IKA', icao: 'OIIE', name: 'Imam Khomeini Intl', nameKo: '이맘 호메이니 국제공항', lat: 35.4161, lng: 51.1522, type: 'large', country: 'IR', city: 'Tehran' },
+ { iata: 'THR', icao: 'OIII', name: 'Mehrabad Intl', nameKo: '메흐라바드 국제공항', lat: 35.6892, lng: 51.3134, type: 'large', country: 'IR', city: 'Tehran' },
+ { iata: 'MHD', icao: 'OIMM', name: 'Mashhad Intl', nameKo: '마슈하드 국제공항', lat: 36.2352, lng: 59.6410, type: 'large', country: 'IR', city: 'Mashhad' },
+ { iata: 'IFN', icao: 'OIFM', name: 'Isfahan Intl', nameKo: '이스파한 국제공항', lat: 32.7508, lng: 51.8613, type: 'large', country: 'IR', city: 'Isfahan' },
+ { iata: 'SYZ', icao: 'OISS', name: 'Shiraz Intl', nameKo: '시라즈 국제공항', lat: 29.5392, lng: 52.5899, type: 'large', country: 'IR', city: 'Shiraz' },
+ { iata: 'TBZ', icao: 'OITT', name: 'Tabriz Intl', nameKo: '타브리즈 국제공항', lat: 38.1339, lng: 46.2350, type: 'medium', country: 'IR', city: 'Tabriz' },
+ { iata: 'BND', icao: 'OIKB', name: 'Bandar Abbas Intl', nameKo: '반다르 아바스 국제공항', lat: 27.2183, lng: 56.3778, type: 'medium', country: 'IR', city: 'Bandar Abbas' },
+ { iata: 'AWZ', icao: 'OIAW', name: 'Ahvaz Intl', nameKo: '아흐바즈 국제공항', lat: 31.3374, lng: 48.7620, type: 'medium', country: 'IR', city: 'Ahvaz' },
+ { iata: 'KIH', icao: 'OIBK', name: 'Kish Island Intl', nameKo: '키시섬 국제공항', lat: 26.5262, lng: 53.9802, type: 'medium', country: 'IR', city: 'Kish Island' },
+ { iata: 'BUZ', icao: 'OIBB', name: 'Bushehr Airport', nameKo: '부셰르 공항', lat: 28.9448, lng: 50.8346, type: 'medium', country: 'IR', city: 'Bushehr' },
+ { iata: 'KER', icao: 'OIKK', name: 'Kerman Airport', nameKo: '케르만 공항', lat: 30.2744, lng: 56.9511, type: 'medium', country: 'IR', city: 'Kerman' },
+ { iata: 'ZAH', icao: 'OIZH', name: 'Zahedan Intl', nameKo: '자헤단 국제공항', lat: 29.4757, lng: 60.9062, type: 'medium', country: 'IR', city: 'Zahedan' },
+
+ // ── 이란 군사기지 (Iran Military) ──
+ { iata: '', icao: 'OIFH', name: 'Isfahan (Haft) AFB', nameKo: '이스파한 군사기지', lat: 32.5669, lng: 51.6916, type: 'military', country: 'IR', city: 'Isfahan' },
+ { iata: '', icao: 'OIIA', name: 'Tehran Doshan Tappeh AFB', nameKo: '도샨 타페 공군기지', lat: 35.7030, lng: 51.4750, type: 'military', country: 'IR', city: 'Tehran' },
+ { iata: '', icao: 'OINR', name: 'Nojeh AFB (Hamadan)', nameKo: '노제 공군기지', lat: 34.8919, lng: 48.2914, type: 'military', country: 'IR', city: 'Hamadan' },
+ { iata: '', icao: 'OIBJ', name: 'Bandar Abbas (Havadarya) NAS', nameKo: '반다르 아바스 해군항공기지', lat: 27.1583, lng: 56.1725, type: 'military', country: 'IR', city: 'Bandar Abbas' },
+ { iata: '', icao: 'OICC', name: 'Tabriz (Shahid Fakouri) AFB', nameKo: '타브리즈 공군기지', lat: 38.1500, lng: 46.2500, type: 'military', country: 'IR', city: 'Tabriz' },
+
+ // ── 이라크 (Iraq) ──
+ { iata: 'BGW', icao: 'ORBI', name: 'Baghdad Intl', nameKo: '바그다드 국제공항', lat: 33.2625, lng: 44.2346, type: 'large', country: 'IQ', city: 'Baghdad' },
+ { iata: 'BSR', icao: 'ORMM', name: 'Basra Intl', nameKo: '바스라 국제공항', lat: 30.5491, lng: 47.6621, type: 'medium', country: 'IQ', city: 'Basra' },
+ { iata: 'EBL', icao: 'ORER', name: 'Erbil Intl', nameKo: '에르빌 국제공항', lat: 36.2376, lng: 43.9632, type: 'medium', country: 'IQ', city: 'Erbil' },
+ { iata: '', icao: 'ORAA', name: 'Al Asad Airbase', nameKo: '알 아사드 공군기지', lat: 33.7856, lng: 42.4412, type: 'military', country: 'IQ', city: 'Anbar' },
+ { iata: '', icao: 'ORBD', name: 'Balad (Al Bakr) AB', nameKo: '발라드 공군기지', lat: 33.9402, lng: 44.3615, type: 'military', country: 'IQ', city: 'Balad' },
+
+ // ── 이스라엘 (Israel) ──
+ { iata: 'TLV', icao: 'LLBG', name: 'Ben Gurion Intl', nameKo: '벤 구리온 국제공항', lat: 32.0114, lng: 34.8867, type: 'large', country: 'IL', city: 'Tel Aviv' },
+ { iata: '', icao: 'LLNV', name: 'Nevatim AFB', nameKo: '네바팀 공군기지', lat: 31.2083, lng: 34.9389, type: 'military', country: 'IL', city: 'Be\'er Sheva' },
+ { iata: '', icao: 'LLRM', name: 'Ramon AFB', nameKo: '라몬 공군기지', lat: 30.7761, lng: 34.6667, type: 'military', country: 'IL', city: 'Negev' },
+ { iata: '', icao: 'LLHA', name: 'Hatzerim AFB', nameKo: '하체림 공군기지', lat: 31.2333, lng: 34.6667, type: 'military', country: 'IL', city: 'Be\'er Sheva' },
+
+ // ── UAE ──
+ { iata: 'DXB', icao: 'OMDB', name: 'Dubai Intl', nameKo: '두바이 국제공항', lat: 25.2528, lng: 55.3644, type: 'large', country: 'AE', city: 'Dubai' },
+ { iata: 'AUH', icao: 'OMAA', name: 'Abu Dhabi Intl', nameKo: '아부다비 국제공항', lat: 24.4430, lng: 54.6511, type: 'large', country: 'AE', city: 'Abu Dhabi' },
+ { iata: 'SHJ', icao: 'OMSJ', name: 'Sharjah Intl', nameKo: '샤르자 국제공항', lat: 25.3286, lng: 55.5172, type: 'medium', country: 'AE', city: 'Sharjah' },
+ { iata: '', icao: 'OMAD', name: 'Al Dhafra AFB', nameKo: '알 다프라 공군기지', lat: 24.2483, lng: 54.5481, type: 'military', country: 'AE', city: 'Abu Dhabi' },
+
+ // ── 사우디아라비아 (Saudi Arabia) ──
+ { iata: 'RUH', icao: 'OERK', name: 'King Khalid Intl', nameKo: '킹 칼리드 국제공항', lat: 24.9576, lng: 46.6988, type: 'large', country: 'SA', city: 'Riyadh' },
+ { iata: 'JED', icao: 'OEJN', name: 'King Abdulaziz Intl', nameKo: '킹 압둘아지즈 국제공항', lat: 21.6796, lng: 39.1565, type: 'large', country: 'SA', city: 'Jeddah' },
+ { iata: 'DMM', icao: 'OEDF', name: 'King Fahd Intl', nameKo: '킹 파흐드 국제공항', lat: 26.4712, lng: 49.7979, type: 'large', country: 'SA', city: 'Dammam' },
+ { iata: '', icao: 'OEPS', name: 'Prince Sultan AB', nameKo: '프린스 술탄 공군기지', lat: 24.0625, lng: 47.5806, type: 'military', country: 'SA', city: 'Al Kharj' },
+
+ // ── 카타르 (Qatar) ──
+ { iata: 'DOH', icao: 'OTHH', name: 'Hamad Intl', nameKo: '하마드 국제공항', lat: 25.2731, lng: 51.6081, type: 'large', country: 'QA', city: 'Doha' },
+ { iata: '', icao: 'OTBH', name: 'Al Udeid AB', nameKo: '알 우데이드 공군기지', lat: 25.1173, lng: 51.3150, type: 'military', country: 'QA', city: 'Doha' },
+
+ // ── 바레인 (Bahrain) ──
+ { iata: 'BAH', icao: 'OBBI', name: 'Bahrain Intl', nameKo: '바레인 국제공항', lat: 26.2708, lng: 50.6336, type: 'large', country: 'BH', city: 'Manama' },
+ { iata: '', icao: 'OBBS', name: 'Isa AB (NSA Bahrain)', nameKo: '이사 공군기지', lat: 26.1572, lng: 50.5911, type: 'military', country: 'BH', city: 'Manama' },
+
+ // ── 쿠웨이트 (Kuwait) ──
+ { iata: 'KWI', icao: 'OKBK', name: 'Kuwait Intl', nameKo: '쿠웨이트 국제공항', lat: 29.2267, lng: 47.9689, type: 'large', country: 'KW', city: 'Kuwait City' },
+ { iata: '', icao: 'OKAJ', name: 'Ali Al Salem AB', nameKo: '알리 알 살렘 공군기지', lat: 29.3467, lng: 47.5211, type: 'military', country: 'KW', city: 'Kuwait' },
+
+ // ── 오만 (Oman) ──
+ { iata: 'MCT', icao: 'OOMS', name: 'Muscat Intl', nameKo: '무스카트 국제공항', lat: 23.5933, lng: 58.2844, type: 'large', country: 'OM', city: 'Muscat' },
+ { iata: '', icao: 'OMTH', name: 'Thumrait AB', nameKo: '튬라이트 공군기지', lat: 17.6660, lng: 54.0246, type: 'military', country: 'OM', city: 'Thumrait' },
+
+ // ── 터키 (Turkey) ──
+ { iata: 'IST', icao: 'LTFM', name: 'Istanbul Airport', nameKo: '이스탄불 공항', lat: 41.2753, lng: 28.7519, type: 'large', country: 'TR', city: 'Istanbul' },
+ { iata: 'ESB', icao: 'LTAC', name: 'Ankara Esenboğa', nameKo: '앙카라 에센보아 공항', lat: 40.1281, lng: 32.9951, type: 'large', country: 'TR', city: 'Ankara' },
+ { iata: 'ADA', icao: 'LTAF', name: 'Adana Şakirpaşa', nameKo: '아다나 공항', lat: 36.9822, lng: 35.2804, type: 'medium', country: 'TR', city: 'Adana' },
+ { iata: '', icao: 'LTAG', name: 'Incirlik AB', nameKo: '인시를릭 공군기지', lat: 37.0021, lng: 35.4259, type: 'military', country: 'TR', city: 'Adana' },
+ { iata: 'DYB', icao: 'LTCC', name: 'Diyarbakır Airport', nameKo: '디야르바키르 공항', lat: 37.8940, lng: 40.2010, type: 'medium', country: 'TR', city: 'Diyarbakır' },
+
+ // ── 요르단 (Jordan) ──
+ { iata: 'AMM', icao: 'OJAI', name: 'Queen Alia Intl', nameKo: '퀸 알리아 국제공항', lat: 31.7226, lng: 35.9932, type: 'large', country: 'JO', city: 'Amman' },
+
+ // ── 레바논 (Lebanon) ──
+ { iata: 'BEY', icao: 'OLBA', name: 'Beirut–Rafic Hariri Intl', nameKo: '베이루트 국제공항', lat: 33.8209, lng: 35.4884, type: 'large', country: 'LB', city: 'Beirut' },
+
+ // ── 시리아 (Syria) ──
+ { iata: 'DAM', icao: 'OSDI', name: 'Damascus Intl', nameKo: '다마스쿠스 국제공항', lat: 33.4114, lng: 36.5156, type: 'large', country: 'SY', city: 'Damascus' },
+
+ // ── 이집트 (Egypt) ──
+ { iata: 'CAI', icao: 'HECA', name: 'Cairo Intl', nameKo: '카이로 국제공항', lat: 30.1219, lng: 31.4056, type: 'large', country: 'EG', city: 'Cairo' },
+
+ // ── 파키스탄 (Pakistan) ──
+ { iata: 'KHI', icao: 'OPKC', name: 'Jinnah Intl', nameKo: '진나 국제공항', lat: 24.9065, lng: 67.1609, type: 'large', country: 'PK', city: 'Karachi' },
+
+ // ── 지부티 (Djibouti) ──
+ { iata: 'JIB', icao: 'HDAM', name: 'Djibouti–Ambouli Intl', nameKo: '지부티 국제공항', lat: 11.5473, lng: 43.1595, type: 'medium', country: 'DJ', city: 'Djibouti' },
+ { iata: '', icao: 'HDCL', name: 'Camp Lemonnier', nameKo: '캠프 르모니에 (미군)', lat: 11.5474, lng: 43.1556, type: 'military', country: 'DJ', city: 'Djibouti' },
+
+ // ── 예멘 (Yemen) ──
+ { iata: 'ADE', icao: 'OYAA', name: 'Aden Intl', nameKo: '아덴 국제공항', lat: 12.8295, lng: 45.0288, type: 'medium', country: 'YE', city: 'Aden' },
+ { iata: 'SAH', icao: 'OYSN', name: 'Sana\'a Intl', nameKo: '사나 국제공항', lat: 15.4763, lng: 44.2197, type: 'medium', country: 'YE', city: 'Sana\'a' },
+
+ // ── 소말리아 (Somalia) ──
+ { iata: 'BSA', icao: 'HCMF', name: 'Bosaso Airport', nameKo: '보사소 공항', lat: 11.2753, lng: 49.1494, type: 'small', country: 'SO', city: 'Bosaso' },
+ { iata: 'MGQ', icao: 'HCMM', name: 'Aden Abdulle Intl', nameKo: '모가디슈 국제공항', lat: 2.0144, lng: 45.3047, type: 'medium', country: 'SO', city: 'Mogadishu' },
+];
diff --git a/src/data/countryLabels.ts b/src/data/countryLabels.ts
new file mode 100644
index 0000000..29666d2
--- /dev/null
+++ b/src/data/countryLabels.ts
@@ -0,0 +1,126 @@
+// ═══ 한글 국가명 라벨 데이터 ═══
+// 중동 + 동아시아 지역 국가명 (지도 오버레이용)
+
+export interface CountryLabel {
+ name: string; // 한글 국가명
+ nameEn: string; // 영문 (참고용)
+ lat: number;
+ lng: number;
+ rank: number; // 1=대국(큰글씨), 2=중간, 3=소국(작은글씨)
+}
+
+export const countryLabels: CountryLabel[] = [
+ // ── 중동 · 서아시아 ──
+ { name: '이란', nameEn: 'Iran', lat: 32.5, lng: 53.7, rank: 1 },
+ { name: '이라크', nameEn: 'Iraq', lat: 33.2, lng: 43.7, rank: 1 },
+ { name: '사우디아라비아', nameEn: 'Saudi Arabia', lat: 24.0, lng: 45.0, rank: 1 },
+ { name: '튀르키예', nameEn: 'Turkey', lat: 39.0, lng: 35.2, rank: 1 },
+ { name: '이집트', nameEn: 'Egypt', lat: 26.8, lng: 30.8, rank: 1 },
+ { name: '시리아', nameEn: 'Syria', lat: 35.0, lng: 38.5, rank: 2 },
+ { name: '요르단', nameEn: 'Jordan', lat: 31.3, lng: 36.5, rank: 2 },
+ { name: '레바논', nameEn: 'Lebanon', lat: 33.9, lng: 35.9, rank: 3 },
+ { name: '이스라엘', nameEn: 'Israel', lat: 31.5, lng: 34.8, rank: 3 },
+ { name: '쿠웨이트', nameEn: 'Kuwait', lat: 29.3, lng: 47.5, rank: 3 },
+ { name: '바레인', nameEn: 'Bahrain', lat: 26.07, lng: 50.55, rank: 3 },
+ { name: '카타르', nameEn: 'Qatar', lat: 25.3, lng: 51.2, rank: 3 },
+ { name: 'UAE', nameEn: 'UAE', lat: 24.0, lng: 54.0, rank: 2 },
+ { name: '오만', nameEn: 'Oman', lat: 21.5, lng: 57.0, rank: 2 },
+ { name: '예멘', nameEn: 'Yemen', lat: 15.6, lng: 48.5, rank: 2 },
+ { name: '아프가니스탄', nameEn: 'Afghanistan', lat: 33.9, lng: 67.7, rank: 1 },
+ { name: '파키스탄', nameEn: 'Pakistan', lat: 30.4, lng: 69.3, rank: 1 },
+ { name: '투르크메니스탄', nameEn: 'Turkmenistan', lat: 39.0, lng: 59.6, rank: 2 },
+ { name: '우즈베키스탄', nameEn: 'Uzbekistan', lat: 41.4, lng: 64.6, rank: 2 },
+ { name: '아르메니아', nameEn: 'Armenia', lat: 40.1, lng: 44.5, rank: 3 },
+ { name: '아제르바이잔', nameEn: 'Azerbaijan', lat: 40.4, lng: 49.9, rank: 3 },
+ { name: '조지아', nameEn: 'Georgia', lat: 42.3, lng: 43.4, rank: 3 },
+ { name: '수단', nameEn: 'Sudan', lat: 16.0, lng: 30.2, rank: 2 },
+ { name: '에리트레아', nameEn: 'Eritrea', lat: 15.2, lng: 39.8, rank: 3 },
+ { name: '에티오피아', nameEn: 'Ethiopia', lat: 9.1, lng: 40.5, rank: 1 },
+ { name: '소말리아', nameEn: 'Somalia', lat: 5.2, lng: 46.2, rank: 2 },
+ { name: '지부티', nameEn: 'Djibouti', lat: 11.6, lng: 43.1, rank: 3 },
+ { name: '리비아', nameEn: 'Libya', lat: 27.0, lng: 17.2, rank: 2 },
+ { name: '키프로스', nameEn: 'Cyprus', lat: 35.1, lng: 33.4, rank: 3 },
+
+ // ── 페르시아만 해역 라벨 ──
+ { name: '페르시아만', nameEn: 'Persian Gulf', lat: 27.0, lng: 51.5, rank: 2 },
+ { name: '호르무즈 해협', nameEn: 'Strait of Hormuz', lat: 26.56, lng: 56.25, rank: 3 },
+ { name: '오만만', nameEn: 'Gulf of Oman', lat: 24.5, lng: 58.5, rank: 3 },
+ { name: '홍해', nameEn: 'Red Sea', lat: 20.0, lng: 38.5, rank: 2 },
+ { name: '아덴만', nameEn: 'Gulf of Aden', lat: 12.5, lng: 47.0, rank: 3 },
+ { name: '아라비아해', nameEn: 'Arabian Sea', lat: 16.0, lng: 62.0, rank: 2 },
+ { name: '카스피해', nameEn: 'Caspian Sea', lat: 41.0, lng: 51.0, rank: 2 },
+ { name: '지중해', nameEn: 'Mediterranean Sea', lat: 35.0, lng: 25.0, rank: 2 },
+
+ // ── 주요 도시 (이란) ──
+ { name: '테헤란', nameEn: 'Tehran', lat: 35.69, lng: 51.39, rank: 2 },
+ { name: '이스파한', nameEn: 'Isfahan', lat: 32.65, lng: 51.68, rank: 3 },
+ { name: '타브리즈', nameEn: 'Tabriz', lat: 38.08, lng: 46.29, rank: 3 },
+ { name: '시라즈', nameEn: 'Shiraz', lat: 29.59, lng: 52.58, rank: 3 },
+ { name: '마슈하드', nameEn: 'Mashhad', lat: 36.30, lng: 59.60, rank: 3 },
+ { name: '반다르아바스', nameEn: 'Bandar Abbas', lat: 27.18, lng: 56.28, rank: 3 },
+ { name: '부셰르', nameEn: 'Bushehr', lat: 28.97, lng: 50.84, rank: 3 },
+ { name: '나탄즈', nameEn: 'Natanz', lat: 33.51, lng: 51.92, rank: 3 },
+ { name: '아바즈', nameEn: 'Ahvaz', lat: 31.32, lng: 48.67, rank: 3 },
+ { name: '케르만', nameEn: 'Kerman', lat: 30.28, lng: 57.08, rank: 3 },
+ { name: '케슘섬', nameEn: 'Qeshm Island', lat: 26.85, lng: 55.90, rank: 3 },
+ { name: '하르그섬', nameEn: 'Kharg Island', lat: 29.24, lng: 50.31, rank: 3 },
+
+ // ── 주요 도시 (중동 기타) ──
+ { name: '바그다드', nameEn: 'Baghdad', lat: 33.31, lng: 44.37, rank: 2 },
+ { name: '에르빌', nameEn: 'Erbil', lat: 36.19, lng: 44.01, rank: 3 },
+ { name: '다마스쿠스', nameEn: 'Damascus', lat: 33.51, lng: 36.28, rank: 3 },
+ { name: '베이루트', nameEn: 'Beirut', lat: 33.89, lng: 35.50, rank: 3 },
+ { name: '예루살렘', nameEn: 'Jerusalem', lat: 31.77, lng: 35.23, rank: 3 },
+ { name: '텔아비브', nameEn: 'Tel Aviv', lat: 32.08, lng: 34.78, rank: 3 },
+ { name: '리야드', nameEn: 'Riyadh', lat: 24.71, lng: 46.67, rank: 2 },
+ { name: '두바이', nameEn: 'Dubai', lat: 25.20, lng: 55.27, rank: 3 },
+ { name: '아부다비', nameEn: 'Abu Dhabi', lat: 24.45, lng: 54.65, rank: 3 },
+ { name: '도하', nameEn: 'Doha', lat: 25.29, lng: 51.53, rank: 3 },
+ { name: '앙카라', nameEn: 'Ankara', lat: 39.93, lng: 32.85, rank: 2 },
+ { name: '인저를릭', nameEn: 'Incirlik AFB', lat: 37.00, lng: 35.43, rank: 3 },
+ { name: '카이로', nameEn: 'Cairo', lat: 30.04, lng: 31.24, rank: 2 },
+ { name: '무스카트', nameEn: 'Muscat', lat: 23.59, lng: 58.54, rank: 3 },
+ { name: '사나', nameEn: 'Sanaa', lat: 15.37, lng: 44.19, rank: 3 },
+ { name: '카불', nameEn: 'Kabul', lat: 34.53, lng: 69.17, rank: 3 },
+
+ // ── 주요 군사기지/시설 ──
+ { name: '알우데이드 기지', nameEn: 'Al Udeid AB', lat: 25.12, lng: 51.32, rank: 3 },
+ { name: '제벨알리 항', nameEn: 'Jebel Ali Port', lat: 25.01, lng: 55.06, rank: 3 },
+ { name: '디에고가르시아', nameEn: 'Diego Garcia', lat: -7.32, lng: 72.42, rank: 3 },
+
+ // ── 동아시아 ──
+ { name: '대한민국', nameEn: 'South Korea', lat: 36.0, lng: 127.8, rank: 1 },
+ { name: '북한', nameEn: 'North Korea', lat: 40.0, lng: 127.0, rank: 1 },
+ { name: '일본', nameEn: 'Japan', lat: 36.2, lng: 138.3, rank: 1 },
+ { name: '중국', nameEn: 'China', lat: 35.9, lng: 104.2, rank: 1 },
+ { name: '대만', nameEn: 'Taiwan', lat: 23.7, lng: 121.0, rank: 2 },
+ { name: '러시아', nameEn: 'Russia', lat: 55.0, lng: 105.0, rank: 1 },
+ { name: '몽골', nameEn: 'Mongolia', lat: 46.9, lng: 103.8, rank: 1 },
+ { name: '필리핀', nameEn: 'Philippines', lat: 12.9, lng: 121.8, rank: 2 },
+ { name: '베트남', nameEn: 'Vietnam', lat: 14.1, lng: 108.3, rank: 2 },
+
+ // ── 동해/서해/남해 해역 ──
+ { name: '동해', nameEn: 'East Sea', lat: 38.5, lng: 132.0, rank: 2 },
+ { name: '서해(황해)', nameEn: 'Yellow Sea', lat: 35.5, lng: 124.0, rank: 2 },
+ { name: '남해', nameEn: 'South Sea', lat: 33.0, lng: 128.0, rank: 3 },
+ { name: '동중국해', nameEn: 'East China Sea', lat: 28.0, lng: 126.0, rank: 2 },
+ { name: '남중국해', nameEn: 'South China Sea', lat: 15.0, lng: 115.0, rank: 2 },
+ { name: '태평양', nameEn: 'Pacific Ocean', lat: 25.0, lng: 155.0, rank: 1 },
+
+ // ── 인도양/아프리카 동부 ──
+ { name: '인도', nameEn: 'India', lat: 20.6, lng: 79.0, rank: 1 },
+ { name: '인도양', nameEn: 'Indian Ocean', lat: 0.0, lng: 75.0, rank: 1 },
+];
+
+/** GeoJSON FeatureCollection 변환 */
+export function countryLabelsGeoJSON(): GeoJSON.FeatureCollection {
+ return {
+ type: 'FeatureCollection',
+ features: countryLabels.map((c, i) => ({
+ type: 'Feature' as const,
+ id: i,
+ geometry: { type: 'Point' as const, coordinates: [c.lng, c.lat] },
+ properties: { name: c.name, nameEn: c.nameEn, rank: c.rank },
+ })),
+ };
+}
diff --git a/src/data/damagedShips.ts b/src/data/damagedShips.ts
new file mode 100644
index 0000000..6f6b54d
--- /dev/null
+++ b/src/data/damagedShips.ts
@@ -0,0 +1,148 @@
+// ═══ 피격 선박 데이터 ═══
+// sampleData.ts의 해상 공격 이벤트와 연동
+
+const T0 = new Date('2026-03-01T12:01:00Z').getTime();
+const HOUR = 3600_000;
+const DAY = 24 * HOUR;
+
+export interface DamagedShip {
+ id: string;
+ name: string;
+ flag: string; // 국적 코드
+ type: string; // VLCC, LNG, Container 등
+ lat: number;
+ lng: number;
+ damagedAt: number; // unix ms — 피격 시각
+ cause: string; // 기뢰, 드론, 대함미사일 등
+ damage: 'sunk' | 'severe' | 'moderate' | 'minor';
+ description: string;
+ eventId: string; // 연관 GeoEvent id
+}
+
+export const damagedShips: DamagedShip[] = [
+ // DAY 3 — 3월 3일
+ {
+ id: 'ds-1',
+ name: 'ATHENA GLORY',
+ flag: 'GR',
+ type: 'VLCC',
+ lat: 26.5500, lng: 56.3500,
+ damagedAt: T0 + 2 * DAY,
+ cause: '기뢰 접촉',
+ damage: 'severe',
+ description: '그리스 국적 VLCC, 호르무즈 해협 기뢰 접촉. 원유 유출.',
+ eventId: 'd3-sea1',
+ },
+ // DAY 6 — 3월 6일: IRGC 고속정
+ {
+ id: 'ds-2',
+ name: 'IRGC FAST BOAT x4',
+ flag: 'IR',
+ type: 'MILITARY',
+ lat: 26.4000, lng: 56.4000,
+ damagedAt: T0 + 2 * DAY + 3 * HOUR,
+ cause: '미 구축함 함포 교전',
+ damage: 'sunk',
+ description: 'IRGC 고속정 4척, 미 구축함 교전 중 격침.',
+ eventId: 'd3-sea2',
+ },
+ // DAY 11 — 3월 11일: 호르무즈 기뢰 자폭
+ {
+ id: 'ds-3',
+ name: 'IRGC MINESWEEPER',
+ flag: 'IR',
+ type: 'MILITARY',
+ lat: 26.5667, lng: 56.2500,
+ damagedAt: T0 + 10 * DAY + 4 * HOUR,
+ cause: '자체 기뢰 폭발',
+ damage: 'sunk',
+ description: 'IRGC 소해정 1척, 자체 배치 기뢰 폭발로 침몰.',
+ eventId: 'd11-ir1',
+ },
+ // DAY 12 — 3월 12일
+ {
+ id: 'ds-4',
+ name: 'SHOWA MARU',
+ flag: 'JP',
+ type: 'VLCC',
+ lat: 26.3500, lng: 56.5000,
+ damagedAt: T0 + 11 * DAY,
+ cause: '기뢰 접촉',
+ damage: 'severe',
+ description: '일본 국적 VLCC, 호르무즈 해협 기뢰 접촉. 선체 파공, 원유 유출.',
+ eventId: 'd12-sea1',
+ },
+ {
+ id: 'ds-5',
+ name: 'SK INNOVATION',
+ flag: 'KR',
+ type: 'LNG',
+ lat: 26.2000, lng: 56.6000,
+ damagedAt: T0 + 11 * DAY + 2 * HOUR,
+ cause: 'IRGC 자폭드론',
+ damage: 'minor',
+ description: '한국행 LNG 운반선, 드론 2대 피격. 경미 손상.',
+ eventId: 'd12-sea2',
+ },
+ {
+ id: 'ds-6',
+ name: 'ATHENS EXPRESS',
+ flag: 'GR',
+ type: 'CONTAINER',
+ lat: 25.8000, lng: 56.8000,
+ damagedAt: T0 + 11 * DAY + 3 * HOUR,
+ cause: 'IRGC 누르 대함미사일',
+ damage: 'moderate',
+ description: '그리스 컨테이너선, 대함미사일 피격. 화재, 승조원 부상.',
+ eventId: 'd12-sea3',
+ },
+ {
+ id: 'ds-7',
+ name: 'IRGC FAST BOAT x5',
+ flag: 'IR',
+ type: 'MILITARY',
+ lat: 26.6000, lng: 56.1000,
+ damagedAt: T0 + 11 * DAY + 5 * HOUR,
+ cause: '미 구축함 함포/CIWS',
+ damage: 'sunk',
+ description: 'IRGC 고속정 5척, USS 마이클 머피 교전 중 격침.',
+ eventId: 'd12-sea4',
+ },
+ // DAY 12 후반 — 3월 12일 오후
+ {
+ id: 'ds-8',
+ name: 'IRGC MINE LAYER',
+ flag: 'IR',
+ type: 'MILITARY',
+ lat: 26.4500, lng: 56.3500,
+ damagedAt: T0 + 11 * DAY + 14 * HOUR,
+ cause: '자체 기뢰 폭발',
+ damage: 'sunk',
+ description: 'IRGC 기뢰부설정, 자체 배치 기뢰 접촉 폭발로 침몰. 승조원 12명 사망 추정.',
+ eventId: 'd12-sea5',
+ },
+ {
+ id: 'ds-9',
+ name: 'PACIFIC PIONEER',
+ flag: 'PA',
+ type: 'BULK',
+ lat: 26.3000, lng: 56.7000,
+ damagedAt: T0 + 11 * DAY + 16 * HOUR,
+ cause: '부유기뢰 접촉',
+ damage: 'moderate',
+ description: '파나마 국적 벌크선, 오만만 북부 부유기뢰 접촉. 선수 파공, 느린 침수. 오만 해군 구조.',
+ eventId: 'd12-sea6',
+ },
+ {
+ id: 'ds-10',
+ name: 'IRGC FAST BOAT x20+',
+ flag: 'IR',
+ type: 'MILITARY',
+ lat: 26.9800, lng: 56.0800,
+ damagedAt: T0 + 11 * DAY + 14 * HOUR,
+ cause: '미 F/A-18F 공습',
+ damage: 'sunk',
+ description: 'IRGC 게쉼섬 고속정 기지 공습. 고속정 20여 척 파괴/대파.',
+ eventId: 'd12-us5',
+ },
+];
diff --git a/src/data/iranBorder.ts b/src/data/iranBorder.ts
new file mode 100644
index 0000000..4ecd6e3
--- /dev/null
+++ b/src/data/iranBorder.ts
@@ -0,0 +1,105 @@
+// Simplified Iran border polygon (GeoJSON)
+// ~60 points tracing the approximate boundary
+export const iranBorderGeoJSON: GeoJSON.Feature = {
+ type: 'Feature',
+ properties: { name: 'Iran' },
+ geometry: {
+ type: 'Polygon',
+ coordinates: [[
+ // Northwest — Turkey/Armenia/Azerbaijan border
+ [44.0, 39.4],
+ [44.8, 39.7],
+ [45.5, 39.0],
+ [46.0, 38.9],
+ [47.0, 39.2],
+ [48.0, 38.8],
+ [48.5, 38.5],
+ [48.9, 38.4],
+ // Caspian Sea coast (south shore)
+ [49.0, 38.4],
+ [49.5, 37.5],
+ [50.0, 37.4],
+ [50.5, 37.0],
+ [51.0, 36.8],
+ [51.5, 36.8],
+ [52.0, 36.9],
+ [53.0, 36.9],
+ [53.9, 37.1],
+ [54.7, 37.3],
+ [55.4, 37.2],
+ [56.0, 37.4],
+ [57.0, 37.4],
+ [57.4, 37.6],
+ // Northeast — Turkmenistan border
+ [58.0, 37.6],
+ [58.8, 37.6],
+ [59.3, 37.5],
+ [60.0, 36.7],
+ [60.5, 36.5],
+ [61.0, 36.6],
+ [61.2, 36.6],
+ // East — Afghanistan border
+ [61.2, 35.6],
+ [61.2, 34.7],
+ [61.0, 34.0],
+ [60.5, 33.7],
+ [60.5, 33.1],
+ [60.8, 32.2],
+ [60.8, 31.5],
+ // Southeast — Pakistan border
+ [61.7, 31.4],
+ [61.8, 30.8],
+ [61.4, 29.8],
+ [60.9, 29.4],
+ [60.6, 28.5],
+ [61.0, 27.2],
+ [62.0, 26.4],
+ [63.2, 25.2],
+ // South coast — Gulf of Oman / Persian Gulf
+ [61.6, 25.2],
+ [60.0, 25.3],
+ [58.5, 25.6],
+ [57.8, 25.7],
+ [57.3, 26.0],
+ [56.4, 26.2],
+ [56.1, 26.0],
+ [55.5, 26.0],
+ [54.8, 26.5],
+ [54.3, 26.5],
+ [53.5, 26.6],
+ [52.5, 27.2],
+ [51.5, 27.9],
+ [50.8, 28.3],
+ [50.5, 28.8],
+ [50.2, 29.1],
+ [50.0, 29.3],
+ [49.5, 29.6],
+ [49.0, 29.8],
+ [48.6, 29.9],
+ // Southwest — Iraq border (Shatt al-Arab and west)
+ [48.4, 30.4],
+ [48.0, 30.5],
+ [47.7, 30.9],
+ [47.6, 31.4],
+ [47.1, 31.6],
+ [46.5, 32.0],
+ [46.1, 32.2],
+ [45.6, 32.9],
+ [45.4, 33.4],
+ [45.5, 33.9],
+ [45.6, 34.2],
+ [45.4, 34.5],
+ [45.2, 35.0],
+ [45.1, 35.4],
+ [45.4, 35.8],
+ [45.1, 36.0],
+ [44.8, 36.4],
+ [44.5, 37.0],
+ [44.3, 37.5],
+ [44.2, 38.0],
+ [44.0, 38.4],
+ [44.0, 39.0],
+ [44.0, 39.4], // close polygon
+ ]],
+ },
+};
diff --git a/src/data/oilFacilities.ts b/src/data/oilFacilities.ts
new file mode 100644
index 0000000..e181239
--- /dev/null
+++ b/src/data/oilFacilities.ts
@@ -0,0 +1,397 @@
+import type { OilFacility } from '../types';
+
+// T0 = 이란 보복 공격 기준시각
+const T0 = new Date('2026-03-01T12:01:00Z').getTime();
+const HOUR = 3600_000;
+
+// 이란 주요 석유·가스 시설 데이터
+// 출처: NIOC, EIA, IEA 공개 데이터 기반
+export const iranOilFacilities: OilFacility[] = [
+ // ═══ 주요 정유시설 (Refineries) ═══
+ {
+ id: 'ref-abadan',
+ name: 'Abadan Refinery',
+ nameKo: '아바단 정유소',
+ lat: 30.3358, lng: 48.2870,
+ type: 'refinery',
+ capacityBpd: 400_000,
+ operator: 'NIOC',
+ description: '이란 최대·최고(最古) 정유시설. 1912년 건설. 일 40만 배럴 처리.',
+ planned: true,
+ plannedLabel: 'D+17 B-2 정밀폭격 예정 — 이란 최대 정유능력 무력화 목표',
+ },
+ {
+ id: 'ref-isfahan',
+ name: 'Isfahan Refinery',
+ nameKo: '이스파한 정유소',
+ lat: 32.6100, lng: 51.7300,
+ type: 'refinery',
+ capacityBpd: 375_000,
+ operator: 'NIOC',
+ description: '이란 2위 정유소. 일 37.5만 배럴 처리. 중부 이란 핵심 시설.',
+ planned: true,
+ plannedLabel: 'D+18 F-35 편대 공격 예정 — 중부 정유능력 차단',
+ },
+ {
+ id: 'ref-bandarabbas',
+ name: 'Bandar Abbas Refinery (Persian Gulf Star)',
+ nameKo: '반다르아바스 정유소 (페르시안걸프스타르)',
+ lat: 27.1700, lng: 56.2200,
+ type: 'refinery',
+ capacityBpd: 360_000,
+ operator: 'PGPIC',
+ description: '2017년 완공 최신 정유소. 가스 응축액 처리. 일 36만 배럴.',
+ damaged: true,
+ damagedAt: T0 - 3 * HOUR, // IL airstrike il5 at 09:01 UTC
+ },
+ {
+ id: 'ref-tehran',
+ name: 'Tehran Refinery',
+ nameKo: '테헤란 정유소',
+ lat: 35.5700, lng: 51.4100,
+ type: 'refinery',
+ capacityBpd: 250_000,
+ operator: 'NIOC',
+ description: '수도 에너지 공급 핵심. 일 25만 배럴.',
+ planned: true,
+ plannedLabel: 'D+19 테헤란 에너지 고립 작전 — 수도 연료공급 차단',
+ },
+ {
+ id: 'ref-tabriz',
+ name: 'Tabriz Refinery',
+ nameKo: '타브리즈 정유소',
+ lat: 38.0100, lng: 46.2700,
+ type: 'refinery',
+ capacityBpd: 110_000,
+ operator: 'NIOC',
+ description: '북서부 이란 주요 정유소. 일 11만 배럴.',
+ damaged: true,
+ damagedAt: T0 - 6.5 * HOUR, // US airstrike us12 at 05:31 UTC
+ },
+ {
+ id: 'ref-arak',
+ name: 'Arak Refinery (Imam Khomeini)',
+ nameKo: '아라크 정유소 (이맘 호메이니)',
+ lat: 34.0700, lng: 49.7100,
+ type: 'refinery',
+ capacityBpd: 250_000,
+ operator: 'NIOC',
+ description: '중부 이란 전략적 위치. 일 25만 배럴.',
+ planned: true,
+ plannedLabel: 'D+18 F-15E 공격 예정 — 중부 연료보급 거점 파괴',
+ },
+ {
+ id: 'ref-shiraz',
+ name: 'Shiraz Refinery',
+ nameKo: '시라즈 정유소',
+ lat: 29.5500, lng: 52.4800,
+ type: 'refinery',
+ capacityBpd: 60_000,
+ operator: 'NIOC',
+ description: '남부 이란 정유소. 일 6만 배럴.',
+ },
+ {
+ id: 'ref-lavan',
+ name: 'Lavan Refinery',
+ nameKo: '라반 정유소',
+ lat: 26.8100, lng: 53.3500,
+ type: 'refinery',
+ capacityBpd: 60_000,
+ operator: 'NIOC',
+ description: '라반섬 해상 정유소. 일 6만 배럴. 페르시아만 원유 수출.',
+ },
+
+ // ═══ 주요 유전 (Oil Fields) ═══
+ {
+ id: 'oil-ahwaz',
+ name: 'Ahwaz-Asmari Oil Field',
+ nameKo: '아흐바즈-아스마리 유전',
+ lat: 31.3200, lng: 48.6700,
+ type: 'oilfield',
+ capacityBpd: 750_000,
+ reservesBbl: 25.5,
+ operator: 'NIOC',
+ description: '이란 최대 유전. 확인매장량 255억 배럴. 후제스탄 주.',
+ },
+ {
+ id: 'oil-marunfield',
+ name: 'Marun Oil Field',
+ nameKo: '마룬 유전',
+ lat: 31.6500, lng: 49.2000,
+ type: 'oilfield',
+ capacityBpd: 520_000,
+ reservesBbl: 16.0,
+ operator: 'NIOC',
+ description: '이란 2위 유전. 확인매장량 160억 배럴.',
+ },
+ {
+ id: 'oil-gachsaran',
+ name: 'Gachsaran Oil Field',
+ nameKo: '가치사란 유전',
+ lat: 30.3600, lng: 50.8000,
+ type: 'oilfield',
+ capacityBpd: 560_000,
+ reservesBbl: 15.0,
+ operator: 'NIOC',
+ description: '이란 3위 유전. 매장량 150억 배럴. 자그로스 산맥 서남.',
+ },
+ {
+ id: 'oil-agha-jari',
+ name: 'Aghajari Oil Field',
+ nameKo: '아가자리 유전',
+ lat: 30.7500, lng: 49.8300,
+ type: 'oilfield',
+ capacityBpd: 200_000,
+ reservesBbl: 14.0,
+ operator: 'NIOC',
+ description: '역사적 대형 유전. 매장량 140억 배럴.',
+ },
+ {
+ id: 'oil-karangoil',
+ name: 'Karanj Oil Field',
+ nameKo: '카란즈 유전',
+ lat: 31.9500, lng: 49.0500,
+ type: 'oilfield',
+ capacityBpd: 180_000,
+ reservesBbl: 5.0,
+ operator: 'NIOC',
+ description: '후제스탄 주 주요 유전. 매장량 50억 배럴.',
+ },
+ {
+ id: 'oil-yadavaran',
+ name: 'Yadavaran Oil Field',
+ nameKo: '야다바란 유전',
+ lat: 31.0200, lng: 47.8500,
+ type: 'oilfield',
+ capacityBpd: 300_000,
+ reservesBbl: 17.0,
+ operator: 'NIOC / Sinopec',
+ description: '이라크 국경 인근 초대형 유전. 매장량 170억 배럴. 중국 합작.',
+ },
+ {
+ id: 'oil-azadegan',
+ name: 'Azadegan Oil Field',
+ nameKo: '아자데간 유전',
+ lat: 31.5000, lng: 47.6000,
+ type: 'oilfield',
+ capacityBpd: 220_000,
+ reservesBbl: 33.0,
+ operator: 'NIOC',
+ description: '이란 최대 미개발 유전. 매장량 330억 배럴.',
+ },
+
+ // ═══ 가스전 (Gas Fields) ═══
+ {
+ id: 'gas-southpars',
+ name: 'South Pars Gas Field',
+ nameKo: '사우스파르스 가스전',
+ lat: 27.0000, lng: 52.0000,
+ type: 'gasfield',
+ capacityMcfd: 20_000,
+ reservesTcf: 500,
+ operator: 'Pars Oil & Gas Co.',
+ description: '세계 최대 가스전 (카타르 노스돔과 공유). 매장량 500조 입방피트. 이란 가스 수출 핵심.',
+ },
+ {
+ id: 'gas-northpars',
+ name: 'North Pars Gas Field',
+ nameKo: '노스파르스 가스전',
+ lat: 27.5000, lng: 52.5000,
+ type: 'gasfield',
+ capacityMcfd: 2_500,
+ reservesTcf: 50,
+ operator: 'NIOC',
+ description: '페르시아만 해상 가스전. 매장량 50조 입방피트.',
+ },
+ {
+ id: 'gas-kish',
+ name: 'Kish Gas Field',
+ nameKo: '키시 가스전',
+ lat: 26.5500, lng: 53.9800,
+ type: 'gasfield',
+ capacityMcfd: 3_000,
+ reservesTcf: 58,
+ operator: 'NIOC',
+ description: '키시섬 인근 해상 가스전. 매장량 58조 입방피트.',
+ },
+
+ // ═══ 수출 터미널 (Export Terminals) ═══
+ {
+ id: 'term-kharg',
+ name: 'Kharg Island Terminal',
+ nameKo: '하르그섬 수출터미널',
+ lat: 29.2300, lng: 50.3200,
+ type: 'terminal',
+ capacityBpd: 5_000_000,
+ operator: 'NIOC',
+ description: '이란 원유 수출의 90% 처리. 일 500만 배럴 수출 능력. 전략적 최핵심 시설.',
+ planned: true,
+ plannedLabel: 'D+16 최우선 타격 예정 — 이란 원유 수출 90% 차단 목표. B-2·F-35 합동 공격.',
+ },
+ {
+ id: 'term-lavan',
+ name: 'Lavan Island Terminal',
+ nameKo: '라반섬 수출터미널',
+ lat: 26.7900, lng: 53.3600,
+ type: 'terminal',
+ capacityBpd: 200_000,
+ operator: 'NIOC',
+ description: '라반섬 원유 수출터미널. 일 20만 배럴.',
+ },
+ {
+ id: 'term-jask',
+ name: 'Jask Oil Terminal',
+ nameKo: '자스크 수출터미널',
+ lat: 25.6400, lng: 57.7700,
+ type: 'terminal',
+ capacityBpd: 1_000_000,
+ operator: 'NIOC',
+ description: '호르무즈 해협 우회 수출터미널. 2021년 개장. 일 100만 배럴.',
+ planned: true,
+ plannedLabel: 'D+17 우회 수출로 차단 작전 — 오만만 경유 원유수출 봉쇄',
+ },
+
+ // ═══ 석유화학단지 (Petrochemical) ═══
+ {
+ id: 'petro-assaluyeh',
+ name: 'Assaluyeh Petrochemical Complex',
+ nameKo: '아살루예 석유화학단지',
+ lat: 27.4800, lng: 52.6100,
+ type: 'petrochemical',
+ operator: 'NPC',
+ description: '사우스파르스 육상 가스처리 허브. 세계 최대급 석유화학 단지.',
+ planned: true,
+ plannedLabel: 'D+20 가스수출 차단 작전 — 사우스파르스 육상 처리시설 타격',
+ },
+ {
+ id: 'petro-mahshahr',
+ name: 'Mahshahr Petrochemical Zone',
+ nameKo: '마흐샤르 석유화학단지',
+ lat: 30.4600, lng: 49.1700,
+ type: 'petrochemical',
+ operator: 'NPC',
+ description: '반다르 이맘 호메이니 인근 대규모 석유화학 단지.',
+ },
+
+ // ═══ 담수화 시설 (Desalination Plants) — 호르무즈 해협 인근 ═══
+ {
+ id: 'desal-jebel-ali',
+ name: 'Jebel Ali Desalination Plant',
+ nameKo: '제벨알리 담수화시설',
+ lat: 25.0547, lng: 55.0272,
+ type: 'desalination',
+ capacityMgd: 636,
+ operator: 'DEWA',
+ description: '세계 최대 담수화시설. 일 636만 갤런. 두바이 수돗물 98% 공급.',
+ },
+ {
+ id: 'desal-taweelah',
+ name: 'Taweelah Desalination Plant',
+ nameKo: '타위라 담수화시설',
+ lat: 24.6953, lng: 54.7428,
+ type: 'desalination',
+ capacityMgd: 200,
+ operator: 'EWEC / ACWA Power',
+ description: '세계 최대 RO 담수화시설. 일 2억 갤런. 아부다비 핵심 수자원.',
+ },
+ {
+ id: 'desal-fujairah',
+ name: 'Fujairah Desalination Plant',
+ nameKo: '푸자이라 담수화시설',
+ lat: 25.1288, lng: 56.3264,
+ type: 'desalination',
+ capacityMgd: 130,
+ operator: 'FEWA',
+ description: '호르무즈 해협 동측. 일 1.3억 갤런. 동부 에미리트 수자원.',
+ },
+ {
+ id: 'desal-sohar',
+ name: 'Sohar Desalination Plant',
+ nameKo: '소하르 담수화시설',
+ lat: 24.3476, lng: 56.7492,
+ type: 'desalination',
+ capacityMgd: 63,
+ operator: 'Sohar Power / Suez',
+ description: '오만 북부 산업지대 수자원. 일 6,300만 갤런.',
+ },
+ {
+ id: 'desal-barka',
+ name: 'Barka Desalination Plant',
+ nameKo: '바르카 담수화시설',
+ lat: 23.6850, lng: 57.8900,
+ type: 'desalination',
+ capacityMgd: 42,
+ operator: 'Oman Power & Water',
+ description: '오만 수도 무스카트 인근. 일 4,200만 갤런.',
+ },
+ {
+ id: 'desal-ghubrah',
+ name: 'Al Ghubrah Desalination Plant',
+ nameKo: '알구브라 담수화시설',
+ lat: 23.6000, lng: 58.4200,
+ type: 'desalination',
+ capacityMgd: 68,
+ operator: 'PAEW',
+ description: '무스카트 시내 위치. 오만 최대 담수화시설. 일 6,800만 갤런.',
+ },
+ {
+ id: 'desal-ras-al-khair',
+ name: 'Ras Al Khair Desalination Plant',
+ nameKo: '라스 알 카이르 담수화시설',
+ lat: 27.1500, lng: 49.2300,
+ type: 'desalination',
+ capacityMgd: 228,
+ operator: 'SWCC',
+ description: '세계 최대 하이브리드 담수화시설. 사우디 동부 해안. 일 2.28억 갤런.',
+ },
+ {
+ id: 'desal-jubail',
+ name: 'Jubail Desalination Plant',
+ nameKo: '주바일 담수화시설',
+ lat: 26.9598, lng: 49.5687,
+ type: 'desalination',
+ capacityMgd: 400,
+ operator: 'SWCC',
+ description: '사우디 동부 주바일 산업도시. 일 4억 갤런. 리야드까지 송수.',
+ },
+ {
+ id: 'desal-hidd',
+ name: 'Al Hidd Desalination Plant',
+ nameKo: '알 히드 담수화시설',
+ lat: 26.1500, lng: 50.6600,
+ type: 'desalination',
+ capacityMgd: 90,
+ operator: 'EWA Bahrain',
+ description: '바레인 주요 수자원. 일 9,000만 갤런. 국가 물 수요 80% 담당.',
+ },
+ {
+ id: 'desal-ras-laffan',
+ name: 'Ras Laffan Desalination Plant',
+ nameKo: '라스 라판 담수화시설',
+ lat: 25.9140, lng: 51.5260,
+ type: 'desalination',
+ capacityMgd: 63,
+ operator: 'Kahramaa',
+ description: '카타르 북부 LNG 허브 인접. 일 6,300만 갤런.',
+ },
+ {
+ id: 'desal-azzour',
+ name: 'Az-Zour Desalination Plant',
+ nameKo: '아즈주르 담수화시설',
+ lat: 28.7200, lng: 48.3700,
+ type: 'desalination',
+ capacityMgd: 107,
+ operator: 'MEW Kuwait',
+ description: '쿠웨이트 남부. 일 1.07억 갤런. 쿠웨이트시 수자원.',
+ },
+ {
+ id: 'desal-bandarabbas',
+ name: 'Bandar Abbas Desalination',
+ nameKo: '반다르아바스 담수화시설',
+ lat: 27.1800, lng: 56.2700,
+ type: 'desalination',
+ capacityMgd: 18,
+ operator: 'ABFA Iran',
+ description: '이란 호르무즈간 주. 일 1,800만 갤런. 이란 최대 해수담수화.',
+ },
+];
diff --git a/src/data/sampleData.ts b/src/data/sampleData.ts
new file mode 100644
index 0000000..44bc8df
--- /dev/null
+++ b/src/data/sampleData.ts
@@ -0,0 +1,1495 @@
+import type { GeoEvent, SensorLog } from '../types';
+
+// 기준 시간: 리플레이 시작 3월 1일 00:01 UTC
+// T0 (이란 보복 공격파) 12:01 UTC = 시작 후 12시간
+// 배경: 미국-이스라엘 합동 이란 공습 2월 28일 개시
+// 3월 1일, 이란 대규모 보복 공격 개시
+const T0 = new Date('2026-03-01T12:01:00Z').getTime();
+const HOUR = 3600_000;
+const MIN = 60_000;
+const DAY = 24 * HOUR;
+
+export const REPLAY_START = new Date('2026-03-01T00:01:00Z').getTime();
+// REPLAY_END: 오늘 23:59 UTC (항상 최신 상태 유지)
+const _today = new Date();
+_today.setUTCHours(23, 59, 0, 0);
+export const REPLAY_END = Math.max(
+ new Date('2026-03-12T23:59:00Z').getTime(), // 최소 D+12
+ _today.getTime(),
+);
+
+export const sampleEvents: GeoEvent[] = [
+ // ═══════════════════════════════════════════════════════════════════
+ // 1단계: 미국-이스라엘의 이란 공습 (2월 28일부터 계속)
+ // ═══════════════════════════════════════════════════════════════════
+
+ // ─── 00:00–03:00 UTC: 테헤란 및 군사시설 야간 공습 ───
+ {
+ id: 'us1', timestamp: T0 - 11.5 * HOUR,
+ lat: 35.6997, lng: 51.4038, type: 'airstrike', source: 'US',
+ label: '테헤란 — 최고지도자 관저 공습',
+ description: '미국-이스라엘 합동공격으로 최고지도자 관저 단지 파괴. 하메네이 암살 보도.',
+ intensity: 100,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b5/Tehran_skyline_may_2007.jpg/800px-Tehran_skyline_may_2007.jpg',
+ imageCaption: '테헤란 시가지 전경 (Wikimedia Commons)',
+ },
+ {
+ id: 'us2', timestamp: T0 - 11 * HOUR,
+ lat: 35.7018, lng: 51.4223, type: 'airstrike', source: 'US',
+ label: '테헤란 — IRGC 본부 (말렉아슈타르)',
+ description: 'IRGC 말렉아슈타르 건물, 순항미사일 공격으로 완전 파괴.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Sarallah_Base.jpg/800px-Sarallah_Base.jpg',
+ imageCaption: 'IRGC 사령부 (Wikimedia Commons)',
+ },
+ {
+ id: 'us3', timestamp: T0 - 10.5 * HOUR,
+ lat: 35.7150, lng: 51.3900, type: 'airstrike', source: 'US',
+ label: '테헤란 — 대통령 관저 타격',
+ description: '페제시키안 대통령 집무실 타격. 건물 심각한 손상.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/Sa%27dAbad_Palace.jpg/800px-Sa%27dAbad_Palace.jpg',
+ imageCaption: '테헤란 대통령 관저 (Wikimedia Commons)',
+ },
+ {
+ id: 'us4', timestamp: T0 - 10 * HOUR,
+ lat: 35.5162, lng: 51.7621, type: 'airstrike', source: 'US',
+ label: '파르친 — 미사일 생산시설',
+ description: '파르친 군사단지(테헤란 남동 40km) 탄도미사일 고체연료 혼합동 3개 파괴.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Parchin_Possible_Location_of_Large_Explosion_Containment_Vessel.jpg/800px-Parchin_Possible_Location_of_Large_Explosion_Containment_Vessel.jpg',
+ imageCaption: '파르친 군사단지 위성사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'us5', timestamp: T0 - 9.5 * HOUR,
+ lat: 35.7384, lng: 51.5778, type: 'airstrike', source: 'US',
+ label: '호지르 — 지하 터널 단지',
+ description: '호지르 군사기지(테헤란 동쪽 20km) 공습. 지하 미사일 생산 터널 타격.',
+ intensity: 90,
+ },
+ {
+ id: 'us6', timestamp: T0 - 9 * HOUR,
+ lat: 35.8400, lng: 50.9391, type: 'airstrike', source: 'US',
+ label: '카라즈 — 군수산업 시설',
+ description: '카라즈 군수산업 시설 타격. 방공 레이더 파괴.',
+ intensity: 80,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Karaj_panorama.jpg/800px-Karaj_panorama.jpg',
+ imageCaption: '카라즈 시 전경 (Wikimedia Commons)',
+ },
+
+ // ─── 03:00–06:00 UTC: 핵시설 및 전략시설 타격 ───
+ {
+ id: 'us7', timestamp: T0 - 9 * HOUR,
+ lat: 33.7219, lng: 51.7276, type: 'airstrike', source: 'US',
+ label: '나탄즈 — 핵시설 타격',
+ description: '미국 벙커버스터 폭탄으로 나탄즈 우라늄 농축시설 타격. IAEA 피해 확인.',
+ intensity: 100,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Natanz_nuclear.jpg/800px-Natanz_nuclear.jpg',
+ imageCaption: '나탄즈 핵시설 위성사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'us8', timestamp: T0 - 8.5 * HOUR,
+ lat: 32.6546, lng: 51.6680, type: 'airstrike', source: 'US',
+ label: '이스파한 — 핵연구센터',
+ description: '이스파한 핵기술센터 타격. 위성 이미지로 심각한 피해 확인.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Isfahan_Nuclear_Facility.jpg/800px-Isfahan_Nuclear_Facility.jpg',
+ imageCaption: '이스파한 핵기술센터 (Wikimedia Commons)',
+ },
+ {
+ id: 'us9', timestamp: T0 - 8 * HOUR,
+ lat: 28.8297, lng: 50.8862, type: 'airstrike', source: 'US',
+ label: '부셰르 — 원자력발전소 인근',
+ description: '부셰르 원전 인근 타격. 이란은 원자로 무사 주장; 공항 터미널 파괴.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Bushehr_Nuclear_Power_Plant_aerial_view.jpg/800px-Bushehr_Nuclear_Power_Plant_aerial_view.jpg',
+ imageCaption: '부셰르 원자력발전소 항공사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'us10', timestamp: T0 - 7.5 * HOUR,
+ lat: 34.3460, lng: 47.1581, type: 'airstrike', source: 'US',
+ label: '케르만샤 — 공군기지',
+ description: '케르만샤 공군기지 타격. 다수 IRGC 공군 항공기 지상 파괴.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Kermanshah_city.jpg/800px-Kermanshah_city.jpg',
+ imageCaption: '케르만샤 시 전경 (Wikimedia Commons)',
+ },
+ {
+ id: 'us11', timestamp: T0 - 7 * HOUR,
+ lat: 34.6416, lng: 50.8746, type: 'airstrike', source: 'US',
+ label: '쿰 — 미사일 저장고',
+ description: 'IRGC 탄도미사일 저장 시설(쿰 인근) 파괴.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/09/Qom_from_above.jpg/800px-Qom_from_above.jpg',
+ imageCaption: '쿰 시 항공사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'us12', timestamp: T0 - 6.5 * HOUR,
+ lat: 38.1289, lng: 46.2350, type: 'airstrike', source: 'US',
+ label: '타브리즈 — 공군기지',
+ description: '타브리즈 샤히드 파쿠리 공군기지 타격. F-14 전투기 및 레이더 파괴.',
+ intensity: 80,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Tabriz_Panorama.jpg/800px-Tabriz_Panorama.jpg',
+ imageCaption: '타브리즈 시 전경 (Wikimedia Commons)',
+ },
+ {
+ id: 'us13', timestamp: T0 - 6 * HOUR,
+ lat: 36.0716, lng: 55.0164, type: 'airstrike', source: 'US',
+ label: '샤흐루드 — 우주/미사일 센터',
+ description: '샤흐루드 우주센터 타격. 미사일 시험 및 고체연료 모터 조립에 사용.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Shahroud_in_Semnan.jpg/800px-Shahroud_in_Semnan.jpg',
+ imageCaption: '샤흐루드 시 위치 (Wikimedia Commons)',
+ },
+ {
+ id: 'us14', timestamp: T0 - 5.5 * HOUR,
+ lat: 29.5392, lng: 52.5900, type: 'airstrike', source: 'US',
+ label: '시라즈 — 공군기지',
+ description: '시라즈 샤히드 다스트가이브 공군기지 피해. IRIAF Su-24 항공기 파괴.',
+ intensity: 80,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/7b/Shiraz_city.jpg/800px-Shiraz_city.jpg',
+ imageCaption: '시라즈 시 전경 (Wikimedia Commons)',
+ },
+ {
+ id: 'us15', timestamp: T0 - 5 * HOUR,
+ lat: 33.6374, lng: 46.4227, type: 'airstrike', source: 'US',
+ label: '일람 — 군사기지',
+ description: '일람 주(이라크 국경 인근) IRGC 지상군 기지 타격.',
+ intensity: 75,
+ },
+ {
+ id: 'us16', timestamp: T0 - 4.5 * HOUR,
+ lat: 35.7100, lng: 52.0700, type: 'airstrike', source: 'US',
+ label: '다마반드 — 미사일 기지',
+ description: '테헤란 동쪽 다마반드 지하 미사일 기지, 벙커버스터로 타격.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Mount_Damavand_in_winter.jpg/800px-Mount_Damavand_in_winter.jpg',
+ imageCaption: '다마반드 산 전경 (Wikimedia Commons)',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 1-B단계: 이스라엘 독자 공습 (IAF F-35I Adir 편대)
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'il1', timestamp: T0 - 10 * HOUR,
+ lat: 33.7219, lng: 51.7276, type: 'airstrike', source: 'IL',
+ label: '나탄즈 — 이스라엘 F-35I 공습',
+ description: 'IAF F-35I 아디르 편대, 나탄즈 핵시설 지하 원심분리기실 정밀타격. GBU-28 벙커버스터 사용.',
+ intensity: 100,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/F-35I_%22Adir%22_%2842%29_%28cropped%29.jpg/800px-F-35I_%22Adir%22_%2842%29_%28cropped%29.jpg',
+ imageCaption: 'IAF F-35I 아디르 전투기 (Wikimedia Commons)',
+ },
+ {
+ id: 'il2', timestamp: T0 - 9 * HOUR,
+ lat: 32.6546, lng: 51.6680, type: 'airstrike', source: 'IL',
+ label: '이스파한 — 이스라엘 공습',
+ description: 'IAF 편대, 이스파한 핵기술센터 UCF(우라늄전환시설) 타격. 위성영상 확인.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Isfahan_Naqsh-e_Jahan_square.jpg/800px-Isfahan_Naqsh-e_Jahan_square.jpg',
+ imageCaption: '이스파한 시 전경 (Wikimedia Commons)',
+ },
+ {
+ id: 'il3', timestamp: T0 - 7 * HOUR,
+ lat: 34.3460, lng: 47.1581, type: 'airstrike', source: 'IL',
+ label: '케르만샤 — 이스라엘 F-15I 공습',
+ description: 'IAF F-15I 라암 편대, 케르만샤 IRGC 공군기지 무장해제 공습. 이라크 영공 경유.',
+ intensity: 85,
+ },
+ {
+ id: 'il4', timestamp: T0 - 5 * HOUR,
+ lat: 35.7384, lng: 51.5778, type: 'airstrike', source: 'IL',
+ label: '호지르 — 이스라엘 정밀타격',
+ description: 'IAF, 호지르 지하 미사일 생산터널 입구 정밀폭격. 고체연료 생산라인 파괴 추정.',
+ intensity: 90,
+ },
+ {
+ id: 'il5', timestamp: T0 - 3 * HOUR,
+ lat: 27.1832, lng: 56.2764, type: 'airstrike', source: 'IL',
+ label: '반다르아바스 — 이스라엘 해군기지 타격',
+ description: 'IAF, 반다르아바스 IRGC 해군기지 초계정 계류시설 및 미사일 저장고 타격.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Bandar_Abbas_Iran_port.jpg/800px-Bandar_Abbas_Iran_port.jpg',
+ imageCaption: '반다르아바스 항구 (Wikimedia Commons)',
+ },
+ {
+ id: 'il6', timestamp: T0 + 1 * HOUR,
+ lat: 34.6416, lng: 50.8746, type: 'airstrike', source: 'IL',
+ label: '쿰 — 이스라엘 2차 공습',
+ description: 'IAF F-35I, 쿰 인근 잔존 미사일 발사대 및 지하 사일로 2차 타격.',
+ intensity: 90,
+ },
+ {
+ id: 'il7', timestamp: T0 + 3 * HOUR,
+ lat: 36.0716, lng: 55.0164, type: 'airstrike', source: 'IL',
+ label: '샤흐루드 — 이스라엘 우주센터 타격',
+ description: 'IAF 장거리 공습, 샤흐루드 우주센터 미사일 시험시설 및 발사대 파괴.',
+ intensity: 80,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 2단계: 이란 경보 및 보복 준비
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'a1', timestamp: T0 - 11 * HOUR,
+ lat: 32.4279, lng: 53.6880, type: 'alert',
+ label: '이란 영공 폐쇄',
+ description: '이란 전 민간 영공 폐쇄. IRGC 전면 군사 동원 명령.',
+ },
+ {
+ id: 'a2', timestamp: T0 - 8 * HOUR,
+ lat: 26.5667, lng: 56.2500, type: 'alert',
+ label: '호르무즈 해협 봉쇄',
+ description: 'IRGC 해군, 호르무즈 해협 봉쇄 선언. 기뢰 배치. 유가 40% 급등.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Strait_of_Hormuz.jpg/800px-Strait_of_Hormuz.jpg',
+ imageCaption: '호르무즈 해협 위성사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'a3', timestamp: T0 - 6 * HOUR,
+ lat: 33.3152, lng: 44.3661, type: 'alert',
+ label: '이라크 — 미군기지 경보 격상',
+ description: '이라크 내 모든 미군기지 THREATCON DELTA 발령. 이라크 PMF 동원.',
+ },
+ {
+ id: 'a4', timestamp: T0 - 5 * HOUR,
+ lat: 25.1175, lng: 51.3150, type: 'alert',
+ label: '알우데이드 — DEFCON 2',
+ description: 'CENTCOM, 카타르 알우데이드 공군기지 방호태세 DEFCON 2 격상.',
+ },
+ {
+ id: 'a5', timestamp: T0 - 4 * HOUR,
+ lat: 26.2235, lng: 50.6009, type: 'alert',
+ label: '바레인 제5함대 — 경보',
+ description: '바레인 주파이르 미 해군 제5함대 사령부 최대 방호태세 발령.',
+ },
+ {
+ id: 'a6', timestamp: T0 - 3 * HOUR,
+ lat: 32.0853, lng: 34.7818, type: 'alert',
+ label: '이스라엘 — 전국 대피 명령',
+ description: 'IDF 후방사령부 전국 대피소 대피 명령. 애로우, 다윗의 투석기, 아이언돔 전면 가동.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 3단계: 이란 보복 발사 (T0-6h ~ T0)
+ // IRGC 주장: 첫 4일간 미사일 500발 + 드론 2,000대
+ // ═══════════════════════════════════════════════════════════════════
+
+ // ─── 드론 공격파 (느린 속도, 먼저 발사) ───
+ {
+ id: 'ir1', timestamp: T0 - 6 * HOUR,
+ lat: 29.9792, lng: 52.8906, type: 'missile_launch', source: 'IR',
+ label: '샤헤드-136 드론 1차파 — 파르스 주',
+ description: 'IRGC, 파르스 주에서 샤헤드-136 자폭드론 500대 이상 발사. 이스라엘 및 걸프 기지 향.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Shahed_136_Geran-2_drone.jpg/800px-Shahed_136_Geran-2_drone.jpg',
+ imageCaption: '샤헤드-136 자폭드론 (Wikimedia Commons)',
+ },
+ {
+ id: 'ir2', timestamp: T0 - 5.5 * HOUR,
+ lat: 34.3142, lng: 47.0650, type: 'missile_launch', source: 'IR',
+ label: '드론 2차파 — 케르만샤',
+ description: '케르만샤에서 추가 드론 편대 발사. 이라크/요르단 회랑 경유 이스라엘행.',
+ intensity: 85,
+ },
+
+ // ─── 순항미사일 ───
+ {
+ id: 'ir3', timestamp: T0 - 3 * HOUR,
+ lat: 33.7219, lng: 51.4215, type: 'missile_launch', source: 'IR',
+ label: '순항미사일 — 중부 이란',
+ description: 'IRGC, 중부 이란에서 수마르·호베이제 순항미사일 이스라엘 향 발사.',
+ intensity: 90,
+ },
+ {
+ id: 'ir4', timestamp: T0 - 2.5 * HOUR,
+ lat: 32.3256, lng: 48.6692, type: 'missile_launch', source: 'IR',
+ label: '순항미사일 — 후제스탄',
+ description: '후제스탄에서 추가 순항미사일 발사. 바레인 미 제5함대 겨냥.',
+ intensity: 85,
+ },
+
+ // ─── 탄도미사일 ───
+ {
+ id: 'ir5', timestamp: T0 - 90 * MIN,
+ lat: 27.1832, lng: 56.2764, type: 'missile_launch', source: 'IR',
+ label: '탄도미사일 — 반다르아바스',
+ description: '반다르아바스 일대에서 대규모 탄도미사일 발사. 에마드, 가드르, 세질 미사일 이스라엘행.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Emad_missile.jpg/800px-Emad_missile.jpg',
+ imageCaption: '이란 에마드 탄도미사일 (Wikimedia Commons)',
+ },
+ {
+ id: 'ir6', timestamp: T0 - 80 * MIN,
+ lat: 34.6416, lng: 50.8746, type: 'missile_launch', source: 'IR',
+ label: '탄도미사일 — 쿰',
+ description: 'IRGC, 쿰 인근 지하 사일로에서 파타 극초음속 미사일 발사.',
+ intensity: 95,
+ },
+ {
+ id: 'ir7', timestamp: T0 - 70 * MIN,
+ lat: 35.7100, lng: 52.0700, type: 'missile_launch', source: 'IR',
+ label: '탄도미사일 — 다마반드',
+ description: '다마반드 잔존 발사대에서 헤이바르 셰칸 중거리 탄도미사일 발사.',
+ intensity: 90,
+ },
+
+ // ─── 대리세력 발사 ───
+ {
+ id: 'ir8', timestamp: T0 - 80 * MIN,
+ lat: 33.3, lng: 44.4, type: 'missile_launch', source: 'proxy',
+ label: '이라크 이슬람저항세력 발사',
+ description: '친이란 PMF/카타이브 헤즈볼라, 바그다드 일대에서 미군기지 향 탄도미사일 발사.',
+ intensity: 80,
+ },
+ {
+ id: 'ir9', timestamp: T0 - 70 * MIN,
+ lat: 15.3694, lng: 44.1910, type: 'missile_launch', source: 'proxy',
+ label: '후티 반군 발사 — 사나',
+ description: '안사르 알라(후티), 사나에서 탄도미사일·사마드-3 드론 발사. 이스라엘 및 홍해 선박 겨냥.',
+ intensity: 85,
+ },
+ {
+ id: 'ir10', timestamp: T0 - 60 * MIN,
+ lat: 33.5138, lng: 36.2765, type: 'missile_launch', source: 'proxy',
+ label: '헤즈볼라 — 다마스쿠스 회랑',
+ description: '시리아 주둔 헤즈볼라 부대, 이스라엘 북부 향 추가 로켓 발사.',
+ intensity: 75,
+ },
+
+ // ─── IRGC 걸프국가 타격 (전례 없는 공격) ───
+ {
+ id: 'ir11', timestamp: T0 - 50 * MIN,
+ lat: 30.4000, lng: 49.0000, type: 'missile_launch', source: 'IR',
+ label: '걸프 미군기지 향 미사일 발사',
+ description: 'IRGC, 중동 전역 미군기지 27곳 및 전 GCC 국가 동시 미사일 발사.',
+ intensity: 95,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 4단계: 요격 작전
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'int1', timestamp: T0 - 40 * MIN,
+ lat: 32.0, lng: 36.5, type: 'intercept',
+ label: '요르단, 이란 드론 요격',
+ description: '요르단 공군 F-16, 요르단 영공 내 이란 드론 요격.',
+ },
+ {
+ id: 'int2', timestamp: T0 - 35 * MIN,
+ lat: 29.3467, lng: 47.5208, type: 'intercept',
+ label: '패트리어트 — 알리알살렘, 쿠웨이트',
+ description: '미군 패트리어트 PAC-3, 쿠웨이트 상공에서 탄도미사일 요격.',
+ },
+ {
+ id: 'int3', timestamp: T0 - 30 * MIN,
+ lat: 33.8, lng: 32.5, type: 'intercept',
+ label: 'USS 카니 (DDG-64) — 동지중해',
+ description: '미 해군 이지스 구축함, 동지중해 상공에서 다수 탄도미사일 요격.',
+ },
+ {
+ id: 'int4', timestamp: T0 - 28 * MIN,
+ lat: 26.0, lng: 56.5, type: 'intercept',
+ label: 'HMS 다이아몬드 — 페르시아만',
+ description: '영국 구축함 HMS 다이아몬드, 씨바이퍼 미사일로 페르시아만 상공 드론 격추.',
+ },
+ {
+ id: 'int5', timestamp: T0 - 25 * MIN,
+ lat: 24.2478, lng: 54.5475, type: 'intercept',
+ label: 'THAAD — 알다프라, UAE',
+ description: '미군 THAAD, 알다프라 공군기지에서 UAE 겨냥 중거리 탄도미사일 요격.',
+ },
+ {
+ id: 'int6', timestamp: T0 - 22 * MIN,
+ lat: 25.1175, lng: 51.3150, type: 'intercept',
+ label: '패트리어트 — 알우데이드, 카타르',
+ description: '패트리어트 포대, 알우데이드 공군기지 방어. 다수 요격 확인.',
+ },
+ {
+ id: 'int7', timestamp: T0 - 20 * MIN,
+ lat: 31.0461, lng: 34.8516, type: 'intercept',
+ label: '아이언돔 — 이스라엘 남부',
+ description: '아이언돔, 이스라엘 남부 전역에서 수백 대의 드론·로켓 요격.',
+ intensity: 90,
+ },
+ {
+ id: 'int8', timestamp: T0 - 15 * MIN,
+ lat: 32.0853, lng: 34.7818, type: 'intercept',
+ label: '다윗의 투석기 — 텔아비브',
+ description: '다윗의 투석기, 텔아비브 수도권 접근 순항미사일 요격.',
+ intensity: 85,
+ },
+ {
+ id: 'int9', timestamp: T0 - 10 * MIN,
+ lat: 29.5581, lng: 34.9482, type: 'intercept',
+ label: '애로우-3 — 대기권 밖 요격',
+ description: '애로우-3, 이스라엘 남부 상공 대기권 밖에서 탄도미사일 요격.',
+ intensity: 95,
+ },
+ {
+ id: 'int10', timestamp: T0 - 8 * MIN,
+ lat: 26.2235, lng: 50.6009, type: 'intercept',
+ label: '패트리어트 — 주파이르, 바레인',
+ description: '미군 패트리어트, 바레인 제5함대 사령부 방어. 다수 미사일 교전.',
+ intensity: 85,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 5단계: 피격 지점 — T0 이후
+ // 이란의 이스라엘, 걸프국가, 미군기지 타격
+ // ═══════════════════════════════════════════════════════════════════
+
+ // ─── 이스라엘 피격 ───
+ {
+ id: 'imp1', timestamp: T0,
+ lat: 31.2083, lng: 34.8622, type: 'impact', source: 'IR',
+ label: '네바팀 공군기지 — 피격',
+ description: '이란 파타 극초음속 미사일, 네바팀 공군기지 타격. 활주로 파손, F-35I 2대 손상. 주요 표적.',
+ intensity: 100,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/PikiWiki_Israel_32121_Nevatim_Airbase.JPG/800px-PikiWiki_Israel_32121_Nevatim_Airbase.JPG',
+ imageCaption: '네바팀 공군기지 (Wikimedia Commons)',
+ },
+ {
+ id: 'imp2', timestamp: T0 + 1 * MIN,
+ lat: 30.7761, lng: 34.6667, type: 'impact', source: 'IR',
+ label: '라몬 공군기지 — 피격',
+ description: '탄도미사일, 네게브 사막 라몬 공군기지 타격. 다수 강화 엄체호 피격.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Ramon_Airbase_Negev_Israel.jpg/800px-Ramon_Airbase_Negev_Israel.jpg',
+ imageCaption: '라몬 공군기지 위성사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'imp3', timestamp: T0 + 2 * MIN,
+ lat: 32.0853, lng: 34.7818, type: 'impact', source: 'IR',
+ label: '텔아비브 — 주거지역 피격',
+ description: '순항미사일, 바트얌/텔아비브 지역 방어망 관통. 9명 사망, 약 200명 부상. 건물 피해.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Tel_Aviv-Yafo_Skyline_%28cropped%29.jpg/800px-Tel_Aviv-Yafo_Skyline_%28cropped%29.jpg',
+ imageCaption: '텔아비브 스카이라인 (Wikimedia Commons)',
+ },
+ {
+ id: 'imp4', timestamp: T0 + 3 * MIN,
+ lat: 31.8940, lng: 34.8110, type: 'impact', source: 'IR',
+ label: '레호보트 — 쇼핑몰 피격',
+ description: '이란 탄도미사일, 레호보트 쇼핑센터 타격. 상당한 민간인 사상자.',
+ intensity: 85,
+ },
+ {
+ id: 'imp5', timestamp: T0 + 4 * MIN,
+ lat: 33.4167, lng: 35.8571, type: 'impact', source: 'IR',
+ label: '헤르몬산 정보기지 — 피격',
+ description: '미사일, 골란고원 헤르몬산 IDF 정보초소 타격.',
+ intensity: 75,
+ },
+ {
+ id: 'imp6', timestamp: T0 + 5 * MIN,
+ lat: 31.2589, lng: 35.2126, type: 'impact', source: 'IR',
+ label: '아라드 — 파편 낙하',
+ description: '아라드 지역에 미사일 파편 낙하. 다수 민간인 부상 보고.',
+ intensity: 60,
+ },
+ {
+ id: 'imp7', timestamp: T0 + 6 * MIN,
+ lat: 31.0667, lng: 35.0333, type: 'impact', source: 'IR',
+ label: '디모나 — 근접 피격 실패',
+ description: '디모나 원자력시설 겨냥 미사일 요격; 시설 주변 넓은 파편 분포.',
+ intensity: 70,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Dimona_Nuclear_Power_Plant.jpg/800px-Dimona_Nuclear_Power_Plant.jpg',
+ imageCaption: '디모나 원자력시설 (Wikimedia Commons)',
+ },
+ {
+ id: 'imp8', timestamp: T0 + 7 * MIN,
+ lat: 33.1, lng: 35.85, type: 'impact', source: 'proxy',
+ label: '골란고원 — 다수 피격',
+ description: '헤즈볼라 로켓·이란 미사일, 이스라엘 점령 골란고원 전역 타격.',
+ intensity: 65,
+ },
+
+ // ─── 바레인 피격 ───
+ {
+ id: 'imp9', timestamp: T0 + 3 * MIN,
+ lat: 26.2235, lng: 50.6009, type: 'impact', source: 'IR',
+ label: '바레인 — 제5함대 사령부 피격',
+ description: '이란 미사일, 바레인 주파이르 미 해군 제5함대 사령부 인근 타격. 미군 다수 사상.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/ce/US_Navy_040723-N-0000X-001_Aerial_view_of_the_U.S._Naval_Support_Activity_%28NSA%29_Bahrain.jpg/800px-US_Navy_040723-N-0000X-001_Aerial_view_of_the_U.S._Naval_Support_Activity_%28NSA%29_Bahrain.jpg',
+ imageCaption: '바레인 미 해군 지원시설 항공사진 (US Navy)',
+ },
+ {
+ id: 'imp10', timestamp: T0 + 5 * MIN,
+ lat: 26.2700, lng: 50.6336, type: 'impact', source: 'IR',
+ label: '바레인 공항 — 드론 피격',
+ description: '이란 드론, 바레인 국제공항 터미널 타격. 물적 피해, 운항 중단.',
+ intensity: 75,
+ },
+ {
+ id: 'imp11', timestamp: T0 + 8 * MIN,
+ lat: 26.2285, lng: 50.5500, type: 'impact', source: 'IR',
+ label: '마나마 — 주거 건물 피격',
+ description: '이란 드론, 바레인 마나마 주거용 타워 타격. 민간인 사상자 보고.',
+ intensity: 70,
+ },
+
+ // ─── 카타르 피격 ───
+ {
+ id: 'imp12', timestamp: T0 + 4 * MIN,
+ lat: 25.1175, lng: 51.3150, type: 'impact', source: 'IR',
+ label: '알우데이드 공군기지 — 피격',
+ description: '탄도미사일, 카타르 알우데이드 공군기지 패트리어트 방어 관통. 미군 3명 전사. CENTCOM CAOC 손상.',
+ intensity: 95,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Al_Udeid_Air_Base.jpg/800px-Al_Udeid_Air_Base.jpg',
+ imageCaption: '알우데이드 공군기지 항공사진 (US Air Force)',
+ },
+
+ // ─── UAE 피격 ───
+ {
+ id: 'imp13', timestamp: T0 + 6 * MIN,
+ lat: 24.2478, lng: 54.5475, type: 'impact', source: 'IR',
+ label: '알다프라 공군기지 — 근접 피격',
+ description: '요격된 미사일 파편, UAE 알다프라 공군기지 인근 낙하. 경미한 피해.',
+ intensity: 55,
+ },
+
+ // ─── 이라크 미군기지 피격 ───
+ {
+ id: 'imp14', timestamp: T0 + 5 * MIN,
+ lat: 33.7856, lng: 42.4441, type: 'impact', source: 'proxy',
+ label: '알아사드 공군기지, 이라크 — 피격',
+ description: 'PMF/카타이브 헤즈볼라 탄도미사일, 알아사드 공군기지 타격. 미군 사상자 발생.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/eb/Ain_al-Asad_base_2020.jpg/800px-Ain_al-Asad_base_2020.jpg',
+ imageCaption: '알아사드 공군기지 위성사진 (Wikimedia Commons)',
+ },
+ {
+ id: 'imp15', timestamp: T0 + 7 * MIN,
+ lat: 36.1911, lng: 44.0094, type: 'impact', source: 'IR',
+ label: '에르빌, 이라크 — 피격',
+ description: '미사일, 쿠르디스탄 에르빌 미 영사관 및 군사시설 인근 타격.',
+ intensity: 75,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Erbil_Citadel.jpg/800px-Erbil_Citadel.jpg',
+ imageCaption: '에르빌 시타델 (Wikimedia Commons)',
+ },
+
+ // ─── 튀르키예 피격 (인저를릭·쿠레지크) ───
+ {
+ id: 'imp-tr1', timestamp: T0 + 4 * MIN,
+ lat: 37.0017, lng: 35.4253, type: 'impact', source: 'IR',
+ label: '인저를릭 공군기지, 튀르키예 — 탄도미사일 피격',
+ description: 'IRGC 세질-3 탄도미사일 3발, 인저를릭(Incirlik) 공군기지 타격. 활주로 2개소 손상, 미군 F-16 격납고 파괴. NATO 즉각 비상.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Incirlik_flightline.jpg/800px-Incirlik_flightline.jpg',
+ imageCaption: '인저를릭 공군기지 (US Air Force)',
+ },
+ {
+ id: 'imp-tr2', timestamp: T0 + 6 * MIN,
+ lat: 37.8200, lng: 37.7500, type: 'impact', source: 'IR',
+ label: '쿠레지크 레이더기지, 튀르키예 — 미사일 피격',
+ description: 'IRGC 에마드 탄도미사일, 쿠레지크(Kürecik) AN/TPY-2 X밴드 레이더 기지 타격. 레이더 돔 파손, 미군 요원 부상.',
+ intensity: 85,
+ },
+ {
+ id: 'int-tr1', timestamp: T0 + 4 * MIN,
+ lat: 37.1000, lng: 35.5000, type: 'intercept',
+ label: '인저를릭 상공 — 패트리어트 요격',
+ description: '인저를릭 주둔 패트리어트 PAC-3, 이란 탄도미사일 2발 요격 성공. 1발 요격 실패로 활주로 피격.',
+ intensity: 80,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 6단계: 폭발 / 2차 피해
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'ex1', timestamp: T0 + 10 * MIN,
+ lat: 30.6500, lng: 34.7700, type: 'explosion',
+ label: '네게브 — 요격 파편 낙하',
+ description: '수백 발의 요격 미사일 파편이 네게브 사막 전역에 낙하.',
+ intensity: 50,
+ },
+ {
+ id: 'ex2', timestamp: T0 + 12 * MIN,
+ lat: 32.0, lng: 34.8, type: 'explosion',
+ label: '텔아비브 — 2차 화재',
+ description: '텔아비브 광역에서 미사일 타격·파편으로 인한 다수 화재 발생. 소방당국 역량 초과.',
+ intensity: 60,
+ },
+ {
+ id: 'ex3', timestamp: T0 + 15 * MIN,
+ lat: 31.9522, lng: 34.8070, type: 'explosion',
+ label: '바트얌 — 건물 붕괴',
+ description: '바트얌 61개 건물 피해. 시장, 다수 구조물 붕괴 보고.',
+ intensity: 70,
+ },
+ {
+ id: 'ex4', timestamp: T0 + 20 * MIN,
+ lat: 31.8630, lng: 34.8210, type: 'explosion',
+ label: '키르얏 에크론 — 쇼핑몰 화재',
+ description: '키르얏 에크론 쇼핑몰, 미사일 타격 후 대형 화재.',
+ intensity: 65,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 7단계: 미국-이스라엘 이란 추가 공습 (T0 이후)
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'us17', timestamp: T0 + 30 * MIN,
+ lat: 35.7450, lng: 51.4650, type: 'airstrike', source: 'US',
+ label: '테헤란 라비잔 — IRGC 지휘소 추가 타격',
+ description: '보복 B-2 공습, 테헤란 북동부 라비잔 IRGC 지휘통제 거점 파괴.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/B-2_Spirit_%28cropped%29.jpg/800px-B-2_Spirit_%28cropped%29.jpg',
+ imageCaption: 'B-2 스피릿 스텔스 폭격기 (US Air Force)',
+ },
+ {
+ id: 'us18', timestamp: T0 + 1 * HOUR,
+ lat: 32.7500, lng: 51.8617, type: 'airstrike', source: 'IL',
+ label: '이스파한 — 제8전술공군기지',
+ description: '이스라엘 F-35I, 이스파한 공군기지(제8TAB) 타격. IRIAF 항공기 활주로 위 파괴.',
+ intensity: 85,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/Isfahan_Naqsh-e_Jahan_square.jpg/800px-Isfahan_Naqsh-e_Jahan_square.jpg',
+ imageCaption: '이스파한 시 (Wikimedia Commons)',
+ },
+ {
+ id: 'us19', timestamp: T0 + 1.5 * HOUR,
+ lat: 37.3500, lng: 49.6000, type: 'airstrike', source: 'US',
+ label: '라슈트 — IRGC 해군기지',
+ description: '카스피해 연안 라슈트 IRGC 해군기지 타격.',
+ intensity: 75,
+ },
+ {
+ id: 'us20', timestamp: T0 + 2 * HOUR,
+ lat: 27.1832, lng: 56.2764, type: 'airstrike', source: 'US',
+ label: '반다르아바스 — 해군/미사일 시설',
+ description: 'B-2 및 F-35, 반다르아바스 IRGC 해군 자산 및 탄도미사일 발사대 타격.',
+ intensity: 90,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f5/Bandar_Abbas_Iran_port.jpg/800px-Bandar_Abbas_Iran_port.jpg',
+ imageCaption: '반다르아바스 항구 (Wikimedia Commons)',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 8단계: 미국의 후티 반군 타격 (예멘)
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'ym1', timestamp: T0 + 2 * HOUR,
+ lat: 15.3694, lng: 44.1910, type: 'airstrike', source: 'US',
+ label: '사나 — 후티 군사본부',
+ description: '미국 토마호크, 사나 후티 군사본부 및 미사일 저장시설 타격.',
+ intensity: 80,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Sanaa_HDR_%286834654486%29.jpg/800px-Sanaa_HDR_%286834654486%29.jpg',
+ imageCaption: '사나 구시가지 전경 (Wikimedia Commons)',
+ },
+ {
+ id: 'ym2', timestamp: T0 + 2.5 * HOUR,
+ lat: 14.7980, lng: 42.9540, type: 'airstrike', source: 'US',
+ label: '호데이다 — 항구/군사시설',
+ description: '미-영 합동, 호데이다 항구 후티 군사시설 타격.',
+ intensity: 80,
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Al_Hudaydah_Port.jpg/800px-Al_Hudaydah_Port.jpg',
+ imageCaption: '호데이다 항구 (Wikimedia Commons)',
+ },
+ {
+ id: 'ym3', timestamp: T0 + 3 * HOUR,
+ lat: 16.9400, lng: 43.7700, type: 'airstrike', source: 'US',
+ label: '사아다 — 후티 발사기지',
+ description: '사아다 주 후티 드론·미사일 발사기지 타격.',
+ intensity: 75,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 9단계: 미국의 이라크 민병대 타격
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'iq1', timestamp: T0 + 1.5 * HOUR,
+ lat: 34.4500, lng: 40.9200, type: 'airstrike', source: 'US',
+ label: '데이르에조르 — 친이란 민병대, 시리아',
+ description: '미군, 시리아 동부 데이르에조르 이란 지원 민병대 거점 공습.',
+ intensity: 75,
+ },
+ {
+ id: 'iq2', timestamp: T0 + 2 * HOUR,
+ lat: 34.4600, lng: 40.5400, type: 'airstrike', source: 'US',
+ label: '알부카말 — 국경지대 타격',
+ description: '시리아-이라크 국경 알부카말, 카타이브 헤즈볼라 무기고 정밀타격.',
+ intensity: 80,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // 10단계: 피해 평가 및 외교적 대응
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'p1', timestamp: T0 + 30 * MIN,
+ lat: 31.7683, lng: 35.2137, type: 'alert',
+ label: 'IDF 피해 평가 개시',
+ description: 'IDF, 이스라엘 11명 사망 확인. 네바팀 공군기지 피해 있으나 부분 운용 가능.',
+ },
+ {
+ id: 'p2', timestamp: T0 + 1 * HOUR,
+ lat: 26.2235, lng: 50.6009, type: 'alert',
+ label: '바레인 사상자 보고',
+ description: '바레인 정부, 이란 드론/미사일 공격으로 마나마 민간인 사상자 확인.',
+ },
+ {
+ id: 'p3', timestamp: T0 + 2 * HOUR,
+ lat: 38.8977, lng: -77.0365, type: 'alert',
+ label: '백악관 성명',
+ description: '트럼프: "이란은 매우 큰 대가를 치를 것." 알우데이드 미군 3명 전사, 추가 가능성 확인.',
+ },
+ {
+ id: 'p4', timestamp: T0 + 3 * HOUR,
+ lat: 48.8566, lng: 2.3522, type: 'alert',
+ label: 'UN 안보리 긴급회의',
+ description: 'UN 안전보장이사회 이란 사태 긴급회의 개최. 러시아·중국 결의안 거부.',
+ },
+ {
+ id: 'p5', timestamp: T0 + 4 * HOUR,
+ lat: 50.8503, lng: 4.3517, type: 'alert',
+ label: 'NATO 제5조 협의',
+ description: 'NATO 회원국, 이란의 인저를릭(튀르키예)·바레인 공격 관련 제5조 적용 논의. 튀르키예 NATO 지원 요청.',
+ },
+ {
+ id: 'p5-tr', timestamp: T0 + 4.5 * HOUR,
+ lat: 39.9334, lng: 32.8597, type: 'alert',
+ label: '튀르키예 앙카라 — 이란 대사 초치',
+ description: '튀르키예 외무부, 이란 대사 초치. 인저를릭·쿠레지크 공격 규탄. 에르도안 "주권 침해 용납 불가" 성명.',
+ },
+ {
+ id: 'p6', timestamp: T0 + 5 * HOUR,
+ lat: 35.6550, lng: 51.4250, type: 'alert',
+ label: 'IRGC — 미군기지 27곳 타격 주장',
+ description: 'IRGC 대변인, 미군기지 27곳 공격 주장. 첫 4일간 미사일 500발, 드론 2,000대 발사 보고.',
+ },
+ {
+ id: 'p7', timestamp: T0 + 6 * HOUR,
+ lat: 24.7136, lng: 46.6753, type: 'alert',
+ label: '사우디아라비아 — 영공 폐쇄',
+ description: '사우디, 영공 폐쇄. 동부 주 상공에서 이란 미사일 요격 보고.',
+ },
+ {
+ id: 'p8', timestamp: T0 + 8 * HOUR,
+ lat: 25.2048, lng: 55.2708, type: 'alert',
+ label: '두바이 — 팜 주메이라 대피',
+ description: 'UAE, 인근 알다프라 공군기지 향 이란 미사일 위협으로 두바이 해안가 일부 대피.',
+ },
+ {
+ id: 'p9', timestamp: T0 + 10 * HOUR,
+ lat: 51.5074, lng: -0.1278, type: 'alert',
+ label: '영국 의회 긴급토론',
+ description: '영국 하원 이란 사태 긴급토론. 총리, HMS 다이아몬드 적대 표적 교전 확인.',
+ },
+ {
+ id: 'p10', timestamp: T0 + 11 * HOUR,
+ lat: 37.5665, lng: 126.9780, type: 'alert',
+ label: '한국 — 청해부대 최고경보',
+ description: '해군 청해부대(아덴만) 최고 경계태세 격상. 걸프 지역 한국 국민 대피 명령.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 2 — 3월 2일: 이란 2차 보복파 + 미국 대규모 추가 공습
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd2-ir1', timestamp: T0 + 1 * DAY,
+ lat: 27.1832, lng: 56.2764, type: 'missile_launch', source: 'IR',
+ label: '이란 2차 탄도미사일 발사 — 반다르아바스',
+ description: 'IRGC, 잔존 이동식 발사대에서 세질-2 탄도미사일 추가 발사. UAE/바레인 겨냥.',
+ intensity: 90,
+ },
+ {
+ id: 'd2-ir2', timestamp: T0 + 1 * DAY + 2 * HOUR,
+ lat: 26.5667, lng: 56.2500, type: 'alert', source: 'IR',
+ label: '호르무즈 해협 — 기뢰 추가 배치',
+ description: 'IRGC 해군, 호르무즈 해협 해저 기뢰 추가 배치. 국제유가 배럴당 $185 돌파.',
+ intensity: 95,
+ },
+ {
+ id: 'd2-us1', timestamp: T0 + 1 * DAY + 4 * HOUR,
+ lat: 35.6750, lng: 51.3500, type: 'airstrike', source: 'US',
+ label: '테헤란 서부 — 통신인프라 타격',
+ description: 'B-2 폭격기, 테헤란 서부 IRGC 통신센터·방송국 타격. 이란 내부 통신 대부분 두절.',
+ intensity: 85,
+ },
+ {
+ id: 'd2-us2', timestamp: T0 + 1 * DAY + 6 * HOUR,
+ lat: 30.2744, lng: 56.9511, type: 'airstrike', source: 'US',
+ label: '케르만 — 미사일 생산시설 2차 타격',
+ description: 'F-35 편대, 케르만 IRGC 미사일 고체연료 제조공장 타격.',
+ intensity: 80,
+ },
+ {
+ id: 'd2-imp1', timestamp: T0 + 1 * DAY + 1 * HOUR,
+ lat: 24.2478, lng: 54.5475, type: 'impact', source: 'IR',
+ label: '알다프라 AFB — 활주로 피격',
+ description: '이란 탄도미사일, UAE 알다프라 공군기지 활주로 타격. F-35A 1대 손상, 미군 2명 부상.',
+ intensity: 85,
+ },
+ {
+ id: 'd2-imp2', timestamp: T0 + 1 * DAY + 1.5 * HOUR,
+ lat: 26.1572, lng: 50.5911, type: 'impact', source: 'IR',
+ label: '이사 AFB, 바레인 — 2차 피격',
+ description: '순항미사일, 바레인 이사 공군기지 격납고 타격. 미군 헬기 2대 파괴.',
+ intensity: 80,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 3 — 3월 3일: 호르무즈 해전 + 유조선 피격
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd3-sea1', timestamp: T0 + 2 * DAY,
+ lat: 26.5500, lng: 56.3500, type: 'explosion',
+ label: '호르무즈 해협 — 유조선 기뢰 접촉',
+ description: '그리스 국적 VLCC "아테나 글로리", 호르무즈 해협에서 기뢰 접촉. 원유 유출 시작.',
+ intensity: 85,
+ },
+ {
+ id: 'd3-sea2', timestamp: T0 + 2 * DAY + 3 * HOUR,
+ lat: 26.4000, lng: 56.4000, type: 'explosion',
+ label: 'IRGC 고속정 — 미 구축함 교전',
+ description: 'IRGC 고속정 편대, USN 알레이버크급 구축함 접근. 함포 교전 — IRGC 고속정 4척 격침.',
+ intensity: 90,
+ },
+ {
+ id: 'd3-us1', timestamp: T0 + 2 * DAY + 5 * HOUR,
+ lat: 26.9400, lng: 56.8500, type: 'airstrike', source: 'US',
+ label: '자스크 — IRGC 해군기지 타격',
+ description: 'F/A-18, 자스크 IRGC 해군기지 초계정 및 대함미사일 저장고 파괴.',
+ intensity: 85,
+ },
+ {
+ id: 'd3-kr1', timestamp: T0 + 2 * DAY + 8 * HOUR,
+ lat: 25.5000, lng: 57.0000, type: 'alert',
+ label: '한국 선박 — 오만만 대피 중',
+ description: '호르무즈 해협 인근 한국 국적 선박 12척, 청해부대 호위 하 오만만으로 긴급 대피.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 4 — 3월 4일: 이란 3차 공격파 + 헤즈볼라 전면전
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd4-ir1', timestamp: T0 + 3 * DAY,
+ lat: 33.8547, lng: 35.8623, type: 'missile_launch', source: 'proxy',
+ label: '헤즈볼라 — 전면 로켓 공격',
+ description: '헤즈볼라, 레바논 남부에서 이스라엘 북부 향 로켓 수백 발 발사. 하이파·키르얏슈모나 타격.',
+ intensity: 90,
+ },
+ {
+ id: 'd4-imp1', timestamp: T0 + 3 * DAY + 20 * MIN,
+ lat: 32.7940, lng: 34.9896, type: 'impact', source: 'proxy',
+ label: '하이파 — 로켓 다수 피격',
+ description: '헤즈볼라 로켓, 하이파 항구 및 정유시설 인근 타격. 대형 화재 발생.',
+ intensity: 85,
+ },
+ {
+ id: 'd4-il1', timestamp: T0 + 3 * DAY + 2 * HOUR,
+ lat: 33.8547, lng: 35.5018, type: 'airstrike', source: 'IL',
+ label: '레바논 남부 — IAF 대규모 공습',
+ description: 'IAF, 레바논 남부 헤즈볼라 거점 200개소 이상 동시 공습.',
+ intensity: 90,
+ },
+ {
+ id: 'd4-ir2', timestamp: T0 + 3 * DAY + 6 * HOUR,
+ lat: 35.5161, lng: 51.7621, type: 'missile_launch', source: 'IR',
+ label: '이란 3차 미사일 발사 — 잔존 발사대',
+ description: 'IRGC, 이동식 발사대에서 파타 극초음속 미사일 추가 발사. 이스라엘 디모나 겨냥.',
+ intensity: 95,
+ },
+ {
+ id: 'd4-imp2', timestamp: T0 + 3 * DAY + 6.5 * HOUR,
+ lat: 31.0667, lng: 35.0333, type: 'impact', source: 'IR',
+ label: '디모나 — 핵시설 인근 피격',
+ description: '극초음속 미사일 1발, 디모나 원자력시설 2km 이내 착탄. 이스라엘 방사능 유출 부인.',
+ intensity: 100,
+ },
+
+ // ─── DAY 4: 튀르키예 2차 피격 + 보복 ───
+ {
+ id: 'd4-tr1', timestamp: T0 + 3 * DAY + 4 * HOUR,
+ lat: 37.0017, lng: 35.4253, type: 'impact', source: 'IR',
+ label: '인저를릭 공군기지 — 2차 미사일 공격',
+ description: 'IRGC 세질-2 탄도미사일 4발, 인저를릭 기지 2차 공격. 패트리어트 3발 요격, 1발 연료저장시설 타격. 대형 화재.',
+ intensity: 85,
+ },
+ {
+ id: 'd4-tr-int1', timestamp: T0 + 3 * DAY + 4 * HOUR,
+ lat: 37.0500, lng: 35.4500, type: 'intercept',
+ label: '인저를릭 상공 — PAC-3 요격 (3/4발)',
+ description: '패트리어트 PAC-3, 이란 탄도미사일 4발 중 3발 요격. 1발 관통하여 기지 내 연료저장시설 피격.',
+ intensity: 80,
+ },
+ {
+ id: 'd4-tr2', timestamp: T0 + 3 * DAY + 8 * HOUR,
+ lat: 37.7500, lng: 29.0900, type: 'impact', source: 'IR',
+ label: '데니즐리, 튀르키예 — 이란 드론 공격',
+ description: 'IRGC 샤헤드-136 자폭드론 6대, 튀르키예 서부 데니즐리 NATO 통신중계기지 공격. 3대 요격, 3대 명중. 시설 부분 파손.',
+ intensity: 70,
+ },
+ {
+ id: 'd4-us-tr1', timestamp: T0 + 3 * DAY + 10 * HOUR,
+ lat: 38.7000, lng: 43.4000, type: 'airstrike', source: 'US',
+ label: '이란-튀르키예 국경 — IRGC 발사대 타격',
+ description: 'F-15E(인저를릭 발진), 이란-튀르키예 국경 인근 IRGC 이동식 미사일 발사대 3기 파괴.',
+ intensity: 80,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 5 — 3월 5일: 사이버전 + 에너지 위기
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd5-cy1', timestamp: T0 + 4 * DAY,
+ lat: 35.6300, lng: 51.3600, type: 'alert',
+ label: '이란 — 대규모 사이버 공격 감행',
+ description: '이란 APT 그룹, 이스라엘 전력망·수자원 시스템 사이버 공격. 텔아비브 일대 정전.',
+ },
+ {
+ id: 'd5-oil1', timestamp: T0 + 4 * DAY + 4 * HOUR,
+ lat: 26.3000, lng: 50.2000, type: 'explosion',
+ label: '사우디 라스타누라 — 드론 공격',
+ description: '후티/이란 연계 드론, 사우디 라스타누라 석유터미널 공격. WTI 유가 $200 돌파.',
+ intensity: 90,
+ },
+ {
+ id: 'd5-p1', timestamp: T0 + 4 * DAY + 8 * HOUR,
+ lat: 39.9042, lng: 116.4074, type: 'alert',
+ label: '중국 — 호르무즈 해협 개방 촉구',
+ description: '시진핑, 유엔 총회 긴급연설. 호르무즈 해협 봉쇄 해제 및 즉각 휴전 촉구.',
+ },
+ {
+ id: 'd5-kr2', timestamp: T0 + 4 * DAY + 10 * HOUR,
+ lat: 37.5665, lng: 126.9780, type: 'alert',
+ label: '한국 — 비상경제대책 발동',
+ description: '정부, 유류비 급등 대응 비상경제대책 발동. 전략비축유 방출 개시. 걸프 한국 교민 1,200명 대피 완료.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 6 — 3월 6일: 미 항모전단 전투 + 이란 해군 교전
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd6-nav1', timestamp: T0 + 5 * DAY,
+ lat: 25.0000, lng: 58.0000, type: 'explosion',
+ label: 'USS 아이젠하워 — 대함미사일 교전',
+ description: 'IRGC, 아라비아해 USS 아이젠하워 항모전단 향 누르 대함미사일 발사. 이지스 시스템 전탄 요격.',
+ intensity: 95,
+ },
+ {
+ id: 'd6-us1', timestamp: T0 + 5 * DAY + 2 * HOUR,
+ lat: 27.1500, lng: 56.2000, type: 'airstrike', source: 'US',
+ label: '반다르아바스 — 이란 해군 잔존 전력 파괴',
+ description: 'B-1B 편대, 반다르아바스 IRGC 해군 잔존 전력(잠수정 4척, 고속정 12척) 완전 파괴.',
+ intensity: 90,
+ },
+ {
+ id: 'd6-ir1', timestamp: T0 + 5 * DAY + 6 * HOUR,
+ lat: 29.9792, lng: 52.8906, type: 'missile_launch', source: 'IR',
+ label: '샤헤드 드론 4차파 — 파르스',
+ description: 'IRGC, 잔존 드론 300대 추가 발사. 이스라엘·사우디·튀르키예 걸프 인프라 겨냥.',
+ intensity: 80,
+ },
+ {
+ id: 'd6-tr1', timestamp: T0 + 5 * DAY + 8 * HOUR,
+ lat: 39.9334, lng: 32.8597, type: 'alert',
+ label: '튀르키예 — NATO 제5조 공식 발동 요청',
+ description: '에르도안, 인저를릭·쿠레지크 반복 공격에 NATO 제5조 공식 발동 요청. NATO 긴급 회의 개최.',
+ },
+ {
+ id: 'd6-tr2', timestamp: T0 + 5 * DAY + 10 * HOUR,
+ lat: 37.0017, lng: 35.4253, type: 'intercept',
+ label: '인저를릭 — 드론 12대 요격',
+ description: '인저를릭 방공체계, 이란 샤헤드 드론 4차파 중 12대 요격. 기지 방공 완벽 방어.',
+ intensity: 75,
+ },
+ {
+ id: 'd6-us-tr1', timestamp: T0 + 5 * DAY + 12 * HOUR,
+ lat: 38.5000, lng: 44.5000, type: 'airstrike', source: 'US',
+ label: '이란 서북부 — 튀르키예 향 미사일 발사대 파괴',
+ description: 'F-16(인저를릭), 이란 서북부 타브리즈 인근 IRGC 이동식 발사대 5기 정밀타격. 튀르키예 향 미사일 위협 제거.',
+ intensity: 85,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 7 — 3월 7일: 이란 내부 동요 + 미국 최후통첩
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd7-p1', timestamp: T0 + 6 * DAY,
+ lat: 35.6980, lng: 51.3380, type: 'alert',
+ label: '테헤란 — 대규모 반전 시위',
+ description: '테헤란·이스파한·시라즈 시민 수십만 명 반전 시위. IRGC 시위대 향 발포 보도.',
+ },
+ {
+ id: 'd7-us1', timestamp: T0 + 6 * DAY + 4 * HOUR,
+ lat: 38.8977, lng: -77.0365, type: 'alert',
+ label: '트럼프 — 이란 최후통첩',
+ description: '트럼프: "48시간 내 모든 미사일 발사 중단하지 않으면 정권 교체 작전 개시."',
+ },
+ {
+ id: 'd7-ir1', timestamp: T0 + 6 * DAY + 8 * HOUR,
+ lat: 35.6700, lng: 51.4500, type: 'alert', source: 'IR',
+ label: 'IRGC — 최후통첩 거부',
+ description: 'IRGC 사령관: "미국 침략에 끝까지 저항. 전면전 불사." 추가 미사일 발사 위협.',
+ },
+ {
+ id: 'd7-us2', timestamp: T0 + 6 * DAY + 12 * HOUR,
+ lat: 36.0716, lng: 55.0164, type: 'airstrike', source: 'US',
+ label: '샤흐루드 — ICBM 시설 완전 파괴',
+ description: '대규모 공습, 샤흐루드 우주센터 ICBM 개발시설 완전 파괴. 이란 장거리 미사일 능력 무력화.',
+ intensity: 95,
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 8 — 3월 8일: 교착 + 인도적 위기
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd8-p1', timestamp: T0 + 7 * DAY,
+ lat: 46.2044, lng: 6.1432, type: 'alert',
+ label: '제네바 — 적십자 긴급 호소',
+ description: 'ICRC, 이란·이스라엘·바레인 민간인 피해 긴급 호소. 이란 내 의약품·식수 부족 심각.',
+ },
+ {
+ id: 'd8-ir1', timestamp: T0 + 7 * DAY + 4 * HOUR,
+ lat: 32.6546, lng: 51.6680, type: 'explosion',
+ label: '이스파한 — 잔존 핵물질 유출 우려',
+ description: 'IAEA, 이스파한 핵시설 피격 후 방사성 물질 유출 가능성 경고. 주민 대피 권고.',
+ intensity: 85,
+ },
+ {
+ id: 'd8-us1', timestamp: T0 + 7 * DAY + 8 * HOUR,
+ lat: 35.7200, lng: 51.4400, type: 'airstrike', source: 'US',
+ label: '테헤란 북부 — 정보부(VAJA) 본부 타격',
+ description: 'B-2, 테헤란 북부 VAJA(정보보안부) 본부 타격. 이란 정보 수집 능력 마비.',
+ intensity: 80,
+ },
+ {
+ id: 'd8-kr3', timestamp: T0 + 7 * DAY + 10 * HOUR,
+ lat: 25.2528, lng: 55.3644, type: 'alert',
+ label: '한국 교민 — 두바이 경유 철수',
+ description: '걸프 잔류 한국 교민 350명, 두바이 경유 군 수송기로 긴급 귀국. 청해부대 한국 선박 호위 지속.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 9 — 3월 9일 (오늘): 휴전 압력 + 이란 미사일 능력 소진
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd9-p1', timestamp: T0 + 8 * DAY,
+ lat: 55.7558, lng: 37.6173, type: 'alert',
+ label: '러시아 — 이란에 휴전 권고',
+ description: '푸틴, 이란 지도부에 비공식 휴전 수용 권고. 러시아, 추가 무기 지원 거부.',
+ },
+ {
+ id: 'd9-ir1', timestamp: T0 + 8 * DAY + 2 * HOUR,
+ lat: 35.6580, lng: 51.4100, type: 'alert', source: 'IR',
+ label: 'IRGC — 미사일 재고 소진 보도',
+ description: '미 정보기관 분석: 이란 탄도미사일 재고 80% 소진. 잔존 이동식 발사대 10기 이하.',
+ },
+ {
+ id: 'd9-us1', timestamp: T0 + 8 * DAY + 6 * HOUR,
+ lat: 33.7219, lng: 51.7276, type: 'airstrike', source: 'US',
+ label: '나탄즈 — 3차 벙커버스터 타격',
+ description: 'B-2, 나탄즈 지하 우라늄 농축시설 GBU-57 벙커버스터 3차 타격. 지하 터널 완전 붕괴.',
+ intensity: 100,
+ },
+ {
+ id: 'd9-p2', timestamp: T0 + 8 * DAY + 10 * HOUR,
+ lat: 48.8566, lng: 2.3522, type: 'alert',
+ label: 'UN — 72시간 휴전 결의안 채택',
+ description: 'UN 안보리, 72시간 인도적 휴전 결의안 채택 (찬성 13, 기권 2). 미국·이란 모두 입장 미정.',
+ },
+ {
+ id: 'd9-kr4', timestamp: T0 + 8 * DAY + 12 * HOUR,
+ lat: 37.5665, lng: 126.9780, type: 'alert',
+ label: '한국 — NSC 긴급회의',
+ description: 'NSC, 호르무즈 해협 봉쇄 장기화 대비 에너지 비상계획 수립. 석유 비축량 90일분 확인.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 10 — 3월 10일: 72시간 휴전 개시 직전 최후 공습
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd10-us1', timestamp: T0 + 9 * DAY,
+ lat: 32.6546, lng: 51.6680, type: 'airstrike', source: 'US',
+ label: '이스파한 — 핵시설 최종 타격',
+ description: 'B-2, 이스파한 핵기술센터 잔존 시설 GBU-57 최종 타격. IAEA 긴급 성명 발표.',
+ intensity: 95,
+ },
+ {
+ id: 'd10-il1', timestamp: T0 + 9 * DAY + 2 * HOUR,
+ lat: 33.8547, lng: 35.5018, type: 'airstrike', source: 'IL',
+ label: '레바논 남부 — 헤즈볼라 지휘부 타격',
+ description: 'IAF F-35I, 레바논 남부 헤즈볼라 고위 지휘관 거점 정밀폭격. 2명 사살 보도.',
+ intensity: 85,
+ },
+ {
+ id: 'd10-ir1', timestamp: T0 + 9 * DAY + 4 * HOUR,
+ lat: 29.9792, lng: 52.8906, type: 'missile_launch', source: 'IR',
+ label: '이란 잔존 드론 5차파 — 최후 발사',
+ description: 'IRGC, 파르스 주 잔존 샤헤드-136 드론 80대 발사. 이란 측 "최후 보복" 선언.',
+ intensity: 70,
+ },
+ {
+ id: 'd10-imp1', timestamp: T0 + 9 * DAY + 5 * HOUR,
+ lat: 31.2083, lng: 34.8622, type: 'impact', source: 'IR',
+ label: '네바팀 공군기지 — 2차 드론 피격',
+ description: '샤헤드 드론 12대 아이언돔 관통, 네바팀 기지 활주로 추가 피격. 복구 중 피해.',
+ intensity: 75,
+ },
+ {
+ id: 'd10-int1', timestamp: T0 + 9 * DAY + 5 * HOUR,
+ lat: 29.5581, lng: 34.9482, type: 'intercept',
+ label: '애로우-3 — 잔여 탄도미사일 요격',
+ description: '애로우-3, 이란 잔존 발사대에서 발사된 세질-2 탄도미사일 3발 대기권 외 요격.',
+ intensity: 85,
+ },
+ {
+ id: 'd10-us2', timestamp: T0 + 9 * DAY + 8 * HOUR,
+ lat: 35.5161, lng: 51.7621, type: 'airstrike', source: 'US',
+ label: '파르친 — 잔존 미사일 시설 최종 파괴',
+ description: 'F-35A 편대, 파르친 군사단지 잔존 고체연료 저장시설 최종 타격. 대형 2차 폭발.',
+ intensity: 90,
+ },
+ {
+ id: 'd10-p1', timestamp: T0 + 9 * DAY + 10 * HOUR,
+ lat: 35.6820, lng: 51.3200, type: 'alert', source: 'IR',
+ label: '이란 — 72시간 휴전 조건부 수용',
+ description: '이란 외무부, UN 72시간 휴전 결의안 "조건부 수용" 발표. 조건: 이스라엘 핵시설 공습 중단.',
+ },
+ {
+ id: 'd10-p2', timestamp: T0 + 9 * DAY + 12 * HOUR,
+ lat: 38.8977, lng: -77.0365, type: 'alert',
+ label: '트럼프 — 휴전 조건 거부',
+ description: '트럼프: "이란의 조건부 휴전은 받아들일 수 없다. 무조건 항복만이 답이다."',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 11 — 3월 11일: 이란 군사력 사실상 궤멸 + 비공식 교전 중단
+ // ═══════════════════════════════════════════════════════════════════
+ {
+ id: 'd11-us1', timestamp: T0 + 10 * DAY,
+ lat: 35.8400, lng: 50.9391, type: 'airstrike', source: 'US',
+ label: '카라즈 — IRGC 잔존 지휘소 타격',
+ description: 'B-2, 카라즈 지하 IRGC 비상지휘소 벙커버스터 타격. IRGC 통신 체계 사실상 마비.',
+ intensity: 90,
+ },
+ {
+ id: 'd11-il1', timestamp: T0 + 10 * DAY + 2 * HOUR,
+ lat: 34.6416, lng: 50.8746, type: 'airstrike', source: 'IL',
+ label: '쿰 — 이스라엘 3차 공습',
+ description: 'IAF F-35I, 쿰 인근 잔존 지하 사일로 3차 타격. 이란 중거리 탄도미사일 능력 완전 무력화.',
+ intensity: 85,
+ },
+ {
+ id: 'd11-ir1', timestamp: T0 + 10 * DAY + 4 * HOUR,
+ lat: 26.5667, lng: 56.2500, type: 'explosion',
+ label: '호르무즈 해협 — 기뢰 자폭 사고',
+ description: 'IRGC 배치 해저기뢰 3개 자체 폭발. 이란 해군 소해정 1척 침몰. 해협 부분 개방 가능성.',
+ intensity: 70,
+ },
+ {
+ id: 'd11-us2', timestamp: T0 + 10 * DAY + 6 * HOUR,
+ lat: 27.1832, lng: 56.2764, type: 'airstrike', source: 'US',
+ label: '반다르아바스 — IRGC 해군 최종 소탕',
+ description: 'F/A-18·B-1B 합동, 반다르아바스 잔존 IRGC 해군 자산(소해정 2척, 미사일정 5척) 최종 파괴.',
+ intensity: 85,
+ },
+ {
+ id: 'd11-imp1', timestamp: T0 + 10 * DAY + 5 * HOUR,
+ lat: 26.2235, lng: 50.6009, type: 'impact', source: 'IR',
+ label: '바레인 — 잔존 순항미사일 피격',
+ description: '이란 잔존 호베이제 순항미사일, 바레인 주파이르 미 해군시설 인근 재차 피격. 미군 1명 부상.',
+ intensity: 65,
+ },
+ {
+ id: 'd11-p1', timestamp: T0 + 10 * DAY + 8 * HOUR,
+ lat: 35.7100, lng: 51.3700, type: 'alert',
+ label: 'IRGC — 비공식 교전 중단 보도',
+ description: '미 정보당국: IRGC 잔존 미사일 재고 5% 이하. 사실상 교전 능력 소진. 비공식 교전 중단 징후.',
+ },
+ {
+ id: 'd11-p2', timestamp: T0 + 10 * DAY + 10 * HOUR,
+ lat: 48.8566, lng: 2.3522, type: 'alert',
+ label: 'UN — 휴전 연장 결의안 논의',
+ description: 'UN 안보리, 72시간 휴전 연장 및 영구적 정전 합의 결의안 논의 개시. 러시아·중국 "조건부 찬성".',
+ },
+ {
+ id: 'd11-kr1', timestamp: T0 + 10 * DAY + 12 * HOUR,
+ lat: 37.5665, lng: 126.9780, type: 'alert',
+ label: '한국 — 유류비 안정화 조치 발표',
+ description: '정부, 유류세 한시 인하 + 전략비축유 2차 방출 발표. WTI $175로 소폭 하락. 호르무즈 부분 개방 기대.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 12 — 3월 12일: 해상 전투 격화 + 유조선 추가 피격 + 소규모 교전
+ // ═══════════════════════════════════════════════════════════════════
+
+ // ─── 선박/해상 공격 ───
+ {
+ id: 'd12-sea1', timestamp: T0 + 11 * DAY,
+ lat: 26.3500, lng: 56.5000, type: 'explosion',
+ label: '호르무즈 해협 — 일본 유조선 기뢰 피격',
+ description: '일본 국적 VLCC "쇼와 마루" 호르무즈 해협 동측 항로에서 기뢰 접촉. 선체 파공, 원유 유출. 승조원 전원 구조.',
+ intensity: 80,
+ },
+ {
+ id: 'd12-sea2', timestamp: T0 + 11 * DAY + 2 * HOUR,
+ lat: 26.2000, lng: 56.6000, type: 'explosion',
+ label: '오만만 — 한국 LNG선 드론 피격',
+ description: '한국행 LNG 운반선 "SK 이노베이션호" IRGC 자폭드론 2대 피격. 선체 경미 손상, 화물 무사. 청해부대 호위 전환.',
+ intensity: 75,
+ },
+ {
+ id: 'd12-sea3', timestamp: T0 + 11 * DAY + 3 * HOUR,
+ lat: 25.8000, lng: 56.8000, type: 'impact', source: 'IR',
+ label: '오만만 — 그리스 컨테이너선 피격',
+ description: 'IRGC 누르 대함미사일, 그리스 국적 컨테이너선 "아테네 익스프레스" 타격. 화재 발생, 승조원 2명 부상.',
+ intensity: 70,
+ },
+ {
+ id: 'd12-sea4', timestamp: T0 + 11 * DAY + 5 * HOUR,
+ lat: 26.6000, lng: 56.1000, type: 'explosion',
+ label: '호르무즈 해협 — IRGC 고속정 미 해군 교전',
+ description: 'IRGC 고속정 6척, USN 알레이버크급 구축함 USS 마이클 머피 접근. 함포·CIWS 교전 — 고속정 5척 격침.',
+ intensity: 85,
+ },
+
+ // ─── 공습 ───
+ {
+ id: 'd12-us1', timestamp: T0 + 11 * DAY + 4 * HOUR,
+ lat: 26.9400, lng: 56.8500, type: 'airstrike', source: 'US',
+ label: '자스크 — IRGC 대함미사일 진지 파괴',
+ description: 'F/A-18E, 자스크 해안 IRGC 누르 대함미사일 발사진지 4개소 정밀타격. 호르무즈 해상 위협 제거.',
+ intensity: 85,
+ },
+ {
+ id: 'd12-us2', timestamp: T0 + 11 * DAY + 6 * HOUR,
+ lat: 25.4000, lng: 57.8000, type: 'airstrike', source: 'US',
+ label: '차바하르 — IRGC 해군기지 타격',
+ description: 'B-1B, 차바하르 IRGC 해군기지 잠수정 계류시설 및 무기고 파괴. 이란 남동부 해군 전력 무력화.',
+ intensity: 80,
+ },
+ {
+ id: 'd12-il1', timestamp: T0 + 11 * DAY + 7 * HOUR,
+ lat: 33.5138, lng: 36.2765, type: 'airstrike', source: 'IL',
+ label: '다마스쿠스 — 이란 무기 수송 차량 타격',
+ description: 'IAF, 다마스쿠스 인근 이란→헤즈볼라 무기 수송 차량 행렬 정밀타격. 차량 8대 파괴.',
+ intensity: 75,
+ },
+
+ // ─── 요격 ───
+ {
+ id: 'd12-int1', timestamp: T0 + 11 * DAY + 3 * HOUR,
+ lat: 24.2478, lng: 54.5475, type: 'intercept',
+ label: 'THAAD — UAE 상공 미사일 요격',
+ description: 'THAAD, 알다프라 공군기지 방어. 이란 잔존 세질-2 미사일 2발 고고도 요격 성공.',
+ intensity: 80,
+ },
+
+ // ─── 피격/피해 ───
+ {
+ id: 'd12-imp1', timestamp: T0 + 11 * DAY + 8 * HOUR,
+ lat: 29.3467, lng: 47.5208, type: 'impact', source: 'IR',
+ label: '쿠웨이트 — 알리알살렘 기지 드론 피격',
+ description: '샤헤드 드론 3대, 쿠웨이트 알리알살렘 공군기지 격납고 타격. 미군 MQ-9 리퍼 1대 파괴.',
+ intensity: 65,
+ },
+
+ // ─── 외교/상황 ───
+ {
+ id: 'd12-p1', timestamp: T0 + 11 * DAY + 9 * HOUR,
+ lat: 26.5667, lng: 56.2500, type: 'alert',
+ label: '호르무즈 해협 — 미 해군 소해 작전 개시',
+ description: '미 해군 제5함대, 호르무즈 해협 기뢰 소해 작전 개시. 영국·프랑스 해군 합류. 상선 통항 일부 재개 목표.',
+ },
+ {
+ id: 'd12-p2', timestamp: T0 + 11 * DAY + 10 * HOUR,
+ lat: 35.6650, lng: 51.4800, type: 'alert', source: 'IR',
+ label: '이란 — IRGC 해군 사령관 사망 확인',
+ description: '이란 국영 IRNA, IRGC 해군 사령관 알리레자 탕시리 제독 반다르아바스 공습 중 사망 확인. 후임 미지정.',
+ },
+ {
+ id: 'd12-kr1', timestamp: T0 + 11 * DAY + 11 * HOUR,
+ lat: 37.5665, lng: 126.9780, type: 'alert',
+ label: '한국 — 한국행 LNG선 피격 관련 긴급 NSC',
+ description: 'NSC 긴급소집, SK 이노베이션호 피격 대응 논의. 청해부대 호르무즈 해협 진입 금지 명령. 대체 항로 검토.',
+ },
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DAY 12 후반 — 3월 12일 오후~밤: 호르무즈 해협 전면 소탕전
+ // ═══════════════════════════════════════════════════════════════════
+
+ // ─── 호르무즈 해협 기뢰 소해 + 해안 포대 제압 ───
+ {
+ id: 'd12-us3', timestamp: T0 + 11 * DAY + 12 * HOUR,
+ lat: 26.5200, lng: 56.3000, type: 'airstrike', source: 'US',
+ label: '호르무즈 해협 — MH-53E 기뢰 소해 작전',
+ description: '미 해군 MH-53E 소해헬기 편대, 호르무즈 해협 주항로 기뢰 14기 제거. 상선 제한적 통항 재개.',
+ intensity: 70,
+ },
+ {
+ id: 'd12-us4', timestamp: T0 + 11 * DAY + 13 * HOUR,
+ lat: 27.1800, lng: 56.2800, type: 'airstrike', source: 'US',
+ label: '반다르아바스 — IRGC 해안 대함미사일 기지 파괴',
+ description: 'B-2 스피릿, 반다르아바스 서측 IRGC C-802 대함미사일 발사대 6기 및 지하 탄약고 정밀폭격.',
+ intensity: 90,
+ },
+ {
+ id: 'd12-us5', timestamp: T0 + 11 * DAY + 14 * HOUR,
+ lat: 26.9800, lng: 56.0800, type: 'airstrike', source: 'US',
+ label: '게쉼섬 — IRGC 고속정 기지 파괴',
+ description: 'F/A-18F, 게쉼섬 IRGC 고속정 은신처 12개소 타격. 고속정 20여 척 파괴/대파.',
+ intensity: 85,
+ },
+ {
+ id: 'd12-us6', timestamp: T0 + 11 * DAY + 15 * HOUR,
+ lat: 26.6700, lng: 55.9000, type: 'airstrike', source: 'US',
+ label: '라라크섬 — IRGC 감시레이더 파괴',
+ description: '토마호크 순항미사일 4발, 라라크섬 IRGC 해상감시레이더 및 통신중계시설 파괴.',
+ intensity: 75,
+ },
+
+ // ─── 해상 전투 ───
+ {
+ id: 'd12-sea5', timestamp: T0 + 11 * DAY + 14 * HOUR,
+ lat: 26.4500, lng: 56.3500, type: 'explosion',
+ label: '호르무즈 해협 — IRGC 기뢰부설정 자폭',
+ description: 'IRGC 소해정 1척, 자체 배치한 기뢰에 접촉 폭발. 승조원 12명 사망 추정. 이란측 기뢰전 역량 추가 손실.',
+ intensity: 70,
+ },
+ {
+ id: 'd12-sea6', timestamp: T0 + 11 * DAY + 16 * HOUR,
+ lat: 26.3000, lng: 56.7000, type: 'explosion',
+ label: '오만만 — 파나마 벌크선 기뢰 피격',
+ description: '파나마 국적 벌크선 "Pacific Pioneer" 오만만 북부에서 부유기뢰 접촉. 선수 파공, 느린 침수. 오만 해군 구조.',
+ intensity: 65,
+ },
+
+ // ─── 인접국가 관련 공습 ───
+ {
+ id: 'd12-il2', timestamp: T0 + 11 * DAY + 13 * HOUR,
+ lat: 33.8900, lng: 35.5000, type: 'airstrike', source: 'IL',
+ label: '베이루트 남부 — 헤즈볼라 미사일 저장소 타격',
+ description: 'IAF F-35I, 베이루트 남부 다히에 지구 헤즈볼라 지하 미사일 저장시설 정밀타격. 2차 폭발 발생.',
+ intensity: 80,
+ },
+ {
+ id: 'd12-us7', timestamp: T0 + 11 * DAY + 15 * HOUR,
+ lat: 34.8000, lng: 36.7000, type: 'airstrike', source: 'US',
+ label: '시리아 홈스 — 이란 무기 이송 거점 파괴',
+ description: 'B-1B, 시리아 홈스 인근 이란 혁명수비대 무기 이송 허브 및 주변 방공진지 파괴.',
+ intensity: 75,
+ },
+ {
+ id: 'd12-us8', timestamp: T0 + 11 * DAY + 17 * HOUR,
+ lat: 33.3100, lng: 44.3700, type: 'airstrike', source: 'US',
+ label: '이라크 바그다드 — 친이란 민병대 거점 타격',
+ description: 'F-15E, 바그다드 남부 카타이브 헤즈볼라 무기고 2개소 정밀타격. 알아사드 기지 로켓공격 보복.',
+ intensity: 70,
+ },
+
+ // ─── 요격 ───
+ {
+ id: 'd12-int2', timestamp: T0 + 11 * DAY + 14 * HOUR,
+ lat: 26.2200, lng: 50.5500, type: 'intercept',
+ label: '바레인 — 패트리어트 미사일 요격',
+ description: '바레인 주둔 미군 패트리어트, IRGC 잔존 단거리 탄도미사일 1발 요격. 미 5함대 사령부 방어.',
+ intensity: 75,
+ },
+ {
+ id: 'd12-int3', timestamp: T0 + 11 * DAY + 16 * HOUR,
+ lat: 29.0000, lng: 48.0000, type: 'intercept',
+ label: '쿠웨이트 — SM-6 순항미사일 요격',
+ description: 'USS 할시 (DDG-97), 쿠웨이트 해상에서 이란발 순항미사일 2발 SM-6로 요격.',
+ intensity: 70,
+ },
+
+ // ─── 이란 보복 ───
+ {
+ id: 'd12-ir1', timestamp: T0 + 11 * DAY + 16 * HOUR,
+ lat: 25.2500, lng: 55.3600, type: 'impact', source: 'IR',
+ label: 'UAE 두바이 — 제벨알리 항구 드론 공격',
+ description: 'IRGC 샤헤드-136 자폭드론 5대, 제벨알리 자유무역항 컨테이너 야적장 타격. 화재 발생, 항구 일시 폐쇄.',
+ intensity: 80,
+ },
+ {
+ id: 'd12-ir2', timestamp: T0 + 11 * DAY + 18 * HOUR,
+ lat: 29.3800, lng: 47.9900, type: 'impact', source: 'IR',
+ label: '쿠웨이트 — 알자흐라 석유터미널 미사일 피격',
+ description: 'IRGC 파테-110 단거리 미사일, 쿠웨이트 알자흐라 석유 수출터미널 타격. 저장탱크 1기 화재. 원유 수출 중단.',
+ intensity: 75,
+ },
+
+ // ─── 상황/외교 ───
+ {
+ id: 'd12-p3', timestamp: T0 + 11 * DAY + 18 * HOUR,
+ lat: 26.5667, lng: 56.2500, type: 'alert',
+ label: '호르무즈 해협 — 미/영/불 연합 해상안전회랑 설정',
+ description: '미 5함대·영국 해군·프랑스 해군 합동, 호르무즈 해협 남측 안전항행 회랑 설정. 상선 호위 개시.',
+ },
+ {
+ id: 'd12-p4', timestamp: T0 + 11 * DAY + 20 * HOUR,
+ lat: 27.1800, lng: 56.2800, type: 'alert', source: 'IR',
+ label: '반다르아바스 — 이란 해군 항복적 후퇴',
+ description: 'IRGC 해군 잔여 함정, 반다르아바스 항 깊숙이 후퇴. 호르무즈 해협 수상 위협 사실상 종료. 기뢰 위협 지속.',
+ },
+ {
+ id: 'd12-p5', timestamp: T0 + 11 * DAY + 22 * HOUR,
+ lat: 40.7128, lng: -74.0060, type: 'alert',
+ label: 'UN 안보리 — 호르무즈 해협 긴급회의 소집',
+ description: 'UN 안보리, 호르무즈 해협 상선 피격 관련 긴급회의 소집. 중국·러시아 즉각 휴전 촉구, 미국 항행자유 강조.',
+ },
+];
+
+// 24시간 동안 10분 간격 센서 데이터 생성
+export function generateSensorData(): SensorLog[] {
+ const data: SensorLog[] = [];
+ const steps = (24 * 60) / 10; // 10분 간격
+
+ for (let i = 0; i <= steps; i++) {
+ const t = REPLAY_START + i * 10 * MIN;
+ const hoursFromT0 = (t - T0) / HOUR;
+ const distFromStrike = Math.abs(hoursFromT0);
+
+ // 지진파: 다중 스파이크 — 초기 미국 공습, T0 이란 타격
+ const seismicBase = 5 + Math.random() * 3;
+ const earlyStrikeSpike = hoursFromT0 < -8 && hoursFromT0 > -12
+ ? 30 + Math.random() * 15
+ : 0;
+ const mainSpike = distFromStrike < 0.5
+ ? 85 + Math.random() * 15
+ : distFromStrike < 1
+ ? 50 + Math.random() * 15
+ : distFromStrike < 2
+ ? 20 + Math.random() * 10
+ : 0;
+
+ // 기압: 양측 공습 시 하락
+ const pressureBase = 1013;
+ const pressureDrop = distFromStrike < 0.3
+ ? -20 - Math.random() * 10
+ : distFromStrike < 1
+ ? -8 - Math.random() * 5
+ : (hoursFromT0 < -8 && hoursFromT0 > -12)
+ ? -5 - Math.random() * 3
+ : Math.random() * 2 - 1;
+
+ // 소음: 분쟁 중 지속적 고수준
+ const noiseBase = 35 + Math.random() * 5;
+ const noiseSpike = distFromStrike < 0.5
+ ? 95 + Math.random() * 30
+ : distFromStrike < 1
+ ? 55 + Math.random() * 15
+ : distFromStrike < 3
+ ? 15 + Math.random() * 8
+ : (hoursFromT0 < -8 && hoursFromT0 > -12)
+ ? 25 + Math.random() * 10
+ : 0;
+
+ // 방사능: 핵시설 타격 후 상승
+ const radBase = 0.1 + Math.random() * 0.05;
+ const radSpike = hoursFromT0 > -8 && hoursFromT0 < 8
+ ? 0.08 + Math.random() * 0.05 // 나탄즈/부셰르 타격으로 상승
+ : 0;
+
+ data.push({
+ timestamp: t,
+ seismic: Math.min(100, seismicBase + earlyStrikeSpike + mainSpike),
+ airPressure: Math.round((pressureBase + pressureDrop) * 10) / 10,
+ noiseLevel: Math.min(140, Math.round(noiseBase + noiseSpike)),
+ radiationLevel: Math.round((radBase + radSpike) * 1000) / 1000,
+ });
+ }
+
+ return data;
+}
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..41c492b
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,9 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_SPG_API_KEY?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/src/hooks/useMonitor.ts b/src/hooks/useMonitor.ts
new file mode 100644
index 0000000..0cdebd7
--- /dev/null
+++ b/src/hooks/useMonitor.ts
@@ -0,0 +1,36 @@
+import { useState, useCallback, useRef, useEffect } from 'react';
+
+const TICK_INTERVAL = 1000; // update every 1 second in live mode
+
+export interface MonitorState {
+ currentTime: number; // always Date.now()
+ historyMinutes: number; // how far back to show (default 60)
+}
+
+export function useMonitor() {
+ const [state, setState] = useState
({
+ currentTime: Date.now(),
+ historyMinutes: 60,
+ });
+
+ const intervalRef = useRef(null);
+
+ // Start ticking immediately
+ useEffect(() => {
+ intervalRef.current = window.setInterval(() => {
+ setState(prev => ({ ...prev, currentTime: Date.now() }));
+ }, TICK_INTERVAL);
+ return () => {
+ if (intervalRef.current !== null) clearInterval(intervalRef.current);
+ };
+ }, []);
+
+ const setHistoryMinutes = useCallback((minutes: number) => {
+ setState(prev => ({ ...prev, historyMinutes: minutes }));
+ }, []);
+
+ const startTime = state.currentTime - state.historyMinutes * 60_000;
+ const endTime = state.currentTime;
+
+ return { state, startTime, endTime, setHistoryMinutes };
+}
diff --git a/src/hooks/useReplay.ts b/src/hooks/useReplay.ts
new file mode 100644
index 0000000..68c4ac6
--- /dev/null
+++ b/src/hooks/useReplay.ts
@@ -0,0 +1,85 @@
+import { useState, useCallback, useRef, useEffect } from 'react';
+import type { ReplayState } from '../types';
+import { REPLAY_START, REPLAY_END } from '../data/sampleData';
+
+const TICK_INTERVAL = 200; // ms between updates
+const TIME_STEP = 240_000; // 4 minutes of replay time per tick at 1x speed (same visual speed)
+
+export function useReplay() {
+ const [state, setState] = useState({
+ isPlaying: false,
+ currentTime: REPLAY_START,
+ startTime: REPLAY_START,
+ endTime: REPLAY_END,
+ speed: 1,
+ });
+
+ const intervalRef = useRef(null);
+
+ const stop = useCallback(() => {
+ if (intervalRef.current !== null) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ }, []);
+
+ const play = useCallback(() => {
+ setState(prev => ({ ...prev, isPlaying: true }));
+ }, []);
+
+ const pause = useCallback(() => {
+ stop();
+ setState(prev => ({ ...prev, isPlaying: false }));
+ }, [stop]);
+
+ const seek = useCallback((time: number) => {
+ setState(prev => ({
+ ...prev,
+ currentTime: Math.max(prev.startTime, Math.min(prev.endTime, time)),
+ }));
+ }, []);
+
+ const setSpeed = useCallback((speed: number) => {
+ setState(prev => ({ ...prev, speed }));
+ }, []);
+
+ const reset = useCallback(() => {
+ stop();
+ setState(prev => ({
+ ...prev,
+ isPlaying: false,
+ currentTime: prev.startTime,
+ }));
+ }, [stop]);
+
+ const setRange = useCallback((start: number, end: number) => {
+ stop();
+ setState(prev => ({
+ ...prev,
+ isPlaying: false,
+ startTime: start,
+ endTime: end,
+ currentTime: Math.max(start, Math.min(end, prev.currentTime)),
+ }));
+ }, [stop]);
+
+ useEffect(() => {
+ if (state.isPlaying) {
+ stop();
+ intervalRef.current = window.setInterval(() => {
+ setState(prev => {
+ const next = prev.currentTime + TIME_STEP * prev.speed;
+ if (next >= prev.endTime) {
+ return { ...prev, currentTime: prev.endTime, isPlaying: false };
+ }
+ return { ...prev, currentTime: next };
+ });
+ }, TICK_INTERVAL);
+ } else {
+ stop();
+ }
+ return stop;
+ }, [state.isPlaying, state.speed, stop]);
+
+ return { state, play, pause, seek, setSpeed, reset, setRange };
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..72b43e1
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,29 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --bg-primary: #0a0a1a;
+ --bg-secondary: #111127;
+ --bg-card: #1a1a2e;
+ --text-primary: #e0e0e0;
+ --text-secondary: #888;
+ --accent: #3b82f6;
+ --danger: #ef4444;
+ --warning: #eab308;
+}
+
+body {
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+
+#root {
+ width: 100%;
+ height: 100vh;
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/src/services/airplaneslive.ts b/src/services/airplaneslive.ts
new file mode 100644
index 0000000..81e4063
--- /dev/null
+++ b/src/services/airplaneslive.ts
@@ -0,0 +1,310 @@
+import type { Aircraft, AircraftCategory } from '../types';
+
+// Airplanes.live API - specializes in military aircraft tracking
+const ADSBX_BASE = 'https://api.airplanes.live/v2';
+
+// Known military type codes
+const MILITARY_TYPES: Record = {
+ 'F16': 'fighter', 'F15': 'fighter', 'F15E': 'fighter', 'FA18': 'fighter',
+ 'F22': 'fighter', 'F35': 'fighter', 'F14': 'fighter', 'EF2K': 'fighter',
+ 'RFAL': 'fighter', 'SU27': 'fighter', 'SU30': 'fighter', 'SU35': 'fighter',
+ 'KC10': 'tanker', 'KC30': 'tanker', 'KC46': 'tanker', 'K35R': 'tanker',
+ 'KC35': 'tanker', 'A332': 'tanker',
+ 'RC135': 'surveillance', 'E3': 'surveillance', 'E8': 'surveillance',
+ 'RQ4': 'surveillance', 'MQ9': 'surveillance', 'P8': 'surveillance',
+ 'EP3': 'surveillance', 'E6': 'surveillance', 'U2': 'surveillance',
+ 'C17': 'cargo', 'C5': 'cargo', 'C130': 'cargo', 'C2': 'cargo',
+};
+
+interface AirplanesLiveAc {
+ hex: string;
+ flight?: string;
+ r?: string; // registration (e.g. "A6-XWC")
+ lat?: number;
+ lon?: number;
+ alt_baro?: number | 'ground';
+ gs?: number;
+ track?: number;
+ baro_rate?: number;
+ t?: string; // aircraft type code (e.g. "A35K")
+ desc?: string; // type description (e.g. "AIRBUS A-350-1000")
+ ownOp?: string; // owner/operator
+ squawk?: string;
+ category?: string;
+ nav_heading?: number;
+ seen?: number;
+ seen_pos?: number;
+ dbFlags?: number;
+ emergency?: string;
+}
+
+function classifyFromType(type: string): AircraftCategory {
+ const t = type.toUpperCase();
+ for (const [code, cat] of Object.entries(MILITARY_TYPES)) {
+ if (t.includes(code)) return cat;
+ }
+ return 'civilian'; // 군용 타입이 아니면 민간기
+}
+
+function parseAirplanesLive(data: { ac?: AirplanesLiveAc[] }): Aircraft[] {
+ if (!data.ac) return [];
+
+ return data.ac
+ .filter(a => a.lat != null && a.lon != null)
+ .map(a => {
+ const typecode = a.t || '';
+ const isMilDb = (a.dbFlags ?? 0) & 1; // military flag in database
+ let category = classifyFromType(typecode);
+ if (category === 'civilian' && isMilDb) category = 'military';
+
+ return {
+ icao24: a.hex,
+ callsign: (a.flight || '').trim(),
+ lat: a.lat!,
+ lng: a.lon!,
+ altitude: a.alt_baro === 'ground' ? 0 : (a.alt_baro ?? 0) * 0.3048, // ft->m
+ velocity: (a.gs ?? 0) * 0.5144, // knots -> m/s
+ heading: a.track ?? a.nav_heading ?? 0,
+ verticalRate: (a.baro_rate ?? 0) * 0.00508, // fpm -> m/s
+ onGround: a.alt_baro === 'ground',
+ category,
+ typecode: typecode || undefined,
+ typeDesc: a.desc || undefined,
+ registration: a.r || undefined,
+ operator: a.ownOp || undefined,
+ squawk: a.squawk || undefined,
+ lastSeen: Date.now() - (a.seen ?? 0) * 1000,
+ };
+ });
+}
+
+export async function fetchMilitaryAircraft(): Promise {
+ try {
+ // Airplanes.live military endpoint - Middle East area
+ const url = `${ADSBX_BASE}/mil`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Airplanes.live ${res.status}`);
+ const data = await res.json();
+
+ // Filter to Middle East + surrounding region
+ return parseAirplanesLive(data).filter(
+ a => a.lat >= 12 && a.lat <= 42 && a.lng >= 25 && a.lng <= 68,
+ );
+ } catch (err) {
+ console.warn('Airplanes.live fetch failed:', err);
+ return []; // Will fallback to OpenSky sample data
+ }
+}
+
+// ═══ Korea region military aircraft ═══
+export async function fetchMilitaryAircraftKorea(): Promise {
+ try {
+ const url = `${ADSBX_BASE}/mil`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Airplanes.live mil ${res.status}`);
+ const data = await res.json();
+ return parseAirplanesLive(data).filter(
+ a => a.lat >= 15 && a.lat <= 50 && a.lng >= 110 && a.lng <= 150,
+ );
+ } catch (err) {
+ console.warn('Airplanes.live Korea mil failed:', err);
+ return [];
+ }
+}
+
+// Korea region queries for all aircraft
+const KR_QUERIES = [
+ { lat: 37.5, lon: 127, radius: 250 }, // 서울 / 수도권
+ { lat: 35, lon: 129, radius: 250 }, // 부산 / 경남
+ { lat: 33.5, lon: 126.5, radius: 200 }, // 제주
+ { lat: 36, lon: 127, radius: 250 }, // 충청 / 대전
+ { lat: 38.5, lon: 128, radius: 200 }, // 동해안 / 강원
+ { lat: 35.5, lon: 131, radius: 250 }, // 동해 / 울릉도
+ { lat: 34, lon: 124, radius: 200 }, // 서해 / 황해
+ { lat: 40, lon: 130, radius: 250 }, // 일본해 / 북방
+];
+
+const krLiveCache = new Map();
+let krInitialDone = false;
+let krQueryIdx = 0;
+let krInitPromise: Promise | null = null;
+
+async function doKrInitialLoad(): Promise {
+ console.log('Airplanes.live Korea: initial load...');
+ for (let i = 0; i < KR_QUERIES.length; i++) {
+ try {
+ if (i > 0) await delay(800);
+ const ac = await fetchOneRegion(KR_QUERIES[i]);
+ krLiveCache.set(`kr-${i}`, { ac, ts: Date.now() });
+ } catch { /* skip */ }
+ }
+ krInitialDone = true;
+ krInitPromise = null;
+}
+
+export async function fetchAllAircraftLiveKorea(): Promise {
+ const now = Date.now();
+
+ if (!krInitialDone) {
+ if (!krInitPromise) krInitPromise = doKrInitialLoad();
+ } else {
+ const toFetch: { idx: number; q: typeof KR_QUERIES[0] }[] = [];
+ for (let i = 0; i < 2; i++) {
+ const idx = (krQueryIdx + i) % KR_QUERIES.length;
+ const cached = krLiveCache.get(`kr-${idx}`);
+ if (!cached || now - cached.ts > CACHE_TTL) {
+ toFetch.push({ idx, q: KR_QUERIES[idx] });
+ }
+ }
+ krQueryIdx = (krQueryIdx + 2) % KR_QUERIES.length;
+
+ for (let i = 0; i < toFetch.length; i++) {
+ try {
+ if (i > 0) await delay(1200);
+ const ac = await fetchOneRegion(toFetch[i].q);
+ krLiveCache.set(`kr-${toFetch[i].idx}`, { ac, ts: Date.now() });
+ } catch { /* skip */ }
+ }
+ }
+
+ const seen = new Set();
+ const merged: Aircraft[] = [];
+ for (const { ac } of krLiveCache.values()) {
+ for (const a of ac) {
+ if (!seen.has(a.icao24)) { seen.add(a.icao24); merged.push(a); }
+ }
+ }
+ return merged;
+}
+
+// Fetch ALL aircraft (military + civilian) in Middle East using point/radius queries
+// Airplanes.live /v2/point/{lat}/{lon}/{radius_nm} — CORS *, no auth
+// Rate limit: ~1 req/5s — must query sequentially with delay
+
+const LIVE_QUERIES = [
+ // ── 이란 ──
+ { lat: 35.5, lon: 51.5, radius: 250 }, // 0: 테헤란 / 북부 이란
+ { lat: 30, lon: 52, radius: 250 }, // 1: 이란 남부 / 시라즈 / 부셰르
+ { lat: 33, lon: 57, radius: 250 }, // 2: 이란 동부 / 이스파한 → 마슈하드
+ // ── 이라크 / 시리아 ──
+ { lat: 33.5, lon: 44, radius: 250 }, // 3: 바그다드 / 이라크 중부
+ // ── 이스라엘 / 동지중해 ──
+ { lat: 33, lon: 36, radius: 250 }, // 4: 레바논 / 이스라엘 / 시리아
+ // ── 터키 남동부 ──
+ { lat: 38, lon: 40, radius: 250 }, // 5: 터키 SE / 인시를릭 AB
+ // ── 걸프 / UAE ──
+ { lat: 25, lon: 55, radius: 250 }, // 6: UAE / 오만 / 호르무즈 해협
+ // ── 사우디 ──
+ { lat: 26, lon: 44, radius: 250 }, // 7: 사우디 중부 / 리야드
+ // ── 예멘 / 홍해 ──
+ { lat: 16, lon: 44, radius: 250 }, // 8: 예멘 / 아덴만
+ // ── 아라비아해 ──
+ { lat: 22, lon: 62, radius: 250 }, // 9: 아라비아해 / 파키스탄 연안
+];
+
+// Accumulated aircraft cache — keeps all regions, refreshed per-region
+const liveCache = new Map();
+const CACHE_TTL = 60_000; // 60s per region cache
+let initialLoadDone = false;
+let queryIndex = 0;
+let initialLoadPromise: Promise | null = null;
+
+function delay(ms: number) {
+ return new Promise(r => setTimeout(r, ms));
+}
+
+async function fetchOneRegion(q: { lat: number; lon: number; radius: number }): Promise {
+ const url = `${ADSBX_BASE}/point/${q.lat}/${q.lon}/${q.radius}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Airplanes.live ${res.status}`);
+ const data = await res.json();
+ return parseAirplanesLive(data);
+}
+
+// Non-blocking initial load: fetch regions in background, return partial results immediately
+async function doInitialLoad(): Promise {
+ console.log('Airplanes.live: initial load — fetching 10 regions in background...');
+ for (let i = 0; i < LIVE_QUERIES.length; i++) {
+ try {
+ if (i > 0) await delay(800);
+ const ac = await fetchOneRegion(LIVE_QUERIES[i]);
+ liveCache.set(`${i}`, { ac, ts: Date.now() });
+ console.log(` Region ${i}: ${ac.length} aircraft`);
+ } catch (err) {
+ console.warn(` Region ${i} failed:`, err);
+ }
+ }
+ initialLoadDone = true;
+ initialLoadPromise = null;
+}
+
+export async function fetchAllAircraftLive(): Promise {
+ const now = Date.now();
+
+ if (!initialLoadDone) {
+ // Start background load if not started yet
+ if (!initialLoadPromise) {
+ initialLoadPromise = doInitialLoad();
+ }
+ // Don't block — return whatever we have so far
+ } else {
+ // ── 이후: 2개 지역씩 순환 갱신 (더 가볍게) ──
+ const toFetch: { idx: number; q: typeof LIVE_QUERIES[0] }[] = [];
+
+ for (let i = 0; i < 2; i++) {
+ const idx = (queryIndex + i) % LIVE_QUERIES.length;
+ const cached = liveCache.get(`${idx}`);
+ if (!cached || now - cached.ts > CACHE_TTL) {
+ toFetch.push({ idx, q: LIVE_QUERIES[idx] });
+ }
+ }
+ queryIndex = (queryIndex + 2) % LIVE_QUERIES.length;
+
+ for (let i = 0; i < toFetch.length; i++) {
+ try {
+ if (i > 0) await delay(1200);
+ const ac = await fetchOneRegion(toFetch[i].q);
+ liveCache.set(`${toFetch[i].idx}`, { ac, ts: Date.now() });
+ } catch (err) {
+ console.warn(`Region ${toFetch[i].idx} fetch failed:`, err);
+ }
+ }
+ }
+
+ // Merge all cached regions, deduplicate by icao24
+ const seen = new Set();
+ const merged: Aircraft[] = [];
+ for (const { ac } of liveCache.values()) {
+ for (const a of ac) {
+ if (!seen.has(a.icao24)) {
+ seen.add(a.icao24);
+ merged.push(a);
+ }
+ }
+ }
+ return merged;
+}
+
+export async function fetchByCallsign(callsign: string): Promise {
+ try {
+ const url = `${ADSBX_BASE}/callsign/${callsign}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Airplanes.live ${res.status}`);
+ const data = await res.json();
+ return parseAirplanesLive(data);
+ } catch {
+ return [];
+ }
+}
+
+export async function fetchByIcao(hex: string): Promise {
+ try {
+ const url = `${ADSBX_BASE}/hex/${hex}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Airplanes.live ${res.status}`);
+ const data = await res.json();
+ return parseAirplanesLive(data);
+ } catch {
+ return [];
+ }
+}
diff --git a/src/services/airports.ts b/src/services/airports.ts
new file mode 100644
index 0000000..f52a211
--- /dev/null
+++ b/src/services/airports.ts
@@ -0,0 +1,38 @@
+// ═══ Korean Airports Data ═══
+// International + Domestic merged into single markers
+
+export interface KoreanAirport {
+ id: string; // IATA code
+ icao: string;
+ name: string;
+ nameKo: string;
+ lat: number;
+ lng: number;
+ type: 'international' | 'domestic' | 'military';
+ intl: boolean; // has international flights
+ domestic: boolean; // has domestic flights
+}
+
+export const KOREAN_AIRPORTS: KoreanAirport[] = [
+ // ═══ 주요 국제공항 ═══
+ { id: 'ICN', icao: 'RKSI', name: 'Incheon Intl', nameKo: '인천국제공항', lat: 37.4602, lng: 126.4407, type: 'international', intl: true, domestic: true },
+ { id: 'GMP', icao: 'RKSS', name: 'Gimpo Intl', nameKo: '김포국제공항', lat: 37.5583, lng: 126.7906, type: 'international', intl: true, domestic: true },
+ { id: 'PUS', icao: 'RKPK', name: 'Gimhae Intl', nameKo: '김해국제공항', lat: 35.1795, lng: 128.9382, type: 'international', intl: true, domestic: true },
+ { id: 'CJU', icao: 'RKPC', name: 'Jeju Intl', nameKo: '제주국제공항', lat: 33.5113, lng: 126.4929, type: 'international', intl: true, domestic: true },
+ { id: 'TAE', icao: 'RKTN', name: 'Daegu Intl', nameKo: '대구국제공항', lat: 35.8941, lng: 128.6589, type: 'international', intl: true, domestic: true },
+ { id: 'CJJ', icao: 'RKTU', name: 'Cheongju Intl', nameKo: '청주국제공항', lat: 36.7166, lng: 127.4991, type: 'international', intl: true, domestic: true },
+ { id: 'MWX', icao: 'RKJB', name: 'Muan Intl', nameKo: '무안국제공항', lat: 34.9914, lng: 126.3828, type: 'international', intl: true, domestic: true },
+
+ // ═══ 국내선 공항 ═══
+ { id: 'KWJ', icao: 'RKJJ', name: 'Gwangju', nameKo: '광주공항', lat: 35.1264, lng: 126.8089, type: 'domestic', intl: false, domestic: true },
+ { id: 'RSU', icao: 'RKJY', name: 'Yeosu', nameKo: '여수공항', lat: 34.8423, lng: 127.6170, type: 'domestic', intl: false, domestic: true },
+ { id: 'USN', icao: 'RKPU', name: 'Ulsan', nameKo: '울산공항', lat: 35.5935, lng: 129.3519, type: 'domestic', intl: false, domestic: true },
+ { id: 'KPO', icao: 'RKTH', name: 'Pohang', nameKo: '포항공항', lat: 35.9878, lng: 129.4205, type: 'domestic', intl: false, domestic: true },
+ { id: 'HIN', icao: 'RKPS', name: 'Sacheon', nameKo: '사천공항', lat: 35.0886, lng: 128.0702, type: 'domestic', intl: false, domestic: true },
+ { id: 'WJU', icao: 'RKNW', name: 'Wonju', nameKo: '원주공항', lat: 37.4381, lng: 127.9604, type: 'domestic', intl: false, domestic: true },
+ { id: 'KUV', icao: 'RKJK', name: 'Gunsan', nameKo: '군산공항', lat: 35.9038, lng: 126.6158, type: 'domestic', intl: false, domestic: true },
+ { id: 'YNY', icao: 'RKNY', name: 'Yangyang Intl', nameKo: '양양국제공항', lat: 38.0613, lng: 128.6690, type: 'international', intl: true, domestic: true },
+
+ // ═══ 도서 공항 ═══
+ { id: 'JDG', icao: 'RKPD', name: 'Jeongseok (Ulleungdo)', nameKo: '울릉공항', lat: 37.5200, lng: 130.8980, type: 'domestic', intl: false, domestic: true },
+];
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000..d4b3048
--- /dev/null
+++ b/src/services/api.ts
@@ -0,0 +1,58 @@
+import type { GeoEvent, SensorLog, ApiConfig } from '../types';
+import { sampleEvents, generateSensorData } from '../data/sampleData';
+
+const defaultConfig: ApiConfig = {
+ eventsEndpoint: '/api/events',
+ sensorEndpoint: '/api/sensors',
+ pollIntervalMs: 30_000,
+};
+
+let cachedSensorData: SensorLog[] | null = null;
+
+export async function fetchEvents(_config?: Partial): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars
+ // In production, replace with actual API call:
+ // const res = await fetch(config.eventsEndpoint);
+ // return res.json();
+ return Promise.resolve(sampleEvents);
+}
+
+export async function fetchSensorData(_config?: Partial): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars
+ // In production, replace with actual API call:
+ // const res = await fetch(config.sensorEndpoint);
+ // return res.json();
+ if (!cachedSensorData) {
+ cachedSensorData = generateSensorData();
+ }
+ return Promise.resolve(cachedSensorData);
+}
+
+export function createPollingService(
+ onEvents: (events: GeoEvent[]) => void,
+ onSensors: (data: SensorLog[]) => void,
+ config: Partial = {},
+) {
+ const merged = { ...defaultConfig, ...config };
+ let intervalId: number | null = null;
+
+ const poll = async () => {
+ const [events, sensors] = await Promise.all([
+ fetchEvents(merged),
+ fetchSensorData(merged),
+ ]);
+ onEvents(events);
+ onSensors(sensors);
+ };
+
+ return {
+ start: () => {
+ poll();
+ intervalId = window.setInterval(poll, merged.pollIntervalMs);
+ },
+ stop: () => {
+ if (intervalId !== null) {
+ clearInterval(intervalId);
+ intervalId = null;
+ }
+ },
+ };
+}
diff --git a/src/services/cctv.ts b/src/services/cctv.ts
new file mode 100644
index 0000000..7bb3d1e
--- /dev/null
+++ b/src/services/cctv.ts
@@ -0,0 +1,43 @@
+// ═══ Korean Coastal CCTV Camera Data ═══
+// Source: 국립해양조사원 KHOA TAGO
+
+export interface CctvCamera {
+ id: number;
+ name: string;
+ region: '제주' | '남해' | '서해' | '동해';
+ lat: number;
+ lng: number;
+ type: 'tide' | 'fog';
+ url: string;
+ streamUrl: string;
+ source: 'KHOA';
+}
+
+/** KHOA HLS 스트림 */
+const KHOA_HLS = 'https://www.khoa.go.kr/SEAFOG/m4NiLawsC202gM5ixA7MPTYtO19KmV/hls/khoa';
+function khoa(site: string) { return `${KHOA_HLS}/${site}/s.m3u8`; }
+
+export const KOREA_CCTV_CAMERAS: CctvCamera[] = [
+ // ═══ 서해 (West Sea) ═══
+ { id: 1, name: '인천항 조위관측소', region: '서해', lat: 37.450382, lng: 126.593028, type: 'tide', url: 'https://www.badatime.com/cctv/29', streamUrl: khoa('Incheon'), source: 'KHOA' },
+ { id: 2, name: '인천항 해무관측', region: '서해', lat: 37.379626, lng: 126.615917, type: 'fog', url: 'https://www.badatime.com/cctv/30', streamUrl: khoa('SeaFog_Incheon'), source: 'KHOA' },
+ { id: 3, name: '대산항 해무관측', region: '서해', lat: 36.978758, lng: 126.304908, type: 'fog', url: 'https://www.badatime.com/cctv/31', streamUrl: khoa('SeaFog_Daesan'), source: 'KHOA' },
+ { id: 4, name: '평택당진항 해무관측', region: '서해', lat: 37.113185, lng: 126.39375, type: 'fog', url: 'https://www.badatime.com/cctv/32', streamUrl: khoa('SeaFog_PTDJ'), source: 'KHOA' },
+
+ // ═══ 남해 (South Sea) ═══
+ { id: 5, name: '목포항 해무관측', region: '남해', lat: 34.751831, lng: 126.310232, type: 'fog', url: 'https://www.badatime.com/cctv/35', streamUrl: khoa('SeaFog_Mokpo'), source: 'KHOA' },
+ { id: 6, name: '진도항 조위관측소', region: '남해', lat: 34.379541, lng: 126.307928, type: 'tide', url: 'https://www.badatime.com/cctv/36', streamUrl: khoa('Jindo'), source: 'KHOA' },
+ { id: 7, name: '여수항 해무관측', region: '남해', lat: 34.75386, lng: 127.752881, type: 'fog', url: 'https://www.badatime.com/cctv/37', streamUrl: khoa('SeaFog_Yeosu'), source: 'KHOA' },
+ { id: 8, name: '여수항 조위관측소', region: '남해', lat: 34.747005, lng: 127.766053, type: 'tide', url: 'https://www.badatime.com/cctv/38', streamUrl: khoa('Yeosu'), source: 'KHOA' },
+ { id: 9, name: '부산항 조위관측소', region: '남해', lat: 35.096329, lng: 129.034726, type: 'tide', url: 'https://www.badatime.com/cctv/39', streamUrl: khoa('Busan'), source: 'KHOA' },
+ { id: 10, name: '부산항 해무관측', region: '남해', lat: 35.077924, lng: 129.081469, type: 'fog', url: 'https://www.badatime.com/cctv/40', streamUrl: khoa('SeaFog_Busan'), source: 'KHOA' },
+ { id: 11, name: '해운대 해무관측', region: '남해', lat: 35.158148, lng: 129.158529, type: 'fog', url: 'https://www.badatime.com/cctv/41', streamUrl: khoa('SeaFog_Haeundae'), source: 'KHOA' },
+
+ // ═══ 동해 (East Sea) ═══
+ { id: 12, name: '울산항 해무관측', region: '동해', lat: 35.501902, lng: 129.387139, type: 'fog', url: 'https://www.badatime.com/cctv/42', streamUrl: khoa('SeaFog_Ulsan'), source: 'KHOA' },
+ { id: 13, name: '포항항 해무관측', region: '동해', lat: 36.051325, lng: 129.378492, type: 'fog', url: 'https://www.badatime.com/cctv/43', streamUrl: khoa('SeaFog_Pohang'), source: 'KHOA' },
+ { id: 14, name: '묵호항 조위관측소', region: '동해', lat: 37.550385, lng: 129.116396, type: 'tide', url: 'https://www.badatime.com/cctv/44', streamUrl: khoa('Mukho'), source: 'KHOA' },
+
+ // ═══ 제주 (Jeju) ═══
+ { id: 15, name: '모슬포항 조위관측소', region: '제주', lat: 33.213884, lng: 126.25051, type: 'tide', url: 'https://www.badatime.com/cctv/45', streamUrl: khoa('Moseulpo'), source: 'KHOA' },
+];
diff --git a/src/services/celestrak.ts b/src/services/celestrak.ts
new file mode 100644
index 0000000..2b68f0a
--- /dev/null
+++ b/src/services/celestrak.ts
@@ -0,0 +1,321 @@
+import * as satellite from 'satellite.js';
+import type { Satellite, SatellitePosition } from '../types';
+
+// CelesTrak TLE groups to fetch — relevant to Middle East theater
+const CELESTRAK_GROUPS: { group: string; category: Satellite['category'] }[] = [
+ { group: 'military', category: 'reconnaissance' },
+ { group: 'gps-ops', category: 'navigation' },
+ { group: 'geo', category: 'communications' },
+ { group: 'weather', category: 'weather' },
+ { group: 'stations', category: 'other' },
+];
+
+// Category override by satellite name keywords
+function refineSatCategory(name: string, defaultCat: Satellite['category']): Satellite['category'] {
+ const n = name.toUpperCase();
+ if (n.includes('SBIRS') || n.includes('NROL') || n.includes('USA') || n.includes('KEYHOLE') || n.includes('LACROSSE')) return 'reconnaissance';
+ if (n.includes('WGS') || n.includes('AEHF') || n.includes('MUOS') || n.includes('STARLINK') || n.includes('MILSTAR')) return 'communications';
+ if (n.includes('GPS') || n.includes('NAVSTAR') || n.includes('GALILEO') || n.includes('BEIDOU') || n.includes('GLONASS')) return 'navigation';
+ if (n.includes('GOES') || n.includes('METOP') || n.includes('NOAA') || n.includes('METEOR') || n.includes('DMSP')) return 'weather';
+ if (n.includes('ISS')) return 'other';
+ return defaultCat;
+}
+
+// Parse 3-line TLE format (name + line1 + line2)
+function parseTLE(text: string, defaultCategory: Satellite['category']): Satellite[] {
+ const lines = text.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0);
+ const sats: Satellite[] = [];
+
+ for (let i = 0; i < lines.length - 2; i++) {
+ // TLE line 1 starts with "1 ", line 2 starts with "2 "
+ if (lines[i + 1].startsWith('1 ') && lines[i + 2].startsWith('2 ')) {
+ const name = lines[i];
+ const tle1 = lines[i + 1];
+ const tle2 = lines[i + 2];
+
+ // Extract NORAD catalog number from line 1 (columns 3-7)
+ const noradId = parseInt(tle1.substring(2, 7).trim(), 10);
+ if (isNaN(noradId)) continue;
+
+ sats.push({
+ noradId,
+ name,
+ tle1,
+ tle2,
+ category: refineSatCategory(name, defaultCategory),
+ });
+
+ i += 2; // skip the 2 TLE lines
+ }
+ }
+
+ return sats;
+}
+
+// Middle East bounding box for filtering LEO satellites
+// Only keep satellites whose ground track passes near the region (lat 15-45, lon 25-65)
+function isNearMiddleEast(sat: Satellite): boolean {
+ try {
+ const satrec = satellite.twoline2satrec(sat.tle1, sat.tle2);
+ const now = new Date();
+ // Check current position and ±45min positions
+ for (const offsetMin of [0, -45, 45, -90, 90]) {
+ const t = new Date(now.getTime() + offsetMin * 60_000);
+ const pv = satellite.propagate(satrec, t);
+ if (!pv || typeof pv.position === 'boolean' || !pv.position) continue;
+ const gmst = satellite.gstime(t);
+ const geo = satellite.eciToGeodetic(pv.position, gmst);
+ const lat = satellite.degreesLat(geo.latitude);
+ const lng = satellite.degreesLong(geo.longitude);
+ // Generous bounding: lat -5 to 55, lon 15 to 75
+ if (lat >= -5 && lat <= 55 && lng >= 15 && lng <= 75) return true;
+ }
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+// Satellite cache — avoid re-fetching within 10 minutes
+let satCache: { sats: Satellite[]; ts: number } | null = null;
+const SAT_CACHE_TTL = 10 * 60_000;
+
+export async function fetchSatelliteTLE(): Promise {
+ // Return cache if fresh
+ if (satCache && Date.now() - satCache.ts < SAT_CACHE_TTL) {
+ return satCache.sats;
+ }
+
+ const allSats: Satellite[] = [];
+ const seenIds = new Set();
+
+ // Fetch TLE groups from CelesTrak sequentially (avoid hammering)
+ for (const { group, category } of CELESTRAK_GROUPS) {
+ try {
+ const url = `https://celestrak.org/NORAD/elements/gp.php?GROUP=${group}&FORMAT=tle`;
+ const res = await fetch(url);
+ if (!res.ok) {
+ console.warn(`CelesTrak ${group}: ${res.status}`);
+ continue;
+ }
+ const text = await res.text();
+ const parsed = parseTLE(text, category);
+ for (const sat of parsed) {
+ if (!seenIds.has(sat.noradId)) {
+ seenIds.add(sat.noradId);
+ allSats.push(sat);
+ }
+ }
+ } catch (err) {
+ console.warn(`CelesTrak ${group} fetch failed:`, err);
+ }
+ }
+
+ if (allSats.length === 0) {
+ console.warn('CelesTrak: no data fetched, using fallback');
+ return FALLBACK_SATELLITES;
+ }
+
+ // For GEO/MEO sats keep all, for LEO filter to Middle East region
+ const filtered: Satellite[] = [];
+ for (const sat of allSats) {
+ try {
+ const satrec = satellite.twoline2satrec(sat.tle1, sat.tle2);
+ const pv = satellite.propagate(satrec, new Date());
+ if (!pv || typeof pv.position === 'boolean' || !pv.position) continue;
+ const geo = satellite.eciToGeodetic(pv.position, satellite.gstime(new Date()));
+ const altKm = geo.height;
+
+ // GEO (>30000km) and MEO (>5000km): always include (they cover wide areas)
+ if (altKm > 5000) {
+ filtered.push(sat);
+ } else {
+ // LEO: only keep if passes near Middle East
+ if (isNearMiddleEast(sat)) {
+ filtered.push(sat);
+ }
+ }
+ } catch {
+ // skip bad TLE
+ }
+ }
+
+ // Cap at ~100 satellites to keep rendering performant
+ const capped = filtered.slice(0, 100);
+
+ satCache = { sats: capped, ts: Date.now() };
+ console.log(`CelesTrak: loaded ${capped.length} satellites (from ${allSats.length} total)`);
+ return capped;
+}
+
+// ═══ Korea region satellite fetch ═══
+function isNearKorea(sat: Satellite): boolean {
+ try {
+ const satrec = satellite.twoline2satrec(sat.tle1, sat.tle2);
+ const now = new Date();
+ for (const offsetMin of [0, -45, 45, -90, 90]) {
+ const t = new Date(now.getTime() + offsetMin * 60_000);
+ const pv = satellite.propagate(satrec, t);
+ if (!pv || typeof pv.position === 'boolean' || !pv.position) continue;
+ const gmst = satellite.gstime(t);
+ const geo = satellite.eciToGeodetic(pv.position, gmst);
+ const lat = satellite.degreesLat(geo.latitude);
+ const lng = satellite.degreesLong(geo.longitude);
+ if (lat >= 10 && lat <= 50 && lng >= 110 && lng <= 150) return true;
+ }
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+let satCacheKorea: { sats: Satellite[]; ts: number } | null = null;
+
+export async function fetchSatelliteTLEKorea(): Promise {
+ if (satCacheKorea && Date.now() - satCacheKorea.ts < SAT_CACHE_TTL) {
+ return satCacheKorea.sats;
+ }
+
+ const allSats: Satellite[] = [];
+ const seenIds = new Set();
+
+ for (const { group, category } of CELESTRAK_GROUPS) {
+ try {
+ const url = `https://celestrak.org/NORAD/elements/gp.php?GROUP=${group}&FORMAT=tle`;
+ const res = await fetch(url);
+ if (!res.ok) continue;
+ const text = await res.text();
+ const parsed = parseTLE(text, category);
+ for (const sat of parsed) {
+ if (!seenIds.has(sat.noradId)) {
+ seenIds.add(sat.noradId);
+ allSats.push(sat);
+ }
+ }
+ } catch { /* skip */ }
+ }
+
+ if (allSats.length === 0) return FALLBACK_SATELLITES;
+
+ const filtered: Satellite[] = [];
+ for (const sat of allSats) {
+ try {
+ const satrec = satellite.twoline2satrec(sat.tle1, sat.tle2);
+ const pv = satellite.propagate(satrec, new Date());
+ if (!pv || typeof pv.position === 'boolean' || !pv.position) continue;
+ const geo = satellite.eciToGeodetic(pv.position, satellite.gstime(new Date()));
+ const altKm = geo.height;
+ if (altKm > 5000) {
+ filtered.push(sat);
+ } else {
+ if (isNearKorea(sat)) filtered.push(sat);
+ }
+ } catch { /* skip */ }
+ }
+
+ const capped = filtered.slice(0, 100);
+ satCacheKorea = { sats: capped, ts: Date.now() };
+ console.log(`CelesTrak Korea: loaded ${capped.length} satellites`);
+ return capped;
+}
+
+// Fallback satellites if CelesTrak is unreachable
+const FALLBACK_SATELLITES: Satellite[] = [
+ { noradId: 25544, name: 'ISS (ZARYA)', category: 'other',
+ tle1: '1 25544U 98067A 26060.50000000 .00016717 00000-0 10270-3 0 9999',
+ tle2: '2 25544 51.6400 210.0000 0005000 350.0000 10.0000 15.49000000 10000' },
+ { noradId: 37481, name: 'SBIRS GEO-1', category: 'reconnaissance',
+ tle1: '1 37481U 11019A 26060.50000000 .00000010 00000-0 00000-0 0 9999',
+ tle2: '2 37481 3.5000 60.0000 0003000 90.0000 270.0000 1.00270000 10000' },
+ { noradId: 44478, name: 'WGS-10', category: 'communications',
+ tle1: '1 44478U 19060A 26060.50000000 .00000010 00000-0 00000-0 0 9999',
+ tle2: '2 44478 0.1000 60.0000 0002000 270.0000 90.0000 1.00270000 10000' },
+ { noradId: 55268, name: 'GPS III-06', category: 'navigation',
+ tle1: '1 55268U 23008A 26060.50000000 .00000010 00000-0 00000-0 0 9999',
+ tle2: '2 55268 55.0000 60.0000 0050000 100.0000 260.0000 2.00600000 10000' },
+ { noradId: 43689, name: 'MetOp-C', category: 'weather',
+ tle1: '1 43689U 18096A 26060.50000000 .00000400 00000-0 20000-3 0 9999',
+ tle2: '2 43689 98.7000 110.0000 0002000 90.0000 270.0000 14.21000000 10000' },
+];
+
+// Cache satrec objects (expensive to create)
+const satrecCache = new Map>();
+
+function getSatrec(sat: Satellite) {
+ let rec = satrecCache.get(sat.noradId);
+ if (!rec) {
+ rec = satellite.twoline2satrec(sat.tle1, sat.tle2);
+ satrecCache.set(sat.noradId, rec);
+ }
+ return rec;
+}
+
+// Cache ground tracks — only recompute every 60s
+const trackCache = new Map();
+const TRACK_CACHE_MS = 60_000;
+
+export function propagateSatellite(
+ sat: Satellite,
+ time: Date,
+ trackMinutes: number = 90,
+): SatellitePosition | null {
+ try {
+ const satrec = getSatrec(sat);
+ const posVel = satellite.propagate(satrec, time);
+ if (!posVel || typeof posVel.position === 'boolean' || !posVel.position) return null;
+
+ const pos = posVel.position;
+ const gmst = satellite.gstime(time);
+ const geo = satellite.eciToGeodetic(pos, gmst);
+
+ const lat = satellite.degreesLat(geo.latitude);
+ const lng = satellite.degreesLong(geo.longitude);
+ const altitude = geo.height;
+
+ // Ground track — use cache if fresh enough
+ const cached = trackCache.get(sat.noradId);
+ let groundTrack: [number, number][];
+
+ if (cached && Math.abs(cached.time - time.getTime()) < TRACK_CACHE_MS) {
+ groundTrack = cached.track;
+ } else {
+ groundTrack = [];
+ const steps = 20; // reduced from 60
+ const stepMs = (trackMinutes * 60 * 1000) / steps;
+
+ for (let i = -steps / 2; i <= steps / 2; i++) {
+ const t = new Date(time.getTime() + i * stepMs);
+ const pv = satellite.propagate(satrec, t);
+ if (!pv || typeof pv.position === 'boolean' || !pv.position) continue;
+ const g = satellite.gstime(t);
+ const gd = satellite.eciToGeodetic(pv.position, g);
+ groundTrack.push([
+ satellite.degreesLat(gd.latitude),
+ satellite.degreesLong(gd.longitude),
+ ]);
+ }
+ trackCache.set(sat.noradId, { time: time.getTime(), track: groundTrack });
+ }
+
+ return {
+ noradId: sat.noradId,
+ name: sat.name,
+ lat,
+ lng,
+ altitude,
+ category: sat.category,
+ groundTrack,
+ };
+ } catch {
+ return null;
+ }
+}
+
+export function propagateAll(
+ satellites: Satellite[],
+ time: Date,
+): SatellitePosition[] {
+ return satellites
+ .map(s => propagateSatellite(s, time))
+ .filter((p): p is SatellitePosition => p !== null);
+}
diff --git a/src/services/coastGuard.ts b/src/services/coastGuard.ts
new file mode 100644
index 0000000..50a318f
--- /dev/null
+++ b/src/services/coastGuard.ts
@@ -0,0 +1,82 @@
+// ═══ 대한민국 해양경찰청 시설 위치 ═══
+// Korea Coast Guard (KCG) facilities
+
+export type CoastGuardType = 'hq' | 'regional' | 'station' | 'substation' | 'vts';
+
+export interface CoastGuardFacility {
+ id: number;
+ name: string;
+ type: CoastGuardType;
+ lat: number;
+ lng: number;
+}
+
+const TYPE_LABEL: Record = {
+ hq: '본청',
+ regional: '지방청',
+ station: '해양경찰서',
+ substation: '파출소',
+ vts: 'VTS센터',
+};
+
+export { TYPE_LABEL as CG_TYPE_LABEL };
+
+export const COAST_GUARD_FACILITIES: CoastGuardFacility[] = [
+ // ═══ 본청 ═══
+ { id: 1, name: '해양경찰청 본청', type: 'hq', lat: 36.9870, lng: 126.9300 },
+
+ // ═══ 지방해양경찰청 ═══
+ { id: 10, name: '중부지방해양경찰청', type: 'regional', lat: 37.4563, lng: 126.5958 },
+ { id: 11, name: '서해지방해양경찰청', type: 'regional', lat: 34.8118, lng: 126.3922 },
+ { id: 12, name: '남해지방해양경찰청', type: 'regional', lat: 34.7436, lng: 127.7370 },
+ { id: 13, name: '동해지방해양경찰청', type: 'regional', lat: 37.7695, lng: 128.8760 },
+ { id: 14, name: '제주지방해양경찰청', type: 'regional', lat: 33.5170, lng: 126.5310 },
+
+ // ═══ 해양경찰서 ═══
+ { id: 20, name: '인천해양경찰서', type: 'station', lat: 37.4500, lng: 126.6100 },
+ { id: 21, name: '평택해양경찰서', type: 'station', lat: 36.9694, lng: 126.8319 },
+ { id: 22, name: '태안해양경찰서', type: 'station', lat: 36.7456, lng: 126.2978 },
+ { id: 23, name: '보령해양경찰서', type: 'station', lat: 36.3500, lng: 126.5880 },
+ { id: 24, name: '군산해양경찰서', type: 'station', lat: 35.9750, lng: 126.6530 },
+ { id: 25, name: '목포해양경찰서', type: 'station', lat: 34.7930, lng: 126.3840 },
+ { id: 26, name: '완도해양경찰서', type: 'station', lat: 34.3110, lng: 126.7550 },
+ { id: 27, name: '여수해양경찰서', type: 'station', lat: 34.7440, lng: 127.7360 },
+ { id: 28, name: '통영해양경찰서', type: 'station', lat: 34.8540, lng: 128.4330 },
+ { id: 29, name: '창원해양경찰서', type: 'station', lat: 35.0800, lng: 128.5970 },
+ { id: 30, name: '부산해양경찰서', type: 'station', lat: 35.1028, lng: 129.0360 },
+ { id: 31, name: '울산해양경찰서', type: 'station', lat: 35.5067, lng: 129.3850 },
+ { id: 32, name: '포항해양경찰서', type: 'station', lat: 36.0320, lng: 129.3650 },
+ { id: 33, name: '동해해양경찰서', type: 'station', lat: 37.5250, lng: 129.1140 },
+ { id: 34, name: '속초해양경찰서', type: 'station', lat: 38.2040, lng: 128.5910 },
+ { id: 35, name: '제주해양경찰서', type: 'station', lat: 33.5200, lng: 126.5250 },
+ { id: 36, name: '서귀포해양경찰서', type: 'station', lat: 33.2400, lng: 126.5620 },
+
+ // ═══ 주요 파출소 ═══
+ { id: 50, name: '옹진해양경찰파출소', type: 'substation', lat: 37.0333, lng: 125.6833 },
+ { id: 51, name: '연평해양경찰파출소', type: 'substation', lat: 37.6660, lng: 125.7000 },
+ { id: 52, name: '백령해양경찰파출소', type: 'substation', lat: 37.9670, lng: 124.7170 },
+ { id: 53, name: '덕적해양경찰파출소', type: 'substation', lat: 37.2320, lng: 126.1450 },
+ { id: 54, name: '흑산해양경찰파출소', type: 'substation', lat: 34.6840, lng: 125.4350 },
+ { id: 55, name: '거문해양경찰파출소', type: 'substation', lat: 34.0290, lng: 127.3080 },
+ { id: 56, name: '추자해양경찰파출소', type: 'substation', lat: 33.9540, lng: 126.2960 },
+ { id: 57, name: '울릉해양경찰파출소', type: 'substation', lat: 37.4840, lng: 130.9060 },
+ { id: 58, name: '독도해양경찰파출소', type: 'substation', lat: 37.2426, lng: 131.8647 },
+ { id: 59, name: '마라도해양경찰파출소', type: 'substation', lat: 33.1140, lng: 126.2670 },
+
+ // ═══ VTS (Vessel Traffic Service) 센터 ═══
+ { id: 100, name: '인천VTS', type: 'vts', lat: 37.4480, lng: 126.6020 },
+ { id: 101, name: '평택VTS', type: 'vts', lat: 36.9600, lng: 126.8220 },
+ { id: 102, name: '대산VTS', type: 'vts', lat: 36.9850, lng: 126.3530 },
+ { id: 103, name: '군산VTS', type: 'vts', lat: 35.9880, lng: 126.5800 },
+ { id: 104, name: '목포VTS', type: 'vts', lat: 34.7850, lng: 126.3780 },
+ { id: 105, name: '완도VTS', type: 'vts', lat: 34.3250, lng: 126.7540 },
+ { id: 106, name: '여수VTS', type: 'vts', lat: 34.7480, lng: 127.7420 },
+ { id: 107, name: '통영VTS', type: 'vts', lat: 34.8500, lng: 128.4280 },
+ { id: 108, name: '마산VTS', type: 'vts', lat: 35.0720, lng: 128.5780 },
+ { id: 109, name: '부산VTS', type: 'vts', lat: 35.0750, lng: 129.0780 },
+ { id: 110, name: '울산VTS', type: 'vts', lat: 35.5100, lng: 129.3750 },
+ { id: 111, name: '포항VTS', type: 'vts', lat: 36.0450, lng: 129.3800 },
+ { id: 112, name: '동해VTS', type: 'vts', lat: 37.5300, lng: 129.1200 },
+ { id: 113, name: '속초VTS', type: 'vts', lat: 38.2100, lng: 128.5930 },
+ { id: 114, name: '제주VTS', type: 'vts', lat: 33.5150, lng: 126.5400 },
+];
diff --git a/src/services/infra.ts b/src/services/infra.ts
new file mode 100644
index 0000000..6da037b
--- /dev/null
+++ b/src/services/infra.ts
@@ -0,0 +1,135 @@
+// ═══ Korean Power Infrastructure from OpenStreetMap (Overpass API) ═══
+
+export interface PowerFacility {
+ id: string;
+ type: 'plant' | 'substation';
+ name: string;
+ lat: number;
+ lng: number;
+ source?: string; // solar, nuclear, gas, coal, wind, hydro, oil
+ output?: string; // e.g. "1000 MW"
+ operator?: string;
+ voltage?: string; // for substations
+}
+
+// Overpass QL: power plants + wind generators + substations in South Korea
+const OVERPASS_QUERY = `
+[out:json][timeout:30][bbox:33,124,39,132];
+(
+ nwr["power"="plant"];
+ nwr["power"="generator"]["generator:source"="wind"];
+ nwr["power"="substation"]["substation"="transmission"];
+);
+out center 500;
+`;
+
+let cachedData: PowerFacility[] | null = null;
+let lastFetch = 0;
+const CACHE_MS = 600_000; // 10 min cache
+
+export async function fetchKoreaInfra(): Promise {
+ if (cachedData && Date.now() - lastFetch < CACHE_MS) return cachedData;
+
+ try {
+ const url = `/api/overpass/api/interpreter`;
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: `data=${encodeURIComponent(OVERPASS_QUERY)}`,
+ });
+
+ if (!res.ok) throw new Error(`Overpass ${res.status}`);
+ const json = await res.json();
+
+ const facilities: PowerFacility[] = [];
+
+ for (const el of json.elements || []) {
+ const tags = el.tags || {};
+ const lat = el.lat ?? el.center?.lat;
+ const lng = el.lon ?? el.center?.lon;
+ if (lat == null || lng == null) continue;
+
+ const isPower = tags.power;
+ if (isPower === 'plant') {
+ facilities.push({
+ id: `plant-${el.id}`,
+ type: 'plant',
+ name: tags.name || tags['name:ko'] || tags['name:en'] || 'Power Plant',
+ lat, lng,
+ source: tags['plant:source'] || tags['generator:source'] || undefined,
+ output: tags['plant:output:electricity'] || undefined,
+ operator: tags.operator || undefined,
+ });
+ } else if (isPower === 'generator' && tags['generator:source'] === 'wind') {
+ facilities.push({
+ id: `wind-${el.id}`,
+ type: 'plant',
+ name: tags.name || tags['name:ko'] || tags['name:en'] || '풍력발전기',
+ lat, lng,
+ source: 'wind',
+ output: tags['generator:output:electricity'] || undefined,
+ operator: tags.operator || undefined,
+ });
+ } else if (isPower === 'substation') {
+ facilities.push({
+ id: `sub-${el.id}`,
+ type: 'substation',
+ name: tags.name || tags['name:ko'] || tags['name:en'] || 'Substation',
+ lat, lng,
+ voltage: tags.voltage || undefined,
+ operator: tags.operator || undefined,
+ });
+ }
+ }
+
+ console.log(`Overpass: ${facilities.length} power facilities in Korea (${facilities.filter(f => f.type === 'plant').length} plants, ${facilities.filter(f => f.type === 'substation').length} substations)`);
+ cachedData = facilities;
+ lastFetch = Date.now();
+ return facilities;
+ } catch (err) {
+ console.warn('Overpass API failed, using fallback data:', err);
+ if (cachedData) return cachedData;
+ return getFallbackInfra();
+ }
+}
+
+// Fallback: major Korean power plants (in case API fails)
+function getFallbackInfra(): PowerFacility[] {
+ return [
+ // Nuclear
+ { id: 'p-kori', type: 'plant', name: '고리 원전', lat: 35.3197, lng: 129.2783, source: 'nuclear', output: '6040 MW', operator: '한국수력원자력' },
+ { id: 'p-shin-kori', type: 'plant', name: '신고리 원전', lat: 35.3220, lng: 129.2900, source: 'nuclear', output: '5600 MW', operator: '한국수력원자력' },
+ { id: 'p-hanul', type: 'plant', name: '한울 원전', lat: 37.0928, lng: 129.3844, source: 'nuclear', output: '5900 MW', operator: '한국수력원자력' },
+ { id: 'p-hanbit', type: 'plant', name: '한빛 원전', lat: 35.4133, lng: 126.4228, source: 'nuclear', output: '5875 MW', operator: '한국수력원자력' },
+ { id: 'p-wolsong', type: 'plant', name: '월성 원전', lat: 35.7128, lng: 129.4753, source: 'nuclear', output: '2779 MW', operator: '한국수력원자력' },
+ { id: 'p-shin-wolsong', type: 'plant', name: '신월성 원전', lat: 35.7100, lng: 129.4800, source: 'nuclear', output: '2000 MW', operator: '한국수력원자력' },
+ // Coal/Gas
+ { id: 'p-dangjin', type: 'plant', name: '당진 화력', lat: 36.9703, lng: 126.6067, source: 'coal', output: '6040 MW', operator: '한국동서발전' },
+ { id: 'p-taean', type: 'plant', name: '태안 화력', lat: 36.7833, lng: 126.2500, source: 'coal', output: '6100 MW', operator: '한국서부발전' },
+ { id: 'p-boryeong', type: 'plant', name: '보령 화력', lat: 36.3500, lng: 126.4833, source: 'coal', output: '4000 MW', operator: '한국중부발전' },
+ { id: 'p-samcheok', type: 'plant', name: '삼척 화력', lat: 37.3667, lng: 129.1500, source: 'coal', output: '2100 MW', operator: '한국남부발전' },
+ { id: 'p-incheon', type: 'plant', name: '인천 LNG', lat: 37.4483, lng: 126.5917, source: 'gas', output: '3478 MW', operator: '한국중부발전' },
+ { id: 'p-pyeongtaek', type: 'plant', name: '평택 LNG', lat: 36.9667, lng: 126.9333, source: 'gas', output: '1770 MW', operator: '한국중부발전' },
+ { id: 'p-yeongheung', type: 'plant', name: '영흥 화력', lat: 37.2167, lng: 126.4333, source: 'coal', output: '5080 MW', operator: '한국남동발전' },
+ // Hydro
+ { id: 'p-chungju', type: 'plant', name: '충주 수력', lat: 36.9833, lng: 127.9833, source: 'hydro', output: '412 MW', operator: '한국수력원자력' },
+ { id: 'p-hapcheon', type: 'plant', name: '합천 수력', lat: 35.5667, lng: 128.1500, source: 'hydro', output: '360 MW', operator: '한국수력원자력' },
+ // Wind
+ { id: 'p-yeongdeok', type: 'plant', name: '영덕 풍력', lat: 36.4150, lng: 129.4300, source: 'wind', output: '39.6 MW', operator: '한국남동발전' },
+ { id: 'p-taebaek', type: 'plant', name: '태백 풍력', lat: 37.1500, lng: 128.9800, source: 'wind', output: '40 MW', operator: '한국동서발전' },
+ { id: 'p-gasiri', type: 'plant', name: '가시리 풍력 (제주)', lat: 33.3600, lng: 126.6800, source: 'wind', output: '15 MW', operator: '제주에너지공사' },
+ { id: 'p-tamra', type: 'plant', name: '탐라 해상풍력 (제주)', lat: 33.2800, lng: 126.1700, source: 'wind', output: '30 MW', operator: '탐라해상풍력' },
+ { id: 'p-seonam', type: 'plant', name: '서남해 해상풍력', lat: 35.0700, lng: 126.0200, source: 'wind', output: '60 MW', operator: '한국해상풍력' },
+ { id: 'p-yeongyang', type: 'plant', name: '영양 풍력', lat: 36.7000, lng: 129.1200, source: 'wind', output: '61.5 MW', operator: '한국남부발전' },
+ { id: 'p-jeongseon', type: 'plant', name: '정선 풍력', lat: 37.3300, lng: 128.7200, source: 'wind', output: '30 MW', operator: '강원풍력' },
+ { id: 'p-daegwallyeong', type: 'plant', name: '대관령 풍력', lat: 37.7000, lng: 128.7500, source: 'wind', output: '98 MW', operator: '대관령풍력' },
+ { id: 'p-sinan', type: 'plant', name: '신안 해상풍력', lat: 34.8500, lng: 125.8800, source: 'wind', output: '8.2 GW', operator: '신안해상풍력' },
+ { id: 'p-ulsan-float', type: 'plant', name: '울산 부유식 해상풍력', lat: 35.4000, lng: 129.6500, source: 'wind', output: '1.5 GW', operator: '울산부유식풍력' },
+ // Major substations
+ { id: 's-singapyeong', type: 'substation', name: '신갑평 변전소', lat: 37.1667, lng: 127.3000, voltage: '765000', operator: 'KEPCO' },
+ { id: 's-sinseosan', type: 'substation', name: '신서산 변전소', lat: 36.7500, lng: 126.5000, voltage: '765000', operator: 'KEPCO' },
+ { id: 's-sinchungju', type: 'substation', name: '신충주 변전소', lat: 36.9833, lng: 127.9500, voltage: '765000', operator: 'KEPCO' },
+ { id: 's-sinyongin', type: 'substation', name: '신용인 변전소', lat: 37.2333, lng: 127.2000, voltage: '765000', operator: 'KEPCO' },
+ { id: 's-sinbukgyeongnam', type: 'substation', name: '신북경남 변전소', lat: 35.4500, lng: 128.7500, voltage: '765000', operator: 'KEPCO' },
+ ];
+}
diff --git a/src/services/koreaEez.ts b/src/services/koreaEez.ts
new file mode 100644
index 0000000..a2b3383
--- /dev/null
+++ b/src/services/koreaEez.ts
@@ -0,0 +1,122 @@
+// ═══ 대한민국 배타적 경제수역(EEZ) 경계 좌표 ═══
+// 출처: 해양수산부, UNCLOS, 한일/한중 어업협정, NLL 기준
+// 서해(한중 잠정조치수역), 남해(한일 중간수역), 동해(한일 중간선) 포함
+
+// EEZ 외곽 경계 (시계방향, [lat, lng])
+export const KOREA_EEZ_BOUNDARY: [number, number][] = [
+ // ── 서해 NLL 부근 (북서단) ──
+ [37.75, 124.40],
+ [37.70, 124.10],
+ [37.40, 123.70],
+ [37.00, 123.50],
+ [36.60, 123.30],
+ [36.20, 123.20],
+ [35.80, 123.10],
+ [35.40, 123.00],
+ [35.00, 122.90],
+
+ // ── 서해 남부 (한중 중간선 부근) ──
+ [34.60, 122.80],
+ [34.20, 123.00],
+ [33.80, 123.30],
+ [33.40, 123.60],
+ [33.00, 124.00],
+
+ // ── 제주도 남서~남 ──
+ [32.60, 124.40],
+ [32.30, 125.00],
+ [32.10, 125.60],
+ [32.00, 126.20],
+
+ // ── 제주도 남쪽 (한일 중간수역 북방한계) ──
+ [32.00, 126.80],
+ [32.10, 127.30],
+ [32.20, 127.80],
+ [32.40, 128.10],
+
+ // ── 대한해협 (대마도 서쪽, 한일 중간선) ──
+ // 대마도(쓰시마) 서안 129.2°E → 중간선은 약 128.5°E
+ [32.70, 128.40],
+ [33.00, 128.50],
+ [33.50, 128.60],
+ [34.00, 128.80],
+ [34.40, 129.10],
+
+ // ── 동해 남부 (한일 중간선) ──
+ [34.80, 129.40],
+ [35.20, 129.70],
+ [35.60, 130.00],
+ [36.00, 130.20],
+ [36.50, 130.40],
+ [37.00, 130.60],
+
+ // ── 동해 중부~북부 (울릉도 동쪽) ──
+ [37.50, 130.80],
+ [38.00, 130.90],
+ [38.30, 130.80],
+ [38.60, 130.60],
+
+ // ── 동해안 따라 남하 (영해 외곽) ──
+ [38.50, 128.80],
+ [38.35, 128.60],
+ [38.30, 128.55],
+
+ // ── 서해 NLL 연결 (육지 경유 개념, 실제는 해상만 표시) ──
+ // 생략 — 폴리곤을 닫기 위해 시작점으로 복귀
+ [37.75, 124.40],
+];
+
+// 한일 중간수역 (Joint Management Zone) — 사용하지 않음, 참고용
+export const KOREA_JAPAN_JMZ: [number, number][] = [];
+
+// 한중 잠정조치수역 (Provisional Measures Zone)
+export const KOREA_CHINA_PMZ: [number, number][] = [
+ [37.00, 123.00],
+ [36.75, 122.80],
+ [36.25, 122.60],
+ [35.75, 122.50],
+ [35.25, 122.40],
+ [34.50, 122.50],
+ [34.00, 122.80],
+ [33.50, 123.20],
+ [33.00, 123.60],
+ [32.50, 124.20],
+ [32.50, 124.80],
+ [33.00, 124.50],
+ [33.50, 124.20],
+ [34.00, 124.00],
+ [34.50, 123.80],
+ [35.00, 123.70],
+ [35.50, 123.60],
+ [36.00, 123.50],
+ [36.50, 123.40],
+ [37.00, 123.30],
+ [37.00, 123.00],
+];
+
+// 독도 주변 12해리 영해 (원형 근사)
+export const DOKDO_TERRITORIAL: { center: [number, number]; radiusKm: number } = {
+ center: [37.2417, 131.8647],
+ radiusKm: 22.2, // 12 nautical miles
+};
+
+// 서해 5도 NLL (Northern Limit Line)
+export const NLL_WEST_SEA: [number, number][] = [
+ [37.75, 124.40],
+ [37.74, 124.65],
+ [37.72, 124.90],
+ [37.70, 125.10],
+ [37.68, 125.30],
+ [37.67, 125.50],
+ [37.67, 125.70],
+];
+
+// 동해 NLL
+export const NLL_EAST_SEA: [number, number][] = [
+ [38.60, 128.35],
+ [38.60, 128.60],
+ [38.60, 129.00],
+ [38.60, 129.50],
+ [38.60, 130.00],
+ [38.60, 130.60],
+];
diff --git a/src/services/navWarning.ts b/src/services/navWarning.ts
new file mode 100644
index 0000000..720abaa
--- /dev/null
+++ b/src/services/navWarning.ts
@@ -0,0 +1,605 @@
+// ═══ 해상사격장 구역 / 항행경보 ═══
+// Source: 해상사격장 구역(좌표) WGS-84 (2025.10.29)
+// 해군/해병대/공군/육군/해경/국방과학연구소 훈련구역
+
+export type NavWarningLevel = 'danger' | 'caution' | 'info';
+export type NavWarningArea = '동해' | '서해' | '남해' | '제주' | '전해역';
+export type TrainingOrg = '해군' | '해병대' | '공군' | '육군' | '해경' | '국과연';
+
+export interface NavWarning {
+ id: string; // R-72, R-99, etc.
+ title: string;
+ org: TrainingOrg;
+ area: NavWarningArea;
+ level: NavWarningLevel;
+ lat: number; // center lat for marker
+ lng: number; // center lng for marker
+ polygon: [number, number][]; // [lat, lng][] vertices
+ altitude: string; // 사용고도
+ description: string;
+ source: string;
+}
+
+const LEVEL_LABEL: Record = {
+ danger: '위험',
+ caution: '주의',
+ info: '정보',
+};
+
+export { LEVEL_LABEL as NW_LEVEL_LABEL };
+
+const ORG_LABEL: Record = {
+ '해군': '해군 훈련구역',
+ '해병대': '해병대 훈련구역',
+ '공군': '공군 훈련구역',
+ '육군': '육군 훈련구역',
+ '해경': '해양경찰청 훈련구역',
+ '국과연': '국방과학연구소 훈련구역',
+};
+
+export { ORG_LABEL as NW_ORG_LABEL };
+
+/** DMS → decimal helper (used at build time, coords below are pre-converted) */
+function dms(d: number, m: number, s: number): number {
+ return d + m / 60 + s / 3600;
+}
+
+/** Compute center of polygon */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function center(pts: [number, number][]): [number, number] {
+ const lat = pts.reduce((s, p) => s + p[0], 0) / pts.length;
+ const lng = pts.reduce((s, p) => s + p[1], 0) / pts.length;
+ return [lat, lng];
+}
+
+// ═══════════════════════════════════════════════════════
+// 해상사격장 구역 데이터 (WGS-84)
+// ═══════════════════════════════════════════════════════
+
+export const NAV_WARNINGS: NavWarning[] = [
+ // ═══════════════════════════════════════
+ // 해군 훈련구역
+ // ═══════════════════════════════════════
+ {
+ id: 'R-72', title: 'R-72 대한해협 육지도남반근해', org: '해군', area: '남해', level: 'danger',
+ lat: 34.22, lng: 128.35, altitude: '무한대',
+ polygon: [
+ [dms(34,9,41), dms(128,0,0)], [dms(34,18,1), dms(128,11,27)],
+ [dms(34,18,0), dms(128,35,0)], [dms(34,34,0), dms(128,35,14)],
+ [dms(34,9,13), dms(128,43,11)], [dms(34,0,0), dms(128,31,0)],
+ [dms(34,0,0), dms(128,0,0)],
+ ],
+ description: '해군 대한해협 육지도남반근해 사격훈련구역. 사용고도 무한대.',
+ source: '해군',
+ },
+ {
+ id: 'R-99', title: 'R-99 대한해협 거제도남동연안', org: '해군', area: '남해', level: 'danger',
+ lat: 34.55, lng: 128.77, altitude: '36,000ft',
+ polygon: [
+ [dms(34,41,4), dms(128,43,27)], [dms(34,46,6), dms(128,50,31)],
+ [dms(34,46,44), dms(128,53,38)], [dms(34,34,47), dms(129,3,21)],
+ [dms(34,19,13), dms(128,41,11)], [dms(34,20,12), dms(128,35,14)],
+ ],
+ description: '해군 대한해협 거제도남동연안 훈련구역. 사용고도 36,000ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-100', title: 'R-100 남해안 남형제도부근', org: '해군', area: '남해', level: 'danger',
+ lat: dms(34,53,0), lng: dms(126,37,0), altitude: '500ft',
+ polygon: [[dms(34,53,0), dms(126,37,0)]], // 중심점 반경 4마일
+ description: '남해안 남형제도 부근 훈련구역. 반경 4마일. 사용고도 500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-115', title: 'R-115 동해 울릉도남방근해', org: '해군', area: '동해', level: 'danger',
+ lat: 37.31, lng: 130.38, altitude: '38,000ft',
+ polygon: [
+ [dms(37,24,0), dms(129,45,0)], [dms(37,13,30), dms(131,0,0)],
+ ],
+ description: '동해 울릉도남방근해 훈련구역. 사용고도 38,000ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-117', title: 'R-117 서해안 우이도북서방면', org: '해군', area: '서해', level: 'danger',
+ lat: dms(34,42,30), lng: dms(125,44,0), altitude: '3,000ft',
+ polygon: [[dms(34,42,30), dms(125,44,0)]], // 중심점 반경 5마일
+ description: '서해안 우이도 북서방면 훈련구역. 반경 5마일. 사용고도 3,000ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-118', title: 'R-118 대한해협 제주도동남근해', org: '해군', area: '제주', level: 'danger',
+ lat: 33.72, lng: 127.53, altitude: '2,500ft',
+ polygon: [
+ [dms(34,0,0), dms(127,40,0)], [dms(34,0,0), dms(128,30,0)],
+ [dms(33,10,0), dms(127,0,0)], [dms(33,10,0), dms(127,40,0)],
+ ],
+ description: '대한해협 제주도동남근해 훈련구역. 사용고도 2,500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-119', title: 'R-119 동해 울산근해', org: '해군', area: '동해', level: 'danger',
+ lat: 35.61, lng: 129.93, altitude: '2,500ft',
+ polygon: [
+ [dms(35,47,0), dms(129,40,55)], [dms(35,43,9), dms(130,12,12)],
+ [dms(35,37,36), dms(130,12,12)], [dms(35,27,56), dms(129,51,48)],
+ [dms(35,28,0), dms(129,40,55)],
+ ],
+ description: '동해 울산근해 훈련구역. 사용고도 2,500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-120', title: 'R-120 동해 포항북동앞바다', org: '해군', area: '동해', level: 'danger',
+ lat: 36.32, lng: 130.38, altitude: '38,000ft',
+ polygon: [
+ [dms(36,44,0), dms(130,25,0)], [dms(36,25,0), dms(130,55,0)],
+ [dms(36,17,0), dms(130,55,0)], [dms(36,2,0), dms(130,29,0)],
+ [dms(36,2,0), dms(130,25,0)],
+ ],
+ description: '동해 포항 북동 앞바다 훈련구역. 사용고도 38,000ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-121', title: 'R-121 동해 속초근해', org: '해군', area: '동해', level: 'danger',
+ lat: 38.15, lng: 129.08, altitude: '2,500ft',
+ polygon: [
+ [dms(38,25,0), dms(128,45,0)], [dms(38,25,0), dms(129,30,0)],
+ [dms(38,25,0), dms(129,30,0)], [dms(38,25,0), dms(129,0,0)],
+ [dms(38,17,0), dms(129,0,50)], [dms(37,17,0), dms(128,45,0)],
+ ],
+ description: '동해 속초근해 훈련구역. 사용고도 2,500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-123', title: 'R-123 관할 완도독서시근해', org: '해군', area: '서해', level: 'danger',
+ lat: 35.78, lng: 125.15, altitude: '3,700ft',
+ polygon: [
+ [dms(36,0,0), dms(125,0,0)], [dms(36,0,0), dms(125,30,0)],
+ [dms(35,0,0), dms(125,30,0)], [dms(35,35,0), dms(125,0,0)],
+ ],
+ description: '관할 완도독서시근해 훈련구역. 사용고도 3,700ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-124', title: 'R-124 서해안 거역도부근', org: '해군', area: '서해', level: 'danger',
+ lat: 36.88, lng: 125.67, altitude: '2,500ft',
+ polygon: [
+ [dms(37,6,0), dms(125,42,0)], [dms(37,6,0), dms(126,10,0)],
+ [dms(36,55,0), dms(125,57,0)], [dms(36,55,0), dms(125,42,0)],
+ ],
+ description: '서해안 거역도부근 훈련구역. 사용고도 2,500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-125', title: 'R-125 서해안 흑산도남서방면', org: '해군', area: '서해', level: 'danger',
+ lat: dms(34,33,0), lng: dms(125,3,0), altitude: '3,500ft',
+ polygon: [[dms(34,33,0), dms(125,3,0)]], // 중심점 반경 6마일
+ description: '서해안 흑산도 남서방면 훈련구역. 반경 6마일. 사용고도 3,500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-126', title: 'R-126 관할 제주도북서해상', org: '해군', area: '제주', level: 'danger',
+ lat: 34.05, lng: 126.05, altitude: '3,000ft',
+ polygon: [
+ [dms(34,30,0), dms(125,48,0)], [dms(34,0,0), dms(126,0,0)],
+ [dms(33,30,0), dms(125,48,0)], [dms(33,30,0), dms(126,10,0)],
+ ],
+ description: '관할 제주도 북서해상 훈련구역. 사용고도 3,000ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-128', title: 'R-128 대한해협 사기포근해', org: '해군', area: '남해', level: 'danger',
+ lat: 33.25, lng: 126.95, altitude: '7,000ft',
+ polygon: [
+ [dms(33,0,0), dms(126,37,0)], [dms(32,40,0), dms(126,45,0)],
+ ],
+ description: '대한해협 사기포근해 훈련구역. 사용고도 7,000ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-133', title: 'R-133 서해안 초지양부근', org: '해군', area: '서해', level: 'danger',
+ lat: dms(37,22,20), lng: dms(126,11,35), altitude: '500ft',
+ polygon: [[dms(37,22,20), dms(126,11,35)]], // 중심점 반경 2마일
+ description: '서해안 초지양부근 훈련구역. 반경 2마일. 사용고도 500ft.',
+ source: '해군',
+ },
+ {
+ id: 'R-135', title: 'R-135 동해 강릉근해', org: '해군', area: '동해', level: 'danger',
+ lat: 37.95, lng: 129.42, altitude: '500ft',
+ polygon: [
+ [dms(38,9,30), dms(129,4,0)], [dms(38,6,0), dms(129,37,45)],
+ [dms(37,33,30), dms(129,24,15)], [dms(37,37,0), dms(129,30,20)],
+ ],
+ description: '동해 강릉근해 훈련구역. 사용고도 500ft.',
+ source: '해군',
+ },
+
+ // ═══════════════════════════════════════
+ // 해병대 훈련구역
+ // ═══════════════════════════════════════
+ {
+ id: 'R-116', title: 'R-116 서해안 대청도감우부근', org: '해병대', area: '서해', level: 'danger',
+ lat: dms(37,47,55), lng: dms(124,39,33), altitude: '2,500ft',
+ polygon: [[dms(37,47,55), dms(124,39,33)]], // 반경 4마일
+ description: '해병대 서해안 대청도 감우부근 훈련구역. 반경 4마일. 사용고도 2,500ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-131', title: 'R-131 서해안 백령도 서방면', org: '해병대', area: '서해', level: 'danger',
+ lat: 37.94, lng: 124.50, altitude: '5,000ft',
+ polygon: [
+ [dms(37,59,0), dms(124,4,10)], [dms(37,59,0), dms(124,38,10)],
+ [dms(37,59,21), dms(124,42,30)], [dms(37,54,0), dms(124,42,30)],
+ [dms(37,54,0), dms(124,38,10)], [dms(37,54,0), dms(124,4,10)],
+ ],
+ description: '해병대 백령도 서방면 훈련구역. 사용고도 5,000ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-132', title: 'R-132 서해안 백령도 남동면', org: '해병대', area: '서해', level: 'danger',
+ lat: 37.90, lng: 124.70, altitude: '10,000ft',
+ polygon: [
+ [dms(37,57,0), dms(124,44,0)], [dms(37,57,0), dms(124,44,0)],
+ [dms(37,45,0), dms(124,50,0)], [dms(37,45,0), dms(124,47,0)],
+ ],
+ description: '해병대 백령도 남동면 훈련구역. 사용고도 10,000ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-134', title: 'R-134 서해안 대청연평(연)부근', org: '해병대', area: '서해', level: 'danger',
+ lat: 37.60, lng: 125.30, altitude: '5,000ft',
+ polygon: [
+ [dms(37,38,40), dms(124,45,0)], [dms(37,42,0), dms(125,5,0)],
+ [dms(37,42,0), dms(125,0,30)], [dms(37,40,0), dms(125,10,0)],
+ [dms(37,38,0), dms(124,45,0)], [dms(37,34,0), dms(124,14,45)],
+ [dms(37,40,0), dms(125,12,0)], [dms(37,41,0), dms(125,41,40)],
+ [dms(37,37,20), dms(125,41,40)], [dms(37,37,30), dms(125,29,0)],
+ [dms(37,30,45), dms(125,24,0)], [dms(37,26,0), dms(125,24,0)],
+ [dms(37,24,6), dms(125,24,0)], [dms(37,32,15), dms(124,48,0)],
+ ],
+ description: '해병대 서해안 대청연평 훈련구역. 사용고도 5,000ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-136', title: 'R-136 동해 삼척근해', org: '해병대', area: '동해', level: 'danger',
+ lat: 37.48, lng: 129.60, altitude: '500ft',
+ polygon: [
+ [dms(37,25,0), dms(129,30,45)], [dms(37,28,30), dms(129,37,0)],
+ [dms(37,6,30), dms(129,47,10)], [dms(37,30,30), dms(129,40,0)],
+ ],
+ description: '해병대 동해 삼척근해 훈련구역. 사용고도 500ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-137', title: 'R-137 서해안 우도앞면', org: '해병대', area: '서해', level: 'danger',
+ lat: 37.63, lng: 125.87, altitude: '5,000ft',
+ polygon: [
+ [dms(37,38,27), dms(125,55,27)], [dms(37,36,23), dms(126,0,23)],
+ [dms(37,36,22), dms(126,0,19)], [dms(37,36,22), dms(125,55,24)],
+ ],
+ description: '해병대 서해안 우도앞면 훈련구역. 사용고도 5,000ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-153', title: 'R-153 서해안 대청도서쪽', org: '해병대', area: '서해', level: 'danger',
+ lat: 37.72, lng: 124.45, altitude: '3,000ft',
+ polygon: [
+ [dms(37,44,55), dms(124,5,10)], [dms(37,44,55), dms(124,56,40)],
+ [dms(37,10,30), dms(124,36,40)], [dms(37,30,14), dms(124,36,10)],
+ ],
+ description: '해병대 서해안 대청도 서쪽 훈련구역. 사용고도 3,000ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-154', title: 'R-154 서해안 연평도부근', org: '해병대', area: '서해', level: 'danger',
+ lat: 37.63, lng: 125.72, altitude: '3,000ft',
+ polygon: [
+ [dms(37,41,46), dms(125,43,12)], [dms(37,35,17), dms(125,43,12)],
+ [dms(37,35,17), dms(125,54,12)], [dms(37,37,47), dms(125,55,10)],
+ ],
+ description: '해병대 서해안 연평도 부근 훈련구역. 사용고도 3,000ft.',
+ source: '해병대',
+ },
+ {
+ id: 'R-156', title: 'R-156 동해안 주문진근해', org: '해병대', area: '동해', level: 'danger',
+ lat: 37.90, lng: 129.30, altitude: '3,500ft',
+ polygon: [
+ [dms(38,14,0), dms(129,0,0)], [dms(37,48,0), dms(129,46,0)],
+ [dms(37,48,0), dms(129,36,0)], [dms(38,10,0), dms(129,19,0)],
+ [dms(38,10,0), dms(129,0,0)],
+ ],
+ description: '해병대 동해안 주문진 근해 훈련구역. 사용고도 3,500ft.',
+ source: '해병대',
+ },
+
+ // ═══════════════════════════════════════
+ // 국방과학연구소 훈련구역
+ // ═══════════════════════════════════════
+ {
+ id: 'R-108A', title: 'R-108A 서해안 안흥1', org: '국과연', area: '서해', level: 'danger',
+ lat: 36.62, lng: 126.25, altitude: '27,000ft',
+ polygon: [
+ [dms(36,40,46), dms(126,9,16)], [dms(36,40,36), dms(126,11,58)],
+ [dms(36,33,48), dms(126,13,48)], [dms(36,33,58), dms(126,9,54)],
+ ],
+ description: '국방과학연구소 서해안 안흥 시험장 1구역. 사용고도 27,000ft.',
+ source: '국과연',
+ },
+ {
+ id: 'R-108B', title: 'R-108B 서해안 안흥2', org: '국과연', area: '서해', level: 'danger',
+ lat: 36.62, lng: 126.18, altitude: '33,000ft',
+ polygon: [
+ [dms(36,40,46), dms(126,9,16)], [dms(36,40,36), dms(126,11,58)],
+ [dms(36,29,25), dms(126,15,1)], [dms(36,28,10), dms(126,7,28)],
+ ],
+ description: '국방과학연구소 서해안 안흥 시험장 2구역. 사용고도 33,000ft.',
+ source: '국과연',
+ },
+ {
+ id: 'R-108D', title: 'R-108D 서해안 안흥4', org: '국과연', area: '서해', level: 'danger',
+ lat: 36.55, lng: 126.15, altitude: '40,000ft',
+ polygon: [
+ [dms(36,40,46), dms(126,9,16)], [dms(36,40,36), dms(126,11,52)],
+ [dms(36,21,50), dms(126,9,7)], [dms(36,23,55), dms(126,2,2)],
+ ],
+ description: '국방과학연구소 서해안 안흥 시험장 4구역. 사용고도 40,000ft.',
+ source: '국과연',
+ },
+ {
+ id: 'R-108F', title: 'R-108F 서해안 안흥6', org: '국과연', area: '서해', level: 'danger',
+ lat: 36.55, lng: 126.08, altitude: '80,000ft',
+ polygon: [
+ [dms(36,40,46), dms(126,9,16)], [dms(36,40,36), dms(126,11,52)],
+ [dms(36,17,19), dms(126,0,32)], [dms(36,18,10), dms(125,56,37)],
+ ],
+ description: '국방과학연구소 서해안 안흥 시험장 6구역. 사용고도 80,000ft.',
+ source: '국과연',
+ },
+
+ // ═══════════════════════════════════════
+ // 해양경찰청 훈련구역
+ // ═══════════════════════════════════════
+ {
+ id: 'R-140', title: 'R-140 동해안 속초연안', org: '해경', area: '동해', level: 'caution',
+ lat: dms(38,9,0), lng: dms(128,53,0), altitude: '300ft',
+ polygon: [[dms(38,9,0), dms(128,53,0)]], // 중심점 반경 이내
+ description: '해양경찰청 동해안 속초연안 훈련구역. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-141', title: 'R-141 동해 묵호동방연안', org: '해경', area: '동해', level: 'caution',
+ lat: 37.58, lng: 129.28, altitude: '300ft',
+ polygon: [
+ [dms(37,47,5), dms(129,11,0)], [dms(37,42,5), dms(129,13,0)],
+ [dms(37,30,5), dms(129,11,0)], [dms(37,30,5), dms(129,12,0)],
+ ],
+ description: '해양경찰청 동해 묵호동방연안 훈련구역. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-142A', title: 'R-142A 동해안 묵호연안(갑)', org: '해경', area: '동해', level: 'caution',
+ lat: dms(37,8,0), lng: dms(129,34,0), altitude: '300ft',
+ polygon: [[dms(37,8,0), dms(129,34,0)]], // 반경 2마일
+ description: '해양경찰청 동해안 묵호연안(갑) 훈련구역. 반경 2마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-142B', title: 'R-142B 동해 강구항근해', org: '해경', area: '동해', level: 'caution',
+ lat: dms(36,20,0), lng: dms(129,50,0), altitude: '300ft',
+ polygon: [[dms(36,20,0), dms(129,50,0)]], // 반경 5마일
+ description: '해양경찰청 동해 강구항근해 훈련구역. 반경 5마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-142C', title: 'R-142C 동해안 호미곶연안', org: '해경', area: '동해', level: 'caution',
+ lat: dms(36,5,0), lng: dms(129,45,0), altitude: '300ft',
+ polygon: [[dms(36,5,0), dms(129,45,0)]], // 반경 5마일
+ description: '해양경찰청 동해안 호미곶연안 훈련구역. 반경 5마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-143', title: 'R-143 남해안 부산연안', org: '해경', area: '남해', level: 'caution',
+ lat: 35.15, lng: 129.28, altitude: '300ft',
+ polygon: [
+ [dms(35,7,10), dms(129,17,0)], [dms(35,4,25), dms(129,20,40)],
+ [dms(34,58,20), dms(129,14,15)], [dms(35,1,10), dms(129,10,25)],
+ ],
+ description: '해양경찰청 남해안 부산연안 훈련구역. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-144', title: 'R-144 남해안 소지도부근', org: '해경', area: '남해', level: 'caution',
+ lat: dms(34,39,0), lng: dms(128,36,0), altitude: '300ft',
+ polygon: [[dms(34,39,0), dms(128,36,0)]], // 반경 4마일
+ description: '해양경찰청 남해안 소지도부근 훈련구역. 반경 4마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-145', title: 'R-145 남해안 세도도부근', org: '해경', area: '남해', level: 'caution',
+ lat: dms(34,29,56), lng: dms(128,4,52), altitude: '300ft',
+ polygon: [[dms(34,29,56), dms(128,4,52)]], // 반경 5마일
+ description: '해양경찰청 남해안 세도도부근 훈련구역. 반경 5마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-146', title: 'R-146 남해안 정도남방연안', org: '해경', area: '남해', level: 'caution',
+ lat: dms(34,4,11), lng: dms(126,51,53), altitude: '300ft',
+ polygon: [[dms(34,4,11), dms(126,51,53)]], // 반경 5마일
+ description: '해양경찰청 남해안 정도남방연안 훈련구역. 반경 5마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-147', title: 'R-147 대한해협 마라도남방연안', org: '해경', area: '제주', level: 'caution',
+ lat: dms(32,40,0), lng: dms(126,0,0), altitude: '300ft',
+ polygon: [[dms(32,40,0), dms(126,0,0)]], // 반경 4마일
+ description: '해양경찰청 대한해협 마라도남방연안 훈련구역. 반경 4마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-148A', title: 'R-148A 서해안 멸치도부근', org: '해경', area: '서해', level: 'caution',
+ lat: dms(34,45,34), lng: dms(126,13,24), altitude: '300ft',
+ polygon: [[dms(34,45,34), dms(126,13,24)]], // 반경 2.5마일
+ description: '해양경찰청 서해안 멸치도부근 훈련구역. 반경 2.5마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-148B', title: 'R-148B 서해안 진도서방연안', org: '해경', area: '서해', level: 'caution',
+ lat: dms(34,25,11), lng: dms(125,54,53), altitude: '300ft',
+ polygon: [[dms(34,25,11), dms(125,54,53)]], // 반경 4마일
+ description: '해양경찰청 서해안 진도서방연안 훈련구역. 반경 4마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-149', title: 'R-149 남해안 화도서방연안', org: '해경', area: '남해', level: 'caution',
+ lat: dms(33,44,45), lng: dms(126,13,0), altitude: '300ft',
+ polygon: [[dms(33,44,45), dms(126,13,0)]], // 반경 5마일
+ description: '해양경찰청 남해안 화도서방연안 훈련구역. 반경 5마일. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-150', title: 'R-150 남해안 마라도동쪽연안', org: '해경', area: '제주', level: 'caution',
+ lat: 33.31, lng: 126.45, altitude: '300ft',
+ polygon: [
+ [dms(33,8,30), dms(126,23,0)], [dms(33,8,30), dms(126,29,0)],
+ [dms(32,58,30), dms(126,29,0)], [dms(32,58,30), dms(126,22,0)],
+ ],
+ description: '해양경찰청 남해안 마라도동쪽연안 훈련구역. 사용고도 300ft.',
+ source: '해경',
+ },
+ {
+ id: 'R-151A', title: 'R-151A 서해안 팔금도남방연안', org: '해경', area: '서해', level: 'caution',
+ lat: 35.55, lng: 126.32, altitude: '300ft',
+ polygon: [
+ [dms(35,50,0), dms(126,18,50)], [dms(35,0,0), dms(126,20,0)],
+ [dms(35,45,0), dms(126,20,0)], [dms(35,45,0), dms(126,15,0)],
+ ],
+ description: '해양경찰청 서해안 팔금도남방연안 훈련구역. 사용고도 300ft.',
+ source: '해경',
+ },
+
+ // ═══════════════════════════════════════
+ // 공군 훈련구역
+ // ═══════════════════════════════════════
+ {
+ id: 'R-74', title: 'R-74 동해 포항북동남서역', org: '공군', area: '동해', level: 'danger',
+ lat: 36.40, lng: 130.10, altitude: '50,000ft',
+ polygon: [
+ [dms(36,52,0), dms(130,0,0)], [dms(36,52,0), dms(130,13,0)],
+ [dms(36,44,0), dms(130,25,0)], [dms(36,2,0), dms(130,25,0)],
+ [dms(36,2,0), dms(130,0,0)],
+ ],
+ description: '공군 동해 포항 북동남서역 훈련구역. 사용고도 50,000ft.',
+ source: '공군',
+ },
+ {
+ id: 'R-77', title: 'R-77 동해 미작진부근', org: '공군', area: '동해', level: 'danger',
+ lat: 38.50, lng: 128.50, altitude: '15,000ft',
+ polygon: [
+ [dms(38,33,0), dms(128,24,0)], [dms(38,33,0), dms(128,31,0)],
+ [dms(38,32,0), dms(128,32,0)], [dms(38,30,0), dms(128,31,0)],
+ [dms(38,31,0), dms(128,24,0)],
+ ],
+ description: '공군 동해 미작진부근 훈련구역. 사용고도 15,000ft.',
+ source: '공군',
+ },
+ {
+ id: 'R-80', title: 'R-80 관할 격렬비도남서역', org: '공군', area: '서해', level: 'danger',
+ lat: 36.35, lng: 125.92, altitude: '50,000ft',
+ polygon: [
+ [dms(36,32,0), dms(124,50,0)], [dms(36,32,0), dms(125,36,0)],
+ [dms(36,30,0), dms(125,36,0)], [dms(36,4,55), dms(124,31,26)],
+ [dms(36,23,0), dms(124,31,26)],
+ ],
+ description: '공군 관할 격렬비도 남서역 훈련구역. 사용고도 50,000ft.',
+ source: '공군',
+ },
+ {
+ id: 'R-84', title: 'R-84 관할 임자도서남역', org: '공군', area: '서해', level: 'danger',
+ lat: 35.22, lng: 125.70, altitude: '50,000ft',
+ polygon: [
+ [dms(35,15,57), dms(124,31,44)], [dms(35,15,0), dms(125,36,10)],
+ [dms(34,30,0), dms(125,36,12)], [dms(34,44,7), dms(125,17,50)],
+ ],
+ description: '공군 관할 임자도서남역 훈련구역. 사용고도 50,000ft.',
+ source: '공군',
+ },
+ {
+ id: 'R-88', title: 'R-88 관할 백비(白碑)도서남역', org: '공군', area: '서해', level: 'danger',
+ lat: 37.10, lng: 126.10, altitude: '50,000ft',
+ polygon: [
+ [dms(37,1,21), dms(124,50,0)], [dms(37,2,0), dms(125,36,0)],
+ [dms(36,32,0), dms(125,36,0)], [dms(36,32,0), dms(125,0,0)],
+ ],
+ description: '공군 관할 백비도서남역 훈련구역. 사용고도 50,000ft.',
+ source: '공군',
+ },
+ {
+ id: 'R-97A', title: 'R-97A 서해안 대천(A)', org: '공군', area: '서해', level: 'danger',
+ lat: 36.28, lng: 126.15, altitude: '30,000ft',
+ polygon: [
+ [dms(36,20,0), dms(125,57,0)], [dms(36,20,0), dms(126,10,0)],
+ [dms(36,18,0), dms(126,25,0)], [dms(36,13,0), dms(126,31,0)],
+ ],
+ description: '공군 서해안 대천(A) 훈련구역. 사용고도 30,000ft.',
+ source: '공군',
+ },
+ {
+ id: 'R-97B', title: 'R-97B 서해안 대천(B)', org: '공군', area: '서해', level: 'danger',
+ lat: 36.35, lng: 126.50, altitude: '무한대',
+ polygon: [
+ [dms(36,20,0), dms(125,57,0)], [dms(36,20,0), dms(126,10,0)],
+ [dms(36,22,17), dms(126,14,41)], [dms(36,21,22), dms(126,30,7)],
+ [dms(36,14,0), dms(126,38,0)], [dms(36,53,0), dms(125,25,0)],
+ [dms(38,14,30), dms(125,57,0)],
+ ],
+ description: '공군 서해안 대천(B) 훈련구역. 사용고도 무한대.',
+ source: '공군',
+ },
+
+ // ═══════════════════════════════════════
+ // 육군 훈련구역
+ // ═══════════════════════════════════════
+ {
+ id: 'R-97E', title: 'R-97E 서해안 대천(E)', org: '육군', area: '서해', level: 'danger',
+ lat: 36.30, lng: 126.60, altitude: '30,000ft',
+ polygon: [
+ [dms(36,18,39), dms(126,19,2)], [dms(36,14,0), dms(126,38,0)],
+ [dms(36,6,21), dms(126,31,0)], [dms(36,11,49), dms(126,25,0)],
+ ],
+ description: '육군 서해안 대천(E) 훈련구역. 사용고도 30,000ft.',
+ source: '육군',
+ },
+ {
+ id: 'R-97F', title: 'R-97F 서해안 대천(F)', org: '육군', area: '서해', level: 'danger',
+ lat: 36.33, lng: 126.78, altitude: '15,000ft',
+ polygon: [
+ [dms(36,20,0), dms(126,31,0)], [dms(36,16,0), dms(126,35,0)],
+ [dms(36,0,0), dms(126,35,57)], [dms(36,17,18), dms(126,25,0)],
+ ],
+ description: '육군 서해안 대천(F) 훈련구역. 사용고도 15,000ft.',
+ source: '육군',
+ },
+ {
+ id: 'R-104', title: 'R-104 서해안 미어도부근', org: '육군', area: '서해', level: 'danger',
+ lat: dms(35,32,51), lng: dms(126,26,26), altitude: '15,000ft',
+ polygon: [[dms(35,32,51), dms(126,26,26)]], // 반경 5마일
+ description: '육군 서해안 미어도부근 훈련구역. 반경 5마일. 사용고도 15,000ft.',
+ source: '육군',
+ },
+ {
+ id: 'R-105', title: 'R-105 서해안 지도부근', org: '육군', area: '서해', level: 'danger',
+ lat: dms(35,53,26), lng: dms(126,4,16), altitude: '25,000ft',
+ polygon: [[dms(35,53,26), dms(126,4,16)]], // 반경 10마일
+ description: '육군 서해안 지도부근 훈련구역. 반경 10마일. 사용고도 25,000ft.',
+ source: '육군',
+ },
+ {
+ id: 'R-107', title: 'R-107 동해 강릉연안(다)', org: '육군', area: '동해', level: 'danger',
+ lat: 38.25, lng: 129.70, altitude: '40,000ft',
+ polygon: [
+ [dms(38,15,0), dms(129,15,30)], [dms(38,14,0), dms(130,0,0)],
+ [dms(37,47,0), dms(130,0,0)],
+ ],
+ description: '육군 동해 강릉연안(다) 훈련구역. 사용고도 40,000ft.',
+ source: '육군',
+ },
+];
diff --git a/src/services/opensky.ts b/src/services/opensky.ts
new file mode 100644
index 0000000..fa51e26
--- /dev/null
+++ b/src/services/opensky.ts
@@ -0,0 +1,586 @@
+import type { Aircraft, AircraftCategory } from '../types';
+
+// OpenSky Network API - free tier, no auth needed for basic queries
+const OPENSKY_BASE = 'https://opensky-network.org/api';
+
+// Middle East bounding box (lat_min, lat_max, lng_min, lng_max)
+const ME_BOUNDS = {
+ lamin: 24,
+ lamax: 42,
+ lomin: 30,
+ lomax: 62,
+};
+
+// Known military callsign prefixes
+const MILITARY_PREFIXES: Record = {
+ 'RCH': 'cargo', // C-17 / C-5 AMC
+ 'REACH': 'cargo',
+ 'KING': 'tanker', // HC-130 rescue tanker
+ 'ETHYL': 'tanker', // KC-135
+ 'STEEL': 'tanker', // KC-135
+ 'PACK': 'tanker', // KC-135
+ 'NCHO': 'tanker', // KC-10
+ 'JULEP': 'tanker',
+ 'IRON': 'fighter',
+ 'VIPER': 'fighter',
+ 'RAGE': 'fighter',
+ 'TOXIN': 'surveillance', // RC-135
+ 'OLIVE': 'surveillance', // RC-135
+ 'COBRA': 'surveillance',
+ 'FORTE': 'surveillance', // RQ-4 Global Hawk
+ 'HAWK': 'surveillance',
+ 'GLOBAL': 'surveillance',
+ 'SNTRY': 'surveillance', // E-3 AWACS
+ 'WIZARD': 'surveillance',
+ 'DOOM': 'military',
+ 'EVAC': 'military',
+ 'SAM': 'military', // VIP/govt
+ 'EXEC': 'military',
+ 'NAVY': 'military',
+ 'TOPCT': 'military',
+ 'DEATH': 'fighter', // B-2 Spirit
+ 'REAPER': 'surveillance', // MQ-9
+ 'DRAGON': 'surveillance', // U-2
+};
+
+function classifyAircraft(callsign: string): AircraftCategory {
+ const cs = callsign.toUpperCase().trim();
+ for (const [prefix, cat] of Object.entries(MILITARY_PREFIXES)) {
+ if (cs.startsWith(prefix)) return cat;
+ }
+ return 'civilian';
+}
+
+function parseOpenSkyResponse(data: { time: number; states: unknown[][] | null }): Aircraft[] {
+ if (!data.states) return [];
+
+ return data.states
+ .filter(s => s[6] !== null && s[5] !== null) // must have position
+ .map(s => {
+ const callsign = ((s[1] as string) || '').trim();
+ const category = classifyAircraft(callsign);
+ return {
+ icao24: s[0] as string,
+ callsign,
+ lat: s[6] as number,
+ lng: s[5] as number,
+ altitude: (s[7] as number) || 0,
+ velocity: (s[9] as number) || 0,
+ heading: (s[10] as number) || 0,
+ verticalRate: (s[11] as number) || 0,
+ onGround: s[8] as boolean,
+ category,
+ lastSeen: (s[4] as number) * 1000,
+ };
+ });
+}
+
+export async function fetchAircraftOpenSky(): Promise {
+ try {
+ const url = `${OPENSKY_BASE}/states/all?lamin=${ME_BOUNDS.lamin}&lomin=${ME_BOUNDS.lomin}&lamax=${ME_BOUNDS.lamax}&lomax=${ME_BOUNDS.lomax}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`OpenSky ${res.status}`);
+ const data = await res.json();
+ return parseOpenSkyResponse(data);
+ } catch (err) {
+ console.warn('OpenSky fetch failed, using sample data:', err);
+ return getSampleAircraft();
+ }
+}
+
+// ═══ Korea region ═══
+const KR_BOUNDS = { lamin: 20, lamax: 45, lomin: 115, lomax: 145 };
+
+export async function fetchAircraftOpenSkyKorea(): Promise {
+ try {
+ const url = `${OPENSKY_BASE}/states/all?lamin=${KR_BOUNDS.lamin}&lomin=${KR_BOUNDS.lomin}&lamax=${KR_BOUNDS.lamax}&lomax=${KR_BOUNDS.lomax}`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`OpenSky Korea ${res.status}`);
+ const data = await res.json();
+ return parseOpenSkyResponse(data);
+ } catch (err) {
+ console.warn('OpenSky Korea fetch failed:', err);
+ return [];
+ }
+}
+
+// T0 = main Iranian retaliation wave
+const T0 = new Date('2026-03-01T12:01:00Z').getTime();
+const HOUR = 3600_000;
+const MIN = 60_000;
+
+// ── 2026 March 1 verified aircraft deployments ──
+// Based on OSINT: Operation Epic Fury order of battle
+function getSampleAircraft(): Aircraft[] {
+ const now = Date.now();
+ return [
+ // ═══════════════════════════════════════════
+ // SURVEILLANCE / ISR (persistent coverage)
+ // ═══════════════════════════════════════════
+
+ // RQ-4B Global Hawk "FORTE12" - 24h ISR orbit over Iraq/Iran border
+ // Deployed from Al Dhafra, UAE (standard CENTCOM ISR asset)
+ {
+ icao24: 'ae1461', callsign: 'FORTE12', lat: 33.2, lng: 43.5, altitude: 16764,
+ velocity: 170, heading: 135, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'RQ4B', lastSeen: now,
+ activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ // MQ-4C Triton "FORTE13" - Maritime ISR over Persian Gulf
+ // One MQ-4C lost near Iran Feb 22; this is replacement sortie
+ {
+ icao24: 'ae1462', callsign: 'FORTE13', lat: 27.0, lng: 55.0, altitude: 15240,
+ velocity: 165, heading: 90, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'MQ4C', lastSeen: now,
+ activeStart: T0 - 10 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+ // RC-135V Rivet Joint "TOXIN31" - SIGINT, relocated to Crete (Souda Bay)
+ // Per OSINT: RC-135 shifted from Turkey-Syria border to Crete Mar 2026
+ {
+ icao24: 'ae5420', callsign: 'TOXIN31', lat: 35.5, lng: 34.0, altitude: 9144,
+ velocity: 230, heading: 90, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'RC135V', lastSeen: now,
+ activeStart: T0 - 10 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // E-3G AWACS "SNTRY60" - Airborne early warning over northern Iraq
+ // Al Dhafra-based, orbiting for Iranian missile launch detection
+ {
+ icao24: 'ae0005', callsign: 'SNTRY60', lat: 34.5, lng: 44.0, altitude: 9144,
+ velocity: 200, heading: 45, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'E3G', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+ // U-2S Dragon Lady "DRAGON01" - Ultra-high altitude recon from Al Dhafra
+ // Facilities hit by Iranian missile strike ~T0+1h; U-2 airborne before impact
+ {
+ icao24: 'ae0006', callsign: 'DRAGON01', lat: 30.5, lng: 52.0, altitude: 21336,
+ velocity: 200, heading: 0, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'U2S', lastSeen: now,
+ activeStart: T0 - 8 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ // MQ-9A Reaper "REAPER41" - Armed ISR over western Iraq
+ // Launched from Al Dhafra pre-strike; base facilities damaged by Iranian missiles
+ {
+ icao24: 'ae0007', callsign: 'REAPER41', lat: 32.0, lng: 41.5, altitude: 7620,
+ velocity: 80, heading: 180, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'MQ9A', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // MQ-9A Reaper "REAPER42" - Armed ISR over Strait of Hormuz
+ {
+ icao24: 'ae0008', callsign: 'REAPER42', lat: 26.5, lng: 56.0, altitude: 6096,
+ velocity: 75, heading: 270, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'MQ9A', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // TANKERS (aerial refueling for strike packages)
+ // ═══════════════════════════════════════════
+
+ // KC-135R "ETHYL71" - Refueling orbit over western Iraq for strike package
+ {
+ icao24: 'ae0001', callsign: 'ETHYL71', lat: 31.5, lng: 42.0, altitude: 10668,
+ velocity: 250, heading: 270, verticalRate: 0, onGround: false,
+ category: 'tanker', typecode: 'KC135R', lastSeen: now,
+ activeStart: T0 - 8 * HOUR, activeEnd: T0 + 6 * HOUR,
+ },
+ // KC-46A Pegasus "STEEL55" - Refueling support for B-2 bombers
+ // B-2s flew from Diego Garcia/Whiteman; needed multiple refueling
+ {
+ icao24: 'ae0009', callsign: 'STEEL55', lat: 28.5, lng: 50.0, altitude: 10972,
+ velocity: 245, heading: 315, verticalRate: 0, onGround: false,
+ category: 'tanker', typecode: 'KC46A', lastSeen: now,
+ activeStart: T0 - 10 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ // KC-135R "PACK22" - Refueling orbit over eastern Jordan
+ // Supporting Israeli F-35I and F-15I strike missions
+ {
+ icao24: 'ae0003', callsign: 'PACK22', lat: 32.8, lng: 38.5, altitude: 10972,
+ velocity: 245, heading: 180, verticalRate: 0, onGround: false,
+ category: 'tanker', typecode: 'KC135R', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ // KC-10A Extender "NCHO45" - Large refueling orbit over northern Gulf
+ {
+ icao24: 'ae0002', callsign: 'NCHO45', lat: 29.8, lng: 47.5, altitude: 10058,
+ velocity: 240, heading: 300, verticalRate: 0, onGround: false,
+ category: 'tanker', typecode: 'KC10A', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 5 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // BOMBERS (US stealth strikes on Iran)
+ // ═══════════════════════════════════════════
+
+ // B-2A Spirit "DEATH11" - Stealth bomber, Whiteman via Diego Garcia
+ // B-2s confirmed conducting deep strikes on Iranian nuclear/military facilities
+ {
+ icao24: 'ae2001', callsign: 'DEATH11', lat: 29.0, lng: 53.0, altitude: 12192,
+ velocity: 260, heading: 0, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'B2A', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 2 * HOUR,
+ },
+ // B-2A Spirit "DEATH12" - Second stealth bomber in pair
+ {
+ icao24: 'ae2002', callsign: 'DEATH12', lat: 28.5, lng: 52.5, altitude: 12192,
+ velocity: 260, heading: 10, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'B2A', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 2 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // US FIGHTERS (Al Udeid, Al Dhafra based)
+ // ═══════════════════════════════════════════
+
+ // F-22A Raptor "RAGE01" - Al Udeid-based, air superiority
+ {
+ icao24: 'ae3001', callsign: 'RAGE01', lat: 30.0, lng: 48.0, altitude: 10668,
+ velocity: 320, heading: 45, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F22A', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ // F-22A Raptor "RAGE02" - Wingman
+ {
+ icao24: 'ae3002', callsign: 'RAGE02', lat: 29.8, lng: 47.5, altitude: 10668,
+ velocity: 318, heading: 50, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F22A', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ // F-35A Lightning II "VIPER11" - From RAF Lakenheath (deployed to Al Udeid)
+ {
+ icao24: 'ae3003', callsign: 'VIPER11', lat: 31.0, lng: 46.0, altitude: 9144,
+ velocity: 300, heading: 60, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F35A', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+ // F-35A Lightning II "VIPER12" - Wingman
+ {
+ icao24: 'ae3004', callsign: 'VIPER12', lat: 30.8, lng: 45.5, altitude: 9144,
+ velocity: 298, heading: 65, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F35A', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+ // F-15E Strike Eagle "IRON41" - From RAF Lakenheath/Al Dhafra
+ // Deep strike mission into Iran
+ {
+ icao24: 'ae3005', callsign: 'IRON41', lat: 29.5, lng: 50.0, altitude: 8534,
+ velocity: 310, heading: 30, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F15E', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 2 * HOUR,
+ },
+ // F-15E Strike Eagle "IRON42" - Wingman
+ {
+ icao24: 'ae3006', callsign: 'IRON42', lat: 29.3, lng: 49.5, altitude: 8534,
+ velocity: 308, heading: 35, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F15E', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 2 * HOUR,
+ },
+ // F/A-18E/F Super Hornet "NAVY51" - USS Abraham Lincoln CVW
+ // Lincoln CSG in Arabian Sea; launching strikes on Iran
+ {
+ icao24: 'ae3007', callsign: 'NAVY51', lat: 24.0, lng: 60.0, altitude: 7620,
+ velocity: 280, heading: 350, verticalRate: 5, onGround: false,
+ category: 'fighter', typecode: 'FA18F', lastSeen: now,
+ activeStart: T0 - 1 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+ // F/A-18E Super Hornet "NAVY52" - Lincoln CAP flight
+ {
+ icao24: 'ae3008', callsign: 'NAVY52', lat: 23.5, lng: 59.5, altitude: 6096,
+ velocity: 275, heading: 0, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'FA18E', lastSeen: now,
+ activeStart: T0 - 1 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // ISRAELI AIR FORCE (strikes on Iran)
+ // ═══════════════════════════════════════════
+
+ // F-35I Adir "IRON33" - From Nevatim AB, deep strike into Iran
+ // F-35I confirmed shooting down Iranian Yak-130 on Mar 4
+ {
+ icao24: 'ae0012', callsign: 'IRON33', lat: 31.2, lng: 34.9, altitude: 10000,
+ velocity: 310, heading: 90, verticalRate: 3, onGround: false,
+ category: 'fighter', typecode: 'F35I', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+ // F-35I Adir "IRON34" - Wingman from Nevatim
+ {
+ icao24: 'ae4002', callsign: 'IRON34', lat: 31.0, lng: 35.2, altitude: 10000,
+ velocity: 308, heading: 95, verticalRate: 2, onGround: false,
+ category: 'fighter', typecode: 'F35I', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+ // F-15I Ra'am "RAGE22" - From Ramon AB, carrying Black Sparrow standoff missiles
+ {
+ icao24: 'ae0011', callsign: 'RAGE22', lat: 30.2, lng: 35.3, altitude: 7620,
+ velocity: 300, heading: 45, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F15I', lastSeen: now,
+ activeStart: T0 - 90 * MIN, activeEnd: T0 + 2 * HOUR,
+ },
+ // F-15I Ra'am "RAGE23" - Wingman
+ {
+ icao24: 'ae4004', callsign: 'RAGE23', lat: 30.0, lng: 35.5, altitude: 7620,
+ velocity: 298, heading: 50, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F15I', lastSeen: now,
+ activeStart: T0 - 90 * MIN, activeEnd: T0 + 2 * HOUR,
+ },
+ // F-16I Sufa "VIPER01" - CAP over Negev/Golan (intercept duty)
+ {
+ icao24: 'ae0010', callsign: 'VIPER01', lat: 30.5, lng: 35.0, altitude: 6096,
+ velocity: 280, heading: 60, verticalRate: 5, onGround: false,
+ category: 'fighter', typecode: 'F16I', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // CARGO / LOGISTICS
+ // ═══════════════════════════════════════════
+
+ // C-17A Globemaster III "RCH882" - Al Udeid → Al Asad logistics
+ {
+ icao24: 'ae0004', callsign: 'RCH882', lat: 29.0, lng: 48.0, altitude: 9753,
+ velocity: 220, heading: 340, verticalRate: -2, onGround: false,
+ category: 'cargo', typecode: 'C17A', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 - 1 * HOUR,
+ },
+ // C-17A "RCH445" - Post-strike logistics, Ramstein → Gulf
+ {
+ icao24: 'ae0013', callsign: 'RCH445', lat: 32.0, lng: 46.0, altitude: 10000,
+ velocity: 215, heading: 200, verticalRate: 0, onGround: false,
+ category: 'cargo', typecode: 'C17A', lastSeen: now,
+ activeStart: T0 + 2 * HOUR, activeEnd: T0 + 9 * HOUR,
+ },
+ // C-5M Super Galaxy "RCH901" - Heavy lift from Ramstein (ammo resupply)
+ {
+ icao24: 'ae5001', callsign: 'RCH901', lat: 35.0, lng: 38.0, altitude: 10668,
+ velocity: 230, heading: 120, verticalRate: 0, onGround: false,
+ category: 'cargo', typecode: 'C5M', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 5 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // CIVILIAN (diverted/cancelled due to airspace closures)
+ // ═══════════════════════════════════════════
+
+ // Qatar Airways QR8101 - Doha → Amman (before airspace closure)
+ {
+ icao24: '738012', callsign: 'QTR8101', lat: 25.3, lng: 51.5, altitude: 3048,
+ velocity: 130, heading: 300, verticalRate: -5, onGround: false,
+ category: 'civilian', lastSeen: now,
+ activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR,
+ },
+ // Emirates EK412 - Dubai → Istanbul (before airspace closure)
+ {
+ icao24: '710104', callsign: 'EK412', lat: 25.1, lng: 55.2, altitude: 10668,
+ velocity: 260, heading: 330, verticalRate: 3, onGround: false,
+ category: 'civilian', lastSeen: now,
+ activeStart: T0 - 8 * HOUR, activeEnd: T0 - 2 * HOUR,
+ },
+ // Qatar Airways QR306 - Doha → London (transiting region post-strike)
+ {
+ icao24: '738020', callsign: 'QTR306', lat: 26.0, lng: 50.5, altitude: 11000,
+ velocity: 240, heading: 315, verticalRate: 0, onGround: false,
+ category: 'civilian', lastSeen: now,
+ activeStart: T0 + 3 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+ // Etihad EY55 - Abu Dhabi → Cairo (post-strike, restricted routing)
+ {
+ icao24: '710110', callsign: 'ETD55', lat: 24.5, lng: 54.0, altitude: 10500,
+ velocity: 250, heading: 280, verticalRate: 2, onGround: false,
+ category: 'civilian', lastSeen: now,
+ activeStart: T0 + 3 * HOUR, activeEnd: T0 + 9 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // ADDITIONAL MILITARY — Coalition & Regional
+ // ═══════════════════════════════════════════
+
+ // RAF Typhoon FGR4 "TYPHON1" — RAF Akrotiri, Cyprus CAP
+ {
+ icao24: 'ae6001', callsign: 'TYPHON1', lat: 35.0, lng: 33.5, altitude: 9144,
+ velocity: 310, heading: 120, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'EUFI', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // RAF Typhoon FGR4 "TYPHON2"
+ {
+ icao24: 'ae6002', callsign: 'TYPHON2', lat: 34.8, lng: 33.8, altitude: 9144,
+ velocity: 308, heading: 125, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'EUFI', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // French Rafale "RAFALE1" — FS Charles de Gaulle launch
+ {
+ icao24: 'ae6003', callsign: 'RAFALE1', lat: 34.5, lng: 30.0, altitude: 10000,
+ velocity: 320, heading: 90, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'RFAL', lastSeen: now,
+ activeStart: T0 + 4 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+ // USAF E-8C JSTARS "WIZARD21" — Ground surveillance over Iraq
+ {
+ icao24: 'ae6004', callsign: 'WIZARD21', lat: 33.5, lng: 42.5, altitude: 10668,
+ velocity: 210, heading: 45, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'E8C', lastSeen: now,
+ activeStart: T0 - 8 * HOUR, activeEnd: T0 + 6 * HOUR,
+ },
+ // P-8A Poseidon "NAVY61" — Maritime patrol, Arabian Sea
+ {
+ icao24: 'ae6005', callsign: 'NAVY61', lat: 24.0, lng: 58.0, altitude: 7620,
+ velocity: 210, heading: 270, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'P8A', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // P-8A Poseidon "NAVY62" — Maritime patrol, Strait of Hormuz
+ {
+ icao24: 'ae6006', callsign: 'NAVY62', lat: 26.0, lng: 55.5, altitude: 6096,
+ velocity: 200, heading: 180, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'P8A', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 9 * HOUR,
+ },
+ // E-2D Hawkeye "NAVY71" — USS Abraham Lincoln AEW
+ {
+ icao24: 'ae6007', callsign: 'NAVY71', lat: 23.0, lng: 61.0, altitude: 7620,
+ velocity: 160, heading: 30, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'E2D', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 6 * HOUR,
+ },
+ // KC-135R "JULEP31" — Refueling over Saudi
+ {
+ icao24: 'ae6008', callsign: 'JULEP31', lat: 27.0, lng: 45.0, altitude: 10668,
+ velocity: 245, heading: 0, verticalRate: 0, onGround: false,
+ category: 'tanker', typecode: 'KC135R', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ // A330 MRTT "UAE50" — UAE Air Force tanker
+ {
+ icao24: 'ae6009', callsign: 'UAE50', lat: 25.0, lng: 53.0, altitude: 10000,
+ velocity: 240, heading: 315, verticalRate: 0, onGround: false,
+ category: 'tanker', typecode: 'A332', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 6 * HOUR,
+ },
+ // C-130J "RCH771" — Tactical airlift Kuwait
+ {
+ icao24: 'ae6010', callsign: 'RCH771', lat: 29.2, lng: 47.5, altitude: 6096,
+ velocity: 150, heading: 180, verticalRate: 0, onGround: false,
+ category: 'cargo', typecode: 'C130J', lastSeen: now,
+ activeStart: T0 - 2 * HOUR, activeEnd: T0 + 5 * HOUR,
+ },
+ // USAF B-1B Lancer "BONE21" — From Diego Garcia, standoff strikes
+ {
+ icao24: 'ae6011', callsign: 'BONE21', lat: 25.0, lng: 59.0, altitude: 10668,
+ velocity: 280, heading: 350, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'B1B', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 3 * HOUR,
+ },
+ // Saudi F-15SA "RSAF01" — Air defense patrol
+ {
+ icao24: 'ae6012', callsign: 'RSAF01', lat: 26.5, lng: 43.0, altitude: 9144,
+ velocity: 300, heading: 90, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F15SA', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+ // Saudi F-15SA "RSAF02"
+ {
+ icao24: 'ae6013', callsign: 'RSAF02', lat: 26.3, lng: 43.5, altitude: 9144,
+ velocity: 298, heading: 95, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F15SA', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+ // UAE F-16E "UAEAF1" — Al Dhafra CAP
+ {
+ icao24: 'ae6014', callsign: 'UAEAF1', lat: 24.5, lng: 55.0, altitude: 8534,
+ velocity: 290, heading: 60, verticalRate: 0, onGround: false,
+ category: 'fighter', typecode: 'F16E', lastSeen: now,
+ activeStart: T0 - 4 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // Israeli G550 "ORON01" — Oron SIGINT/AEW platform
+ {
+ icao24: 'ae6015', callsign: 'ORON01', lat: 31.0, lng: 34.5, altitude: 12192,
+ velocity: 200, heading: 45, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'G550', lastSeen: now,
+ activeStart: T0 - 3 * HOUR, activeEnd: T0 + 8 * HOUR,
+ },
+ // MQ-9 "REAPER43" — Over Yemen
+ {
+ icao24: 'ae6016', callsign: 'REAPER43', lat: 15.0, lng: 44.0, altitude: 6096,
+ velocity: 80, heading: 180, verticalRate: 0, onGround: false,
+ category: 'surveillance', typecode: 'MQ9A', lastSeen: now,
+ activeStart: T0 - 6 * HOUR, activeEnd: T0 + 10 * HOUR,
+ },
+
+ // ═══════════════════════════════════════════
+ // CIVILIAN — 영공 폐쇄 전/후 항공편 (중동 주요 노선)
+ // ═══════════════════════════════════════════
+
+ // ── 공습 전 출발 (T0-12h ~ T0-4h) ──
+ // 이란/이라크 영공 폐쇄 전 정상 운항
+ { icao24: 'c10001', callsign: 'QTR777', lat: 25.3, lng: 51.6, altitude: 10668, velocity: 250, heading: 310, verticalRate: 3, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 6 * HOUR },
+ { icao24: 'c10002', callsign: 'QTR003', lat: 26.0, lng: 50.0, altitude: 11000, velocity: 245, heading: 290, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR },
+ { icao24: 'c10003', callsign: 'UAE203', lat: 25.2, lng: 55.3, altitude: 10668, velocity: 260, heading: 320, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR },
+ { icao24: 'c10004', callsign: 'UAE501', lat: 25.1, lng: 55.2, altitude: 11000, velocity: 255, heading: 275, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 10 * HOUR, activeEnd: T0 - 4 * HOUR },
+ { icao24: 'c10005', callsign: 'ETD401', lat: 24.4, lng: 54.7, altitude: 10668, velocity: 248, heading: 310, verticalRate: 1, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 10 * HOUR, activeEnd: T0 - 4 * HOUR },
+ { icao24: 'c10006', callsign: 'GFA271', lat: 26.2, lng: 50.6, altitude: 10000, velocity: 240, heading: 300, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 6 * HOUR },
+ { icao24: 'c10007', callsign: 'SVA103', lat: 24.7, lng: 46.7, altitude: 10668, velocity: 250, heading: 350, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 6 * HOUR },
+ { icao24: 'c10008', callsign: 'SVA321', lat: 25.0, lng: 46.0, altitude: 11000, velocity: 255, heading: 45, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR },
+ { icao24: 'c10009', callsign: 'KAC501', lat: 29.2, lng: 47.9, altitude: 10668, velocity: 240, heading: 300, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR },
+ { icao24: 'c10010', callsign: 'OMA143', lat: 23.6, lng: 58.3, altitude: 10668, velocity: 248, heading: 315, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 10 * HOUR, activeEnd: T0 - 4 * HOUR },
+ { icao24: 'c10011', callsign: 'THY772', lat: 37.0, lng: 40.0, altitude: 11000, velocity: 240, heading: 270, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 6 * HOUR },
+ { icao24: 'c10012', callsign: 'THY784', lat: 38.0, lng: 38.0, altitude: 10668, velocity: 245, heading: 180, verticalRate: -1, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR },
+ { icao24: 'c10013', callsign: 'IRA712', lat: 35.7, lng: 51.4, altitude: 3048, velocity: 120, heading: 180, verticalRate: -5, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 11 * HOUR },
+ { icao24: 'c10014', callsign: 'IRA332', lat: 32.0, lng: 52.0, altitude: 10000, velocity: 230, heading: 90, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 11 * HOUR },
+ { icao24: 'c10015', callsign: 'BAW115', lat: 30.0, lng: 45.0, altitude: 11000, velocity: 250, heading: 130, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 6 * HOUR },
+ { icao24: 'c10016', callsign: 'DLH600', lat: 33.0, lng: 42.0, altitude: 11000, velocity: 248, heading: 120, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 6 * HOUR },
+ { icao24: 'c10017', callsign: 'AFR562', lat: 34.0, lng: 38.0, altitude: 11000, velocity: 245, heading: 110, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 11 * HOUR, activeEnd: T0 - 5 * HOUR },
+ { icao24: 'c10018', callsign: 'SIA472', lat: 28.0, lng: 55.0, altitude: 11000, velocity: 260, heading: 100, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 10 * HOUR, activeEnd: T0 - 4 * HOUR },
+ { icao24: 'c10019', callsign: 'CPA761', lat: 27.0, lng: 56.0, altitude: 10668, velocity: 255, heading: 80, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 10 * HOUR, activeEnd: T0 - 4 * HOUR },
+ { icao24: 'c10020', callsign: 'KAL505', lat: 29.0, lng: 50.0, altitude: 11000, velocity: 250, heading: 70, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 - 6 * HOUR },
+
+ // ── 영공 폐쇄 직전 긴급 이탈 (T0-6h ~ T0-2h) ──
+ { icao24: 'c10021', callsign: 'QTR008', lat: 26.5, lng: 49.0, altitude: 10668, velocity: 260, heading: 250, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 - 1 * HOUR },
+ { icao24: 'c10022', callsign: 'UAE022', lat: 25.5, lng: 54.0, altitude: 11000, velocity: 255, heading: 200, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 - 1 * HOUR },
+ { icao24: 'c10023', callsign: 'ETD203', lat: 24.8, lng: 54.5, altitude: 10000, velocity: 245, heading: 240, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 5 * HOUR, activeEnd: T0 - 1 * HOUR },
+ { icao24: 'c10024', callsign: 'FDB523', lat: 25.3, lng: 55.4, altitude: 9000, velocity: 230, heading: 190, verticalRate: -2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 5 * HOUR, activeEnd: T0 - 1 * HOUR },
+ { icao24: 'c10025', callsign: 'SVA661', lat: 26.0, lng: 44.0, altitude: 10668, velocity: 250, heading: 270, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 - 2 * HOUR },
+ { icao24: 'c10026', callsign: 'GFA511', lat: 26.5, lng: 50.3, altitude: 8000, velocity: 220, heading: 210, verticalRate: -3, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 5 * HOUR, activeEnd: T0 - 1 * HOUR },
+ { icao24: 'c10027', callsign: 'OMA155', lat: 24.0, lng: 57.0, altitude: 10668, velocity: 245, heading: 160, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 - 2 * HOUR },
+ { icao24: 'c10028', callsign: 'IAW117', lat: 33.3, lng: 44.4, altitude: 5000, velocity: 150, heading: 300, verticalRate: -4, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 - 4 * HOUR },
+ { icao24: 'c10029', callsign: 'THY790', lat: 37.5, lng: 36.0, altitude: 10668, velocity: 250, heading: 160, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 - 2 * HOUR },
+ { icao24: 'c10030', callsign: 'KAC303', lat: 29.0, lng: 48.2, altitude: 8000, velocity: 230, heading: 250, verticalRate: -2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 5 * HOUR, activeEnd: T0 - 1 * HOUR },
+
+ // ── 영공 폐쇄 구간 (T0-2h ~ T0+3h) — 거의 민간기 없음 ──
+ // 우회 항공편만 소수
+ { icao24: 'c10031', callsign: 'QTR922', lat: 28.0, lng: 42.0, altitude: 11000, velocity: 260, heading: 300, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 1 * HOUR, activeEnd: T0 + 3 * HOUR },
+ { icao24: 'c10032', callsign: 'UAE780', lat: 20.0, lng: 58.0, altitude: 11000, velocity: 255, heading: 250, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 - 1 * HOUR, activeEnd: T0 + 4 * HOUR },
+
+ // ── 영공 부분 개방 후 (T0+3h ~) ── 우회 노선으로 운항 재개
+ { icao24: 'c10033', callsign: 'QTR102', lat: 25.5, lng: 51.4, altitude: 10668, velocity: 248, heading: 280, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 3 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10034', callsign: 'QTR844', lat: 25.2, lng: 51.5, altitude: 11000, velocity: 252, heading: 60, verticalRate: 3, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 11 * HOUR },
+ { icao24: 'c10035', callsign: 'UAE412', lat: 25.3, lng: 55.2, altitude: 10668, velocity: 260, heading: 310, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 3 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10036', callsign: 'UAE506', lat: 25.0, lng: 55.0, altitude: 11000, velocity: 255, heading: 80, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 11 * HOUR },
+ { icao24: 'c10037', callsign: 'ETD231', lat: 24.5, lng: 54.6, altitude: 10000, velocity: 250, heading: 290, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 3 * HOUR, activeEnd: T0 + 9 * HOUR },
+ { icao24: 'c10038', callsign: 'ETD889', lat: 24.3, lng: 54.8, altitude: 11000, velocity: 248, heading: 100, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10039', callsign: 'SVA115', lat: 24.8, lng: 46.5, altitude: 10668, velocity: 245, heading: 340, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 3 * HOUR, activeEnd: T0 + 9 * HOUR },
+ { icao24: 'c10040', callsign: 'SVA717', lat: 24.5, lng: 46.8, altitude: 10668, velocity: 250, heading: 50, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10041', callsign: 'FDB881', lat: 25.1, lng: 55.5, altitude: 9500, velocity: 235, heading: 200, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10042', callsign: 'AXB221', lat: 25.0, lng: 55.1, altitude: 10668, velocity: 240, heading: 130, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10043', callsign: 'GFA891', lat: 26.3, lng: 50.5, altitude: 10000, velocity: 235, heading: 280, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 11 * HOUR },
+ { icao24: 'c10044', callsign: 'KAC117', lat: 29.5, lng: 47.8, altitude: 10668, velocity: 248, heading: 300, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10045', callsign: 'OMA303', lat: 23.8, lng: 57.5, altitude: 10668, velocity: 245, heading: 250, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 3 * HOUR, activeEnd: T0 + 9 * HOUR },
+ { icao24: 'c10046', callsign: 'THY801', lat: 37.2, lng: 37.0, altitude: 11000, velocity: 240, heading: 140, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 10 * HOUR },
+ { icao24: 'c10047', callsign: 'SIA478', lat: 22.0, lng: 58.0, altitude: 11000, velocity: 260, heading: 100, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10048', callsign: 'CPA763', lat: 21.0, lng: 59.0, altitude: 11000, velocity: 258, heading: 85, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 11 * HOUR },
+ { icao24: 'c10049', callsign: 'KAL507', lat: 25.0, lng: 52.0, altitude: 11000, velocity: 250, heading: 65, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10050', callsign: 'BAW117', lat: 30.0, lng: 38.0, altitude: 11000, velocity: 255, heading: 130, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10051', callsign: 'DLH602', lat: 33.0, lng: 35.0, altitude: 11000, velocity: 245, heading: 120, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 11 * HOUR },
+ { icao24: 'c10052', callsign: 'AFR564', lat: 34.0, lng: 33.0, altitude: 11000, velocity: 248, heading: 110, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10053', callsign: 'UAE870', lat: 25.2, lng: 55.3, altitude: 10668, velocity: 258, heading: 340, verticalRate: 3, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 6 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10054', callsign: 'QTR360', lat: 25.4, lng: 51.3, altitude: 10668, velocity: 252, heading: 320, verticalRate: 2, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 6 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10055', callsign: 'SVA401', lat: 24.9, lng: 46.6, altitude: 10668, velocity: 250, heading: 80, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 6 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10056', callsign: 'FDB301', lat: 25.0, lng: 55.2, altitude: 9000, velocity: 230, heading: 250, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 7 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10057', callsign: 'AIC115', lat: 25.3, lng: 55.0, altitude: 10668, velocity: 245, heading: 120, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10058', callsign: 'PAK302', lat: 25.5, lng: 55.3, altitude: 10000, velocity: 238, heading: 90, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 6 * HOUR, activeEnd: T0 + 12 * HOUR },
+ { icao24: 'c10059', callsign: 'RJA401', lat: 31.7, lng: 36.0, altitude: 10668, velocity: 240, heading: 180, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 5 * HOUR, activeEnd: T0 + 11 * HOUR },
+ { icao24: 'c10060', callsign: 'MEA341', lat: 33.8, lng: 35.5, altitude: 10000, velocity: 235, heading: 250, verticalRate: 0, onGround: false, category: 'civilian', lastSeen: now, activeStart: T0 + 6 * HOUR, activeEnd: T0 + 12 * HOUR },
+ ];
+}
diff --git a/src/services/osint.ts b/src/services/osint.ts
new file mode 100644
index 0000000..3530912
--- /dev/null
+++ b/src/services/osint.ts
@@ -0,0 +1,756 @@
+// ═══ Real-time OSINT Feed Service ═══
+// Fetches live news from GDELT Project API + Google News RSS
+// Focus: Iran, Hormuz Strait, Middle East military, oil/energy
+
+export interface OsintItem {
+ id: string;
+ timestamp: number; // unix ms
+ title: string;
+ source: string; // "Reuters", "AP", etc.
+ url: string;
+ category: 'military' | 'oil' | 'diplomacy' | 'shipping' | 'nuclear' | 'general' | 'maritime_accident' | 'fishing' | 'maritime_traffic';
+ language: 'en' | 'ko' | 'other';
+ imageUrl?: string;
+ lat?: number; // extracted from title keywords
+ lng?: number;
+}
+
+// ── Korean maritime location extraction ──
+const KOREA_LOCATIONS: [RegExp, number, number][] = [
+ // 주요 항구/해역
+ [/인천|경인/i, 37.45, 126.60],
+ [/평택|당진/i, 36.97, 126.83],
+ [/대산/i, 36.98, 126.35],
+ [/군산/i, 35.97, 126.65],
+ [/목포/i, 34.79, 126.38],
+ [/완도/i, 34.31, 126.76],
+ [/여수|광양/i, 34.74, 127.74],
+ [/통영/i, 34.85, 128.43],
+ [/거제/i, 34.88, 128.62],
+ [/마산|창원/i, 35.08, 128.60],
+ [/부산/i, 35.10, 129.04],
+ [/울산/i, 35.51, 129.39],
+ [/포항/i, 36.03, 129.37],
+ [/울릉/i, 37.48, 130.91],
+ [/독도/i, 37.24, 131.87],
+ [/묵호|동해시/i, 37.55, 129.11],
+ [/강릉/i, 37.75, 128.90],
+ [/속초/i, 38.20, 128.59],
+ [/제주/i, 33.51, 126.53],
+ [/서귀포/i, 33.24, 126.56],
+ [/마라도/i, 33.11, 126.27],
+ [/추자/i, 33.95, 126.30],
+ [/흑산/i, 34.68, 125.44],
+ [/백령/i, 37.97, 124.72],
+ [/연평/i, 37.67, 125.70],
+ [/대청/i, 37.83, 124.72],
+ [/진도/i, 34.38, 126.31],
+ [/태안/i, 36.75, 126.30],
+ [/보령/i, 36.35, 126.59],
+ [/서해|서쪽.*바다/i, 36.50, 125.50],
+ [/남해|남쪽.*바다/i, 34.50, 128.00],
+ [/동해|동쪽.*바다/i, 37.00, 130.00],
+ [/대한해협/i, 34.00, 128.50],
+ [/제주해협/i, 33.50, 126.80],
+ [/호르무즈|Hormuz/i, 26.56, 56.25],
+ [/페르시아만|Persian Gulf/i, 27.00, 51.50],
+];
+
+/** 제목에서 위치 추출 */
+export function extractLocation(title: string): { lat: number; lng: number } | null {
+ for (const [pattern, lat, lng] of KOREA_LOCATIONS) {
+ if (pattern.test(title)) return { lat, lng };
+ }
+ return null;
+}
+
+// ── Category classification by keywords ──
+const CATEGORY_RULES: [RegExp, OsintItem['category']][] = [
+ // Maritime-specific (must come before general shipping)
+ [/해양사고|해난|좌초|침몰|전복|충돌사고|구조작업|해상사고|조난|실종.*선|표류|구명|인명사고|maritime accident|collision.*vessel|capsiz|grounding|sinking|rescue.*sea|distress/i, 'maritime_accident'],
+ [/어선|어업|어획|수산|조업|불법조업|중국어선|오징어|꽃게|해삼|어민|fishing|trawler|illegal fishing|IUU|poaching|fishery|fish.*boat/i, 'fishing'],
+ [/해상교통|VTS|항로|선박통항|교통관제|해상안전|입출항|항만.*교통|해사|AIS.*추적|해양수산부|해양경찰청|해경|vessel traffic|sea lane|port congestion|maritime traffic|shipping lane|navigation.*safety|Ministry of Oceans|Korea Coast Guard/i, 'maritime_traffic'],
+ [/\b(strike|missile|attack|bomb|military|war|defense|weapon|drone|fighter|navy|army|air\s?force|IRGC|pentagon|carrier|destroyer|intercept|airstrike|combat|troop)\b/i, 'military'],
+ [/\b(oil|crude|WTI|brent|OPEC|barrel|petroleum|gas|LNG|energy|refiner|pipeline|tanker|fuel)\b/i, 'oil'],
+ [/\b(diplomacy|sanction|UN|NATO|treaty|negotiat|summit|ambassador|ceasefire|peace|talk|deal|accord)\b/i, 'diplomacy'],
+ [/\b(ship|vessel|maritime|strait|hormuz|port|cargo|shipping|blockade|naval|fleet|escort|piracy|AIS)\b/i, 'shipping'],
+ [/\b(nuclear|uranium|centrifuge|enrichment|IAEA|nonproliferation|warhead|plutonium)\b/i, 'nuclear'],
+];
+
+function classifyArticle(title: string): OsintItem['category'] {
+ for (const [pattern, cat] of CATEGORY_RULES) {
+ if (pattern.test(title)) return cat;
+ }
+ return 'general';
+}
+
+// ── GDELT DOC 2.0 API ──
+// Free, no auth, returns JSON with articles matching keywords
+// 이란 상황: 호르무즈 해협 중심
+const GDELT_KEYWORDS_IRAN = '"Strait of Hormuz" OR Hormuz OR "Persian Gulf" OR Iran OR IRGC OR "oil tanker" OR "Gulf shipping" OR "oil price" OR "Middle East oil"';
+// 한국 현황: 해양사고/해상교통/어선/수산/항만/해상안전
+const GDELT_KEYWORDS_KOREA = '"Korea coast guard" OR "Korea maritime accident" OR "Korea ship collision" OR "Korea vessel rescue" OR "Korea illegal fishing" OR "Korea NLL" OR "Korea Dokdo" OR "Korea sea patrol" OR "Korea ship sinking"';
+
+const GNEWS_KR_IRAN = '호르무즈 해협 OR 이란 유조선 OR 페르시아만 OR 중동 원유 OR 호르무즈 봉쇄 OR 이란 해군';
+const GNEWS_KR_KOREA = '해양사고 OR 해상구조 OR 해양경찰 OR 해양경찰청 OR 선박충돌 OR 선박좌초 OR 해상안전 OR 불법조업 OR 해상교통관제 OR 선박침몰 OR 해경 단속 OR NLL OR 독도 영해';
+
+const GNEWS_EN_IRAN = '"Strait of Hormuz" OR "Persian Gulf" OR "Iran oil" OR "Gulf tanker" OR "Iran navy" OR "Hormuz blockade"';
+const GNEWS_EN_KOREA = '"Korea maritime accident" OR "Korea fishing" OR "Korea port" OR "Korea coast guard" OR "Korea vessel traffic" OR "Korea sea safety"';
+
+async function fetchGDELT(keywords: string): Promise {
+ const query = encodeURIComponent(keywords);
+ const url = `/api/gdelt/api/v2/doc/doc?query=${query}&mode=ArtList&maxrecords=30&format=json&sort=DateDesc×pan=24h`;
+
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`GDELT ${res.status}`);
+ const data = await res.json();
+
+ if (!data.articles || !Array.isArray(data.articles)) return [];
+
+ return data.articles.map((a: Record, i: number) => {
+ const title = a.title || 'Untitled';
+ const loc = extractLocation(title);
+ return {
+ id: `gdelt-${i}-${a.seendate || Date.now()}`,
+ timestamp: a.seendate ? new Date(a.seendate.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z')).getTime() : Date.now(),
+ title,
+ source: a.domain || a.sourcecountry || 'Unknown',
+ url: a.url || '',
+ category: classifyArticle(title),
+ language: (a.language === 'Korean' ? 'ko' : a.language === 'English' ? 'en' : 'other') as OsintItem['language'],
+ imageUrl: a.socialimage || undefined,
+ ...(loc ? { lat: loc.lat, lng: loc.lng } : {}),
+ };
+ });
+ } catch (err) {
+ console.warn('GDELT fetch failed:', err);
+ return [];
+ }
+}
+
+// ── Google News RSS (Korean) ──
+async function fetchGoogleNewsKR(keywords: string): Promise {
+ const query = encodeURIComponent(keywords);
+ const url = `/api/rss/rss/search?q=${query}&hl=ko&gl=KR&ceid=KR:ko`;
+
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Google RSS ${res.status}`);
+ const text = await res.text();
+
+ const parser = new DOMParser();
+ const xml = parser.parseFromString(text, 'text/xml');
+ const items = xml.querySelectorAll('item');
+
+ const results: OsintItem[] = [];
+ items.forEach((item, i) => {
+ const title = item.querySelector('title')?.textContent || '';
+ const link = item.querySelector('link')?.textContent || '';
+ const pubDate = item.querySelector('pubDate')?.textContent || '';
+ const source = item.querySelector('source')?.textContent || '';
+
+ const loc = extractLocation(title);
+ results.push({
+ id: `gnews-kr-${i}-${Date.now()}`,
+ timestamp: pubDate ? new Date(pubDate).getTime() : Date.now(),
+ title,
+ source: source || 'Google News',
+ url: link,
+ category: classifyArticle(title),
+ language: 'ko',
+ ...(loc ? { lat: loc.lat, lng: loc.lng } : {}),
+ });
+ });
+
+ return results;
+ } catch (err) {
+ console.warn('Google News KR fetch failed:', err);
+ return [];
+ }
+}
+
+// ── Google News RSS (English) ──
+async function fetchGoogleNewsEN(keywords: string): Promise {
+ const query = encodeURIComponent(keywords);
+ const url = `/api/rss/rss/search?q=${query}&hl=en&gl=US&ceid=US:en`;
+
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Google RSS EN ${res.status}`);
+ const text = await res.text();
+
+ const parser = new DOMParser();
+ const xml = parser.parseFromString(text, 'text/xml');
+ const items = xml.querySelectorAll('item');
+
+ const results: OsintItem[] = [];
+ items.forEach((item, i) => {
+ const title = item.querySelector('title')?.textContent || '';
+ const link = item.querySelector('link')?.textContent || '';
+ const pubDate = item.querySelector('pubDate')?.textContent || '';
+ const source = item.querySelector('source')?.textContent || '';
+
+ const loc = extractLocation(title);
+ results.push({
+ id: `gnews-en-${i}-${Date.now()}`,
+ timestamp: pubDate ? new Date(pubDate).getTime() : Date.now(),
+ title,
+ source: source || 'Google News',
+ url: link,
+ category: classifyArticle(title),
+ language: 'en',
+ ...(loc ? { lat: loc.lat, lng: loc.lng } : {}),
+ });
+ });
+
+ return results;
+ } catch (err) {
+ console.warn('Google News EN fetch failed:', err);
+ return [];
+ }
+}
+
+// ── X.com (Twitter) — U.S. Central Command (@CENTCOM) RSS ──
+// 여러 Nitter 인스턴스 + RSSHub fallback
+// 중동 지역 위치 패턴 (CENTCOM 포스트용)
+const ME_LOCATIONS: [RegExp, number, number][] = [
+ [/Iran|Tehran|이란|테헤란/i, 35.69, 51.39],
+ [/Hormuz|호르무즈/i, 26.56, 56.25],
+ [/Iraq|Baghdad|이라크|바그다드/i, 33.31, 44.37],
+ [/Syria|시리아|Damascus/i, 33.51, 36.28],
+ [/Yemen|Houthi|예멘|후티|Sanaa/i, 15.37, 44.19],
+ [/Bahrain|바레인/i, 26.22, 50.60],
+ [/Qatar|카타르|Al Udeid/i, 25.12, 51.32],
+ [/UAE|Abu Dhabi|아부다비|Dubai/i, 24.25, 54.55],
+ [/Kuwait|쿠웨이트/i, 29.35, 47.52],
+ [/Erbil|에르빌/i, 36.19, 44.01],
+ [/Lebanon|Hezbollah|레바논|헤즈볼라/i, 33.85, 35.86],
+ [/Red Sea|홍해/i, 20.00, 38.00],
+ [/Gulf of Oman|오만만/i, 24.50, 58.50],
+ [/Bandar Abbas|반다르아바스/i, 27.18, 56.28],
+ [/Natanz|나탄즈/i, 33.72, 51.73],
+ [/Isfahan|이스파한/i, 32.65, 51.67],
+];
+
+function extractMELocation(text: string): { lat: number; lng: number } | null {
+ for (const [pattern, lat, lng] of ME_LOCATIONS) {
+ if (pattern.test(text)) return { lat, lng };
+ }
+ return null;
+}
+
+// ── CENTCOM 최신 게시물 (수동 업데이트 — RSS 대체) ──
+// Nitter/RSSHub 모두 X.com 차단으로 사용 불가하므로 주요 CENTCOM 게시물 수동 관리
+const CENTCOM_POSTS: { text: string; date: string; url: string }[] = [
+ // ── 3월 16일 (D+16) 최신 ──
+ {
+ text: 'CENTCOM: Isfahan military complex struck overnight by B-2 stealth bombers. 15 targets destroyed including underground command bunkers',
+ date: '2026-03-16T06:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'UPDATE: Iran FM Araghchi states "We never asked for a ceasefire." CENTCOM forces maintain full operational tempo across theater',
+ date: '2026-03-16T03:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ // ── 3월 15일 (D+15) ──
+ {
+ text: 'CENTCOM: Over 5,000 targets struck since Operation Epic Fury began. Iran offensive missile capability assessed below 3%',
+ date: '2026-03-15T18:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'NATO air defenses intercept THIRD Iranian ballistic missile over Turkish airspace near Hatay Province. No casualties reported',
+ date: '2026-03-15T12:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'CENTCOM confirms 8 US service members KIA, ~140 WIA since start of operations. KC-135 crash in Iraq accounts for 6 of the fatalities',
+ date: '2026-03-15T08:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ // ── 3월 14일 (D+14) ──
+ {
+ text: 'IEA announces largest-ever emergency oil stockpile release: 400 million barrels. Brent crude holds above $103/barrel despite release',
+ date: '2026-03-14T20:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'CENTCOM: Trump calls on Korea, Japan, China, France, UK to send warships to Strait of Hormuz to protect commercial shipping',
+ date: '2026-03-14T14:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'Pentagon vows to ramp up military campaign against Iran. New supreme leader Mojtaba Khamenei warns retaliatory attacks will continue',
+ date: '2026-03-14T06:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ // ── 3월 13일 (D+13) ──
+ {
+ text: 'CENTCOM: Hormuz Strait commercial shipping corridor now 95% mine-free. 28 mines cleared in past 72hrs by US, UK, French minesweeping forces',
+ date: '2026-03-13T08:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'CENTCOM forces intercept Iranian Shahed-136 drone swarm targeting Al Udeid Air Base, Qatar. All 8 drones destroyed by Patriot & C-RAM systems',
+ date: '2026-03-13T05:30:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'UPDATE: IRGC naval forces have effectively ceased offensive operations in the Strait of Hormuz. Remaining fast boats sheltering in Bandar Abbas harbor',
+ date: '2026-03-13T03:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'CENTCOM confirms successful strike on last known IRGC mobile missile TEL in western Iran near Tabriz. Iran\'s offensive missile capability assessed at less than 5%',
+ date: '2026-03-13T00:30:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ // ── 3월 12일 (D+12) ──
+ {
+ text: 'U.S., UK, and French naval forces establish Joint Maritime Security Corridor through Strait of Hormuz. First commercial convoy of 12 tankers transits safely',
+ date: '2026-03-12T18:00:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+ {
+ text: 'USS Michael Murphy (DDG 112) engaged and sank five IRGC fast attack craft in the Strait of Hormuz after they attempted to harass a commercial tanker convoy',
+ date: '2026-03-12T04:30:00Z',
+ url: 'https://x.com/CENTCOM',
+ },
+];
+
+async function fetchXCentcom(): Promise {
+ const results: OsintItem[] = [];
+
+ // 1) Nitter/RSSHub 시도 (동작할 경우)
+ const rssUrls = [
+ `/api/nitter1/CENTCOM/rss`,
+ `/api/nitter2/CENTCOM/rss`,
+ `/api/nitter3/CENTCOM/rss`,
+ `/api/rsshub/twitter/user/CENTCOM`,
+ ];
+
+ for (const url of rssUrls) {
+ try {
+ const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
+ if (!res.ok) continue;
+ const text = await res.text();
+ const parser = new DOMParser();
+ const xml = parser.parseFromString(text, 'text/xml');
+ let items = xml.querySelectorAll('item');
+ if (items.length === 0) items = xml.querySelectorAll('entry');
+ if (items.length === 0) continue;
+
+ items.forEach((item, i) => {
+ const rawTitle = item.querySelector('title')?.textContent || '';
+ const link = item.querySelector('link')?.textContent
+ || item.querySelector('link')?.getAttribute('href') || '';
+ const pubDate = item.querySelector('pubDate')?.textContent
+ || item.querySelector('published')?.textContent || '';
+ const desc = item.querySelector('description')?.textContent
+ || item.querySelector('content')?.textContent || '';
+
+ const cleanTitle = rawTitle.replace(/<[^>]+>/g, '').trim();
+ const cleanDesc = desc.replace(/<[^>]+>/g, '').trim();
+ const title = cleanTitle || cleanDesc.slice(0, 280);
+ if (!title || title.length < 5) return;
+
+ const loc = extractLocation(title + ' ' + cleanDesc)
+ || extractMELocation(title + ' ' + cleanDesc)
+ || { lat: 26.0, lng: 54.0 };
+ const xUrl = link.replace(/nitter\.[^/]+/, 'x.com').replace(/xcancel\.com/, 'x.com')
+ || 'https://x.com/CENTCOM';
+
+ results.push({
+ id: `x-centcom-${i}-${Date.now()}`,
+ timestamp: pubDate ? new Date(pubDate).getTime() : Date.now(),
+ title: `[CENTCOM] ${title}`,
+ source: 'X @CENTCOM',
+ url: xUrl,
+ category: classifyArticle(title + ' ' + cleanDesc) === 'general' ? 'military' : classifyArticle(title + ' ' + cleanDesc),
+ language: 'en',
+ lat: loc.lat,
+ lng: loc.lng,
+ });
+ });
+
+ if (results.length > 0) {
+ console.log(`[OSINT] X.com @CENTCOM: ${results.length} posts from RSS`);
+ return results;
+ }
+ } catch {
+ continue;
+ }
+ }
+
+ // 2) RSS 실패 시 → 수동 관리 CENTCOM 게시물 사용
+ console.log('[OSINT] X.com @CENTCOM: RSS unavailable, using curated posts');
+ return CENTCOM_POSTS.map((post, i) => {
+ const loc = extractMELocation(post.text) || { lat: 26.0, lng: 54.0 };
+ return {
+ id: `x-centcom-curated-${i}`,
+ timestamp: new Date(post.date).getTime(),
+ title: `[CENTCOM] ${post.text}`,
+ source: 'X @CENTCOM',
+ url: post.url,
+ category: classifyArticle(post.text) === 'general' ? 'military' : classifyArticle(post.text),
+ language: 'en' as const,
+ lat: loc.lat,
+ lng: loc.lng,
+ };
+ });
+}
+
+// ── Pinned OSINT articles (manually curated) ──
+const PINNED_IRAN: OsintItem[] = [
+ // ── 3월 16일 최신 ──
+ {
+ id: 'pinned-kr-isfahan-0316',
+ timestamp: new Date('2026-03-16T10:00:00+09:00').getTime(),
+ title: '[속보] 미-이스라엘, 이스파한 군사시설 야간 폭격… 이란 사망자 1,444명 돌파',
+ source: '연합뉴스',
+ url: 'https://www.yna.co.kr',
+ category: 'military',
+ language: 'ko',
+ lat: 32.65,
+ lng: 51.67,
+ },
+ {
+ id: 'pinned-kr-ceasefire-0316',
+ timestamp: new Date('2026-03-16T08:00:00+09:00').getTime(),
+ title: '이란 외무 "휴전 요청한 적 없다"… 트럼프 "이란이 딜 원해" 주장 정면 반박',
+ source: 'VOA Korea',
+ url: 'https://www.voakorea.com',
+ category: 'diplomacy',
+ language: 'ko',
+ lat: 35.69,
+ lng: 51.39,
+ },
+ // ── 3월 15일 ──
+ {
+ id: 'pinned-kr-hormuz-派兵-0315',
+ timestamp: new Date('2026-03-15T18:00:00+09:00').getTime(),
+ title: '[단독] 트럼프, 한국 등 5개국에 호르무즈 군함 파견 요구… 청해부대식 파병 논의',
+ source: '뉴데일리',
+ url: 'https://www.newdaily.co.kr',
+ category: 'military',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ {
+ id: 'pinned-kr-dispatch-debate-0315',
+ timestamp: new Date('2026-03-15T15:00:00+09:00').getTime(),
+ title: '[사설] 미국의 호르무즈 파병 요청, 이란전 참전 비칠 수 있어… 신중 대응 필요',
+ source: '경향신문',
+ url: 'https://www.khan.co.kr',
+ category: 'diplomacy',
+ language: 'ko',
+ lat: 37.57,
+ lng: 126.98,
+ },
+ {
+ id: 'pinned-kr-turkey-nato-0315',
+ timestamp: new Date('2026-03-15T12:00:00+09:00').getTime(),
+ title: 'NATO 방공망, 튀르키예 상공서 이란 탄도미사일 3번째 요격… Article 5 논의 가속',
+ source: 'BBC Korea',
+ url: 'https://www.bbc.com/korean',
+ category: 'military',
+ language: 'ko',
+ lat: 37.00,
+ lng: 35.43,
+ },
+ {
+ id: 'pinned-kr-kospi-0315',
+ timestamp: new Date('2026-03-15T09:00:00+09:00').getTime(),
+ title: '이란 전쟁 장기화에 코스피 4,800선 위협… 시총 500조원 이상 증발',
+ source: '뉴데일리',
+ url: 'https://biz.newdaily.co.kr',
+ category: 'oil',
+ language: 'ko',
+ lat: 37.57,
+ lng: 126.98,
+ },
+ // ── 3월 14일 ──
+ {
+ id: 'pinned-kr-iea-oil-0314',
+ timestamp: new Date('2026-03-14T20:00:00+09:00').getTime(),
+ title: 'IEA 사상 최대 4억 배럴 비축유 방출 결정… 브렌트유 $103 여전히 고공행진',
+ source: '매일경제',
+ url: 'https://www.mk.co.kr',
+ category: 'oil',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ {
+ id: 'pinned-kr-hormuz-shutdown-0314',
+ timestamp: new Date('2026-03-14T14:00:00+09:00').getTime(),
+ title: '호르무즈 해협 "사실상 마비"… 한국 원유 70% 중동산, 수급 비상등',
+ source: 'iFM',
+ url: 'https://news.ifm.kr',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ {
+ id: 'pinned-kr-tanker-0314',
+ timestamp: new Date('2026-03-14T10:00:00+09:00').getTime(),
+ title: '한국 해운사 시노코르, 유조선 용선료 10배 폭등 $50만/일… 전쟁 특수',
+ source: 'Bloomberg Korea',
+ url: 'https://www.bloomberg.com',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ // ── 3월 13일 ──
+ {
+ id: 'pinned-kr-hormuz-0313a',
+ timestamp: new Date('2026-03-13T09:00:00+09:00').getTime(),
+ title: '[속보] 호르무즈 해협 안전항행 회랑 설정… 한국행 유조선 3척 통과 성공',
+ source: '연합뉴스',
+ url: 'https://www.yna.co.kr',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ {
+ id: 'pinned-kr-hormuz-0313b',
+ timestamp: new Date('2026-03-13T08:00:00+09:00').getTime(),
+ title: '[단독] 청해부대 "문무대왕함" 호르무즈 해협 진입… 한국 선박 호위 개시',
+ source: 'KBS',
+ url: 'https://news.kbs.co.kr',
+ category: 'military',
+ language: 'ko',
+ lat: 26.30,
+ lng: 56.50,
+ },
+ {
+ id: 'pinned-kr-ship-0312',
+ timestamp: new Date('2026-03-12T18:00:00+09:00').getTime(),
+ title: '[긴급] 한국 LNG선 "SK이노베이션호" 이란 드론 피격… 선체 경미 손상, 승조원 무사',
+ source: 'SBS',
+ url: 'https://news.sbs.co.kr',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.20,
+ lng: 56.60,
+ },
+];
+
+// ── Pinned OSINT articles (Korea maritime/security) ──
+const PINNED_KOREA: OsintItem[] = [
+ // ── 3월 15일 최신 ──
+ {
+ id: 'pin-kr-nk-missile-0315',
+ timestamp: new Date('2026-03-15T07:00:00+09:00').getTime(),
+ title: '[속보] 북한, 동해상으로 탄도미사일 약 10발 발사… 350km 비행',
+ source: '연합뉴스',
+ url: 'https://www.yna.co.kr',
+ category: 'military',
+ language: 'ko',
+ lat: 39.00,
+ lng: 127.00,
+ },
+ {
+ id: 'pin-kr-nk-kimyojong-0315',
+ timestamp: new Date('2026-03-15T10:00:00+09:00').getTime(),
+ title: '김여정 "전술핵으로 상대 군사인프라 생존 불가"… 자유의 방패 훈련 반발',
+ source: 'KBS',
+ url: 'https://news.kbs.co.kr',
+ category: 'military',
+ language: 'ko',
+ lat: 39.00,
+ lng: 125.75,
+ },
+ {
+ id: 'pin-kr-hormuz-deploy-0315',
+ timestamp: new Date('2026-03-15T18:00:00+09:00').getTime(),
+ title: '트럼프, 한국 등 5개국에 호르무즈 군함 파견 요구… 청해부대 파병 논의 본격화',
+ source: '뉴데일리',
+ url: 'https://www.newdaily.co.kr',
+ category: 'military',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ {
+ id: 'pin-kr-kctu-0315',
+ timestamp: new Date('2026-03-15T14:00:00+09:00').getTime(),
+ title: '민주노총 "호르무즈 파병은 침략전쟁 참전"… 파병 반대 성명',
+ source: '경향신문',
+ url: 'https://www.khan.co.kr',
+ category: 'diplomacy',
+ language: 'ko',
+ lat: 37.57,
+ lng: 126.98,
+ },
+ // ── 3월 14일 ──
+ {
+ id: 'pin-kr-hormuz-zero-0314',
+ timestamp: new Date('2026-03-14T20:00:00+09:00').getTime(),
+ title: '[긴급] 호르무즈 해협 통항 제로… AIS 기준 양방향 선박 이동 완전 중단',
+ source: 'News1',
+ url: 'https://www.news1.kr',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+ {
+ id: 'pin-kr-freedom-shield-0314',
+ timestamp: new Date('2026-03-14T09:00:00+09:00').getTime(),
+ title: '한미 자유의 방패 2026 훈련 진행 중… 미군 일부 중동 전환 배치에도 "방위태세 문제 없어"',
+ source: 'MBC',
+ url: 'https://imnews.imbc.com',
+ category: 'military',
+ language: 'ko',
+ lat: 37.50,
+ lng: 127.00,
+ },
+ {
+ id: 'pin-kr-hmm-0314',
+ timestamp: new Date('2026-03-14T15:00:00+09:00').getTime(),
+ title: 'HMM 선박 6~7척 호르무즈 인근 대기 중… 해운업계 운임 50~80% 급등',
+ source: '해사신문',
+ url: 'https://www.haesanews.com',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.00,
+ lng: 56.00,
+ },
+ // ── 3월 13일 ──
+ {
+ id: 'pin-kr-fuel-cap-0313',
+ timestamp: new Date('2026-03-13T12:00:00+09:00').getTime(),
+ title: '[속보] 정부, 1997년 이후 첫 유류 가격 상한제 시행… 휘발유 1,724원/L 상한',
+ source: '서울경제',
+ url: 'https://en.sedaily.com',
+ category: 'oil',
+ language: 'ko',
+ lat: 37.57,
+ lng: 126.98,
+ },
+ {
+ id: 'pin-kr-coast-guard-0313',
+ timestamp: new Date('2026-03-13T08:00:00+09:00').getTime(),
+ title: '해경, 서해5도 꽃게 시즌 대비 중국 불법어선 단속 강화… 6척 나포, 241척 검문',
+ source: '아시아경제',
+ url: 'https://www.asiae.co.kr',
+ category: 'maritime_traffic',
+ language: 'ko',
+ lat: 37.67,
+ lng: 125.70,
+ },
+ {
+ id: 'pin-kr-nk-destroyer-0312',
+ timestamp: new Date('2026-03-12T16:00:00+09:00').getTime(),
+ title: '북한 최현급 구축함, 순항미사일 시험 발사 확인… VLS 88셀로 증강',
+ source: 'AEI/국방일보',
+ url: 'https://www.aei.org',
+ category: 'military',
+ language: 'ko',
+ lat: 39.80,
+ lng: 127.50,
+ },
+ {
+ id: 'pin-kr-oil-reserve-0312',
+ timestamp: new Date('2026-03-12T14:00:00+09:00').getTime(),
+ title: '한국, IEA 공조로 전략비축유 역대 최대 2,246만 배럴 방출… 잔여 7,764만 배럴',
+ source: '한국경제',
+ url: 'https://www.hankyung.com',
+ category: 'oil',
+ language: 'ko',
+ lat: 36.97,
+ lng: 126.83,
+ },
+ {
+ id: 'pin-kr-mof-emergency-0312',
+ timestamp: new Date('2026-03-12T10:00:00+09:00').getTime(),
+ title: '해양수산부 24시간 비상체제 가동… 호르무즈 인근 한국선박 40척 안전관리',
+ source: '해사신문',
+ url: 'https://www.haesanews.com',
+ category: 'shipping',
+ language: 'ko',
+ lat: 36.00,
+ lng: 127.00,
+ },
+ {
+ id: 'pin-kr-chinese-fishing-0311',
+ timestamp: new Date('2026-03-11T09:00:00+09:00').getTime(),
+ title: '서해 NLL 인근 중국 불법어선 하루 200척 이상… "어획량 1/3로 급감" 어민 호소',
+ source: '아시아A',
+ url: 'https://www.asiaa.co.kr',
+ category: 'fishing',
+ language: 'ko',
+ lat: 37.67,
+ lng: 125.50,
+ },
+ {
+ id: 'pin-kr-spring-safety-0311',
+ timestamp: new Date('2026-03-11T08:00:00+09:00').getTime(),
+ title: '해수부, 봄철 해양사고 예방대책 시행… 안개 충돌사고 대비 인천항 무인순찰로봇 도입',
+ source: 'iFM',
+ url: 'https://news.ifm.kr',
+ category: 'maritime_traffic',
+ language: 'ko',
+ lat: 37.45,
+ lng: 126.60,
+ },
+ {
+ id: 'pin-kr-ships-hormuz-0311',
+ timestamp: new Date('2026-03-11T07:00:00+09:00').getTime(),
+ title: '호르무즈 해협 내 한국 국적선 26척·한국인 144명 체류 확인… 미사일 100m 근접 피격 증언',
+ source: '서울신문',
+ url: 'https://www.seoul.co.kr',
+ category: 'shipping',
+ language: 'ko',
+ lat: 26.56,
+ lng: 56.25,
+ },
+];
+
+// ── Main fetch: merge all sources, deduplicate, sort by time ──
+export async function fetchOsintFeed(focus: 'iran' | 'korea' = 'iran'): Promise {
+ const gdeltKw = focus === 'korea' ? GDELT_KEYWORDS_KOREA : GDELT_KEYWORDS_IRAN;
+ const gnKrKw = focus === 'korea' ? GNEWS_KR_KOREA : GNEWS_KR_IRAN;
+ const gnEnKw = focus === 'korea' ? GNEWS_EN_KOREA : GNEWS_EN_IRAN;
+
+ const sources = [
+ fetchGDELT(gdeltKw),
+ fetchGoogleNewsKR(gnKrKw),
+ fetchGoogleNewsEN(gnEnKw),
+ ...(focus === 'iran' ? [fetchXCentcom()] : []),
+ ];
+ const results = await Promise.allSettled(sources);
+
+ const pinned = focus === 'iran' ? PINNED_IRAN : PINNED_KOREA;
+ const all: OsintItem[] = [
+ ...pinned,
+ ...results.flatMap(r => r.status === 'fulfilled' ? r.value : []),
+ ];
+
+ // Filter out irrelevant articles for Korea feed
+ const KOREA_NOISE = /쿠팡|수산시장|맛집|레시피|요리|축제|관광|여행|부동산|아파트|주식|코스피|연예|드라마|영화|스포츠|야구|축구|골프|coupang|recipe|tourism|real estate/i;
+ const filtered = focus === 'korea'
+ ? all.filter(item => !KOREA_NOISE.test(item.title))
+ : all;
+
+ // Deduplicate by similar title (first 40 chars)
+ const seen = new Set();
+ const unique = filtered.filter(item => {
+ const key = item.title.slice(0, 40).toLowerCase();
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+
+ // Sort newest first
+ unique.sort((a, b) => b.timestamp - a.timestamp);
+
+ return unique.slice(0, 50); // cap at 50 items
+}
diff --git a/src/services/piracy.ts b/src/services/piracy.ts
new file mode 100644
index 0000000..727aafa
--- /dev/null
+++ b/src/services/piracy.ts
@@ -0,0 +1,61 @@
+// ═══ 해적 위험 해역 데이터 ═══
+
+export interface PiracyZone {
+ id: string;
+ name: string;
+ nameKo: string;
+ lat: number;
+ lng: number;
+ level: 'critical' | 'high' | 'moderate';
+ description: string;
+ detail: string;
+ recentIncidents?: number; // 최근 1년 발생 건수
+}
+
+export const PIRACY_ZONES: PiracyZone[] = [
+ {
+ id: 'pz-singapore',
+ name: 'Singapore Strait',
+ nameKo: '싱가포르 해협',
+ lat: 1.18,
+ lng: 104.00,
+ level: 'critical',
+ description: '아시아 해적 사건 80% 이상 집중',
+ detail: '인도네시아 빈탄섬·바탐섬 북쪽 해역, 필립 채널(Phillip Channel) 부근. 야간(20:00~06:00) 항해 중 선박 침입하여 엔진 부품·선원 소지품 절도. 최근 총기 소지 비율 증가 추세.',
+ recentIncidents: 58,
+ },
+ {
+ id: 'pz-malacca',
+ name: 'Straits of Malacca',
+ nameKo: '말라카 해협',
+ lat: 2.50,
+ lng: 101.20,
+ level: 'high',
+ description: '세계 물동량 핵심 요충지, 좁은 수로',
+ detail: '싱가포르 해협과 연결되는 좁은 수로. 세계 물동량의 약 25% 통과. 좁은 통로 특성상 선박 감속 필수 → 해적 표적 용이. 인도네시아·말레이시아·태국 해역 접경 지대.',
+ recentIncidents: 22,
+ },
+ {
+ id: 'pz-sulu',
+ name: 'Sulu-Celebes Seas',
+ nameKo: '술루-셀레베스해',
+ lat: 5.80,
+ lng: 121.00,
+ level: 'moderate',
+ description: '무장 단체 선원 납치 위협',
+ detail: '필리핀 남부~말레이시아 사바주 동부 해역. 아부 사야프(Abu Sayyaf) 등 무장 단체의 선원 납치 사건 빈발 지역. 최근 3국 합동순찰(INDOMALPHI) 강화로 감소 추세이나 여전히 경계 필요.',
+ recentIncidents: 8,
+ },
+];
+
+export const PIRACY_LEVEL_COLOR: Record = {
+ critical: '#ef4444',
+ high: '#f97316',
+ moderate: '#eab308',
+};
+
+export const PIRACY_LEVEL_LABEL: Record = {
+ critical: '최고위험',
+ high: '고위험',
+ moderate: '주의',
+};
diff --git a/src/services/propagation.ts b/src/services/propagation.ts
new file mode 100644
index 0000000..ae83690
--- /dev/null
+++ b/src/services/propagation.ts
@@ -0,0 +1,451 @@
+import type { Aircraft, Ship } from '../types';
+
+// T0 = main strike moment
+const T0 = new Date('2026-03-01T12:01:00Z').getTime();
+const HOUR = 3600_000;
+const DEG2RAD = Math.PI / 180;
+
+// ── Waypoint system ──────────────────────────────────
+// Each waypoint: [hoursFromT0, lat, lng]
+type WP = [number, number, number];
+
+// Interpolate position along waypoints at a given time
+function interpWaypoints(wps: WP[], hoursFromT0: number): { lat: number; lng: number; heading: number } {
+ // Clamp to waypoint range
+ if (hoursFromT0 <= wps[0][0]) {
+ const next = wps.length > 1 ? wps[1] : wps[0];
+ return { lat: wps[0][1], lng: wps[0][2], heading: calcHeading(wps[0][1], wps[0][2], next[1], next[2]) };
+ }
+ if (hoursFromT0 >= wps[wps.length - 1][0]) {
+ const prev = wps.length > 1 ? wps[wps.length - 2] : wps[wps.length - 1];
+ const last = wps[wps.length - 1];
+ return { lat: last[1], lng: last[2], heading: calcHeading(prev[1], prev[2], last[1], last[2]) };
+ }
+
+ // Find segment
+ for (let i = 0; i < wps.length - 1; i++) {
+ const [t0, lat0, lng0] = wps[i];
+ const [t1, lat1, lng1] = wps[i + 1];
+ if (hoursFromT0 >= t0 && hoursFromT0 <= t1) {
+ const frac = (hoursFromT0 - t0) / (t1 - t0);
+ const lat = lat0 + (lat1 - lat0) * frac;
+ const lng = lng0 + (lng1 - lng0) * frac;
+ const heading = calcHeading(lat0, lng0, lat1, lng1);
+ return { lat, lng, heading };
+ }
+ }
+
+ // Fallback
+ return { lat: wps[0][1], lng: wps[0][2], heading: 0 };
+}
+
+function calcHeading(lat1: number, lng1: number, lat2: number, lng2: number): number {
+ const dLng = (lng2 - lng1) * DEG2RAD;
+ const y = Math.sin(dLng) * Math.cos(lat2 * DEG2RAD);
+ const x = Math.cos(lat1 * DEG2RAD) * Math.sin(lat2 * DEG2RAD) -
+ Math.sin(lat1 * DEG2RAD) * Math.cos(lat2 * DEG2RAD) * Math.cos(dLng);
+ return ((Math.atan2(y, x) / DEG2RAD) + 360) % 360;
+}
+
+// Generate trail by sampling recent positions
+function generateTrail(wps: WP[], hoursFromT0: number, count: number, intervalMin: number): [number, number][] {
+ const trail: [number, number][] = [];
+ for (let i = count; i >= 0; i--) {
+ const t = hoursFromT0 - (i * intervalMin) / 60;
+ const pos = interpWaypoints(wps, t);
+ trail.push([pos.lat, pos.lng]);
+ }
+ return trail;
+}
+
+// ── Aircraft Flight Plans ────────────────────────────
+// Waypoints: [hoursFromT0, lat, lng]
+
+const FLIGHT_PLANS: Record = {
+ // ── SURVEILLANCE / ISR ──
+
+ // FORTE12 (RQ-4B Global Hawk) - 24h ISR racetrack over Iraq/Iran border
+ 'ae1461': [
+ [-12, 33.0, 42.0], [-10, 34.0, 44.0], [-8, 33.0, 46.0], [-6, 34.0, 44.0],
+ [-4, 33.0, 42.0], [-2, 34.0, 44.0], [0, 33.5, 45.0], [2, 33.0, 42.0],
+ [4, 34.0, 44.0], [6, 33.0, 46.0], [8, 34.0, 44.0], [10, 33.0, 42.0],
+ [12, 34.0, 44.0],
+ ],
+ // FORTE13 (MQ-4C Triton) - Maritime ISR racetrack over Persian Gulf
+ 'ae1462': [
+ [-10, 27.0, 54.0], [-8, 26.5, 56.0], [-6, 27.5, 55.0], [-4, 26.5, 56.5],
+ [-2, 27.0, 54.0], [0, 26.5, 56.0], [2, 27.5, 55.0], [4, 26.5, 56.5],
+ [6, 27.0, 54.0], [8, 26.5, 56.0], [10, 27.5, 55.0],
+ ],
+ // TOXIN31 (RC-135V) - SIGINT orbit from Crete over Eastern Med
+ 'ae5420': [
+ [-10, 35.5, 26.0], [-8, 35.0, 30.0], [-6, 35.5, 34.0], [-4, 35.0, 30.0],
+ [-2, 35.5, 26.0], [0, 35.0, 30.0], [2, 35.5, 34.0], [4, 35.0, 30.0],
+ [6, 35.5, 26.0], [8, 35.0, 30.0],
+ ],
+ // SNTRY60 (E-3G AWACS) - AEW orbit over northern Iraq
+ 'ae0005': [
+ [-6, 35.0, 43.0], [-4, 34.5, 44.5], [-2, 35.0, 43.0], [-1, 34.5, 44.5],
+ [0, 35.0, 43.5], [1, 34.5, 44.5], [2, 35.0, 43.0], [4, 34.5, 44.5],
+ [6, 35.0, 43.0], [8, 34.5, 44.5], [10, 35.0, 43.0],
+ ],
+ // DRAGON01 (U-2S) - Ultra-high altitude recon over Iran
+ 'ae0006': [
+ [-8, 24.2, 54.7], [-6, 28.0, 52.0], [-4, 31.0, 53.0], [-2, 33.0, 52.0],
+ [0, 30.5, 52.0], [2, 28.0, 53.0], [4, 25.0, 54.0],
+ ],
+ // REAPER41 (MQ-9A) - Armed ISR racetrack over western Iraq
+ 'ae0007': [
+ [-6, 33.0, 42.0], [-4, 32.0, 41.0], [-2, 33.0, 42.5], [0, 32.0, 41.5],
+ [2, 33.0, 42.0], [4, 32.0, 41.0], [6, 33.0, 42.5], [8, 32.0, 41.5],
+ ],
+ // REAPER42 (MQ-9A) - Armed ISR over Strait of Hormuz
+ 'ae0008': [
+ [-4, 26.5, 56.5], [-2, 27.0, 55.5], [0, 26.5, 56.0], [2, 27.0, 55.5],
+ [4, 26.5, 56.5], [6, 27.0, 55.5], [8, 26.5, 56.0], [10, 27.0, 55.5],
+ ],
+
+ // ── TANKERS ──
+
+ // ETHYL71 (KC-135R) - Refueling orbit over western Iraq
+ 'ae0001': [
+ [-8, 32.5, 40.0], [-6, 31.5, 41.5], [-4, 32.5, 40.0], [-2, 31.5, 41.5],
+ [-1, 32.0, 40.5], [0, 31.5, 41.5], [1, 32.5, 40.0], [3, 31.5, 41.5],
+ [5, 32.5, 40.0], [6, 32.0, 40.5],
+ ],
+ // STEEL55 (KC-46A) - B-2 tanker support orbit over northern Gulf
+ 'ae0009': [
+ [-10, 28.0, 49.0], [-8, 29.0, 50.5], [-6, 28.0, 49.0], [-4, 29.0, 50.5],
+ [-2, 28.0, 49.0], [0, 29.0, 50.5], [2, 28.0, 49.0], [4, 28.5, 50.0],
+ ],
+ // PACK22 (KC-135R) - Refueling over eastern Jordan (Israeli strikes)
+ 'ae0003': [
+ [-3, 32.0, 37.5], [-1.5, 33.0, 39.0], [0, 32.0, 37.5], [1.5, 33.0, 39.0],
+ [3, 32.0, 37.5], [4, 32.5, 38.0],
+ ],
+ // NCHO45 (KC-10A) - Refueling orbit over Kuwait/northern Gulf
+ 'ae0002': [
+ [-4, 29.5, 47.0], [-2, 30.2, 48.5], [0, 29.5, 47.0], [1, 30.2, 48.5],
+ [3, 29.5, 47.0], [5, 30.2, 48.5],
+ ],
+
+ // ── BOMBERS ──
+
+ // DEATH11 (B-2A Spirit) - Ingress from south, strike Tehran/Isfahan, egress
+ 'ae2001': [
+ [-6, 22.0, 58.0], [-4, 25.0, 55.0], [-2, 28.0, 53.0], [-1, 30.0, 52.0],
+ [0, 32.5, 51.5], [1, 30.0, 53.0], [2, 26.0, 56.0],
+ ],
+ // DEATH12 (B-2A Spirit) - Second bomber, offset route
+ 'ae2002': [
+ [-6, 21.5, 59.0], [-4, 24.5, 56.0], [-2, 27.5, 53.5], [-1, 29.5, 52.5],
+ [0, 32.0, 52.0], [1, 29.5, 53.5], [2, 25.5, 57.0],
+ ],
+
+ // ── US FIGHTERS ──
+
+ // RAGE01 (F-22A) - Al Udeid scramble, air superiority sweep
+ 'ae3001': [
+ [-2, 25.2, 51.4], [-1.5, 27.0, 50.0], [-1, 29.0, 48.5], [0, 31.0, 47.0],
+ [1, 32.5, 48.0], [2, 31.0, 47.0], [3, 28.0, 49.0], [4, 25.5, 51.0],
+ ],
+ // RAGE02 (F-22A) - Wingman
+ 'ae3002': [
+ [-2, 25.0, 51.2], [-1.5, 26.8, 49.8], [-1, 28.8, 48.3], [0, 30.8, 46.8],
+ [1, 32.3, 47.8], [2, 30.8, 46.8], [3, 27.8, 48.8], [4, 25.3, 50.8],
+ ],
+ // VIPER11 (F-35A) - Deep strike into Iran from Al Udeid
+ 'ae3003': [
+ [-3, 25.2, 51.4], [-2, 28.0, 50.0], [-1, 31.0, 49.0], [0, 33.0, 50.0],
+ [1, 31.0, 49.0], [2, 28.0, 50.0], [3, 25.2, 51.4],
+ ],
+ // VIPER12 (F-35A) - Wingman
+ 'ae3004': [
+ [-3, 25.0, 51.2], [-2, 27.8, 49.8], [-1, 30.8, 48.8], [0, 32.8, 49.8],
+ [1, 30.8, 48.8], [2, 27.8, 49.8], [3, 25.0, 51.2],
+ ],
+ // IRON41 (F-15E) - Strike Eagles, deep strike
+ 'ae3005': [
+ [-4, 24.2, 54.7], [-3, 27.0, 53.0], [-2, 29.5, 51.0], [-1, 31.5, 50.0],
+ [0, 33.5, 51.5], [1, 31.0, 50.5], [2, 27.0, 53.0],
+ ],
+ // IRON42 (F-15E) - Wingman
+ 'ae3006': [
+ [-4, 24.0, 54.5], [-3, 26.8, 52.8], [-2, 29.3, 50.8], [-1, 31.3, 49.8],
+ [0, 33.3, 51.3], [1, 30.8, 50.3], [2, 26.8, 52.8],
+ ],
+ // NAVY51 (F/A-18F) - Launch from USS Abraham Lincoln, strike & return
+ 'ae3007': [
+ [-1, 23.5, 62.0], [-0.5, 24.5, 59.0], [0, 26.0, 57.0], [1, 27.5, 55.5],
+ [2, 25.0, 58.0], [3, 23.5, 62.0],
+ ],
+ // NAVY52 (F/A-18E) - Lincoln CAP
+ 'ae3008': [
+ [-1, 23.0, 62.5], [-0.5, 24.0, 60.0], [0, 24.5, 59.0], [1, 24.0, 60.5],
+ [2, 23.5, 61.5], [3, 23.0, 62.5],
+ ],
+
+ // ── ISRAELI FIGHTERS ──
+
+ // IRON33 (F-35I Adir) - Nevatim → Iran strike → return
+ 'ae0012': [
+ [-2, 31.2, 34.9], [-1.5, 31.8, 36.0], [-1, 32.5, 38.0], [-0.5, 33.0, 42.0],
+ [0, 33.5, 48.0], [0.5, 33.0, 45.0], [1, 32.5, 40.0], [1.5, 32.0, 37.0],
+ [2, 31.5, 35.5], [3, 31.2, 34.9],
+ ],
+ // IRON34 (F-35I Adir) - Wingman
+ 'ae4002': [
+ [-2, 31.0, 35.2], [-1.5, 31.6, 36.2], [-1, 32.3, 38.2], [-0.5, 32.8, 42.2],
+ [0, 33.3, 47.8], [0.5, 32.8, 44.8], [1, 32.3, 39.8], [1.5, 31.8, 37.2],
+ [2, 31.3, 35.7], [3, 31.0, 35.2],
+ ],
+ // RAGE22 (F-15I Ra'am) - Ramon → eastern Iraq standoff launch → return
+ 'ae0011': [
+ [-1.5, 30.0, 34.8], [-1, 30.5, 36.0], [-0.5, 31.0, 38.5], [0, 31.5, 40.0],
+ [0.5, 31.0, 38.5], [1, 30.5, 36.0], [1.5, 30.2, 35.0], [2, 30.0, 34.8],
+ ],
+ // RAGE23 (F-15I Ra'am) - Wingman
+ 'ae4004': [
+ [-1.5, 29.8, 35.0], [-1, 30.3, 36.2], [-0.5, 30.8, 38.7], [0, 31.3, 40.2],
+ [0.5, 30.8, 38.7], [1, 30.3, 36.2], [1.5, 30.0, 35.2], [2, 29.8, 35.0],
+ ],
+ // VIPER01 (F-16I Sufa) - CAP orbit over Negev/Golan
+ 'ae0010': [
+ [-2, 30.8, 34.7], [-1.5, 31.5, 35.5], [-1, 32.5, 35.8], [-0.5, 33.0, 35.5],
+ [0, 32.5, 35.8], [0.5, 31.5, 35.5], [1, 30.8, 34.7], [1.5, 31.5, 35.5],
+ [2, 32.5, 35.8], [3, 31.5, 35.0],
+ ],
+
+ // ── CARGO ──
+
+ // RCH882 (C-17A) - Al Udeid → Al Asad
+ 'ae0004': [
+ [-6, 25.1, 51.3], [-4.5, 27.0, 49.0], [-3, 29.0, 47.0], [-1.5, 31.5, 44.5],
+ [-1, 33.8, 42.4],
+ ],
+ // RCH445 (C-17A) - Ramstein → Gulf post-strike resupply
+ 'ae0013': [
+ [2, 36.0, 40.0], [4, 33.0, 43.0], [6, 30.0, 46.0], [8, 27.0, 49.0],
+ [9, 25.1, 51.3],
+ ],
+ // RCH901 (C-5M) - Ramstein → Al Udeid heavy lift
+ 'ae5001': [
+ [-3, 37.0, 30.0], [-1.5, 35.0, 35.0], [0, 33.0, 40.0], [2, 30.0, 45.0],
+ [4, 27.0, 49.0], [5, 25.1, 51.3],
+ ],
+
+ // ── CIVILIAN ──
+
+ // QTR8101 - Doha → Amman
+ '738012': [
+ [-11, 25.3, 51.6], [-9, 27.5, 48.0], [-7, 30.0, 43.0], [-5, 31.9, 36.0],
+ ],
+ // EK412 - Dubai → Istanbul
+ '710104': [
+ [-8, 25.3, 55.3], [-6, 28.0, 50.0], [-4, 32.0, 44.0], [-2, 37.0, 38.0],
+ ],
+ // QTR306 - Doha → London (post-strike, southern routing to avoid Iranian airspace)
+ '738020': [
+ [3, 25.3, 51.6], [5, 26.0, 45.0], [7, 30.0, 38.0], [9, 34.0, 32.0],
+ [10, 38.0, 28.0],
+ ],
+ // ETD55 - Abu Dhabi → Cairo (restricted routing post-strike)
+ '710110': [
+ [3, 24.4, 54.7], [5, 26.0, 48.0], [7, 28.5, 40.0], [9, 30.1, 31.4],
+ ],
+};
+
+// ── Aircraft Propagation ─────────────────────────────
+
+export function propagateAircraft(
+ baseAircraft: Aircraft[],
+ currentTime: number,
+): Aircraft[] {
+ const hoursFromT0 = (currentTime - T0) / HOUR;
+
+ // Filter to only aircraft active at this time
+ const active = baseAircraft.filter(ac => {
+ if (ac.activeStart != null && currentTime < ac.activeStart) return false;
+ if (ac.activeEnd != null && currentTime > ac.activeEnd) return false;
+ return true;
+ });
+
+ return active.map(ac => {
+ const flightPlan = FLIGHT_PLANS[ac.icao24];
+
+ if (flightPlan) {
+ // Waypoint-based interpolation
+ const pos = interpWaypoints(flightPlan, hoursFromT0);
+ const trail = generateTrail(flightPlan, hoursFromT0, 5, 10); // 5 points, 10min intervals
+
+ return {
+ ...ac,
+ lat: pos.lat,
+ lng: pos.lng,
+ heading: pos.heading,
+ trail,
+ };
+ }
+
+ // No flight plan - keep static position (for API-sourced aircraft)
+ return ac;
+ });
+}
+
+// ── Ship Waypoints ───────────────────────────────────
+
+const SHIP_PLANS: Record = {
+ // USS Abraham Lincoln CSG - Arabian Sea patrol
+ '369970072': [
+ [-12, 23.0, 62.0], [-8, 23.5, 61.0], [-4, 23.0, 62.0], [0, 23.5, 61.5],
+ [4, 23.0, 62.0], [8, 23.5, 61.0], [12, 23.0, 62.0],
+ ],
+ // USS Frank E. Petersen Jr DDG-121 - escort ahead
+ '369970121': [
+ [-12, 23.3, 61.8], [-8, 23.8, 60.8], [-4, 23.3, 61.8], [0, 23.8, 61.3],
+ [4, 23.3, 61.8], [8, 23.8, 60.8], [12, 23.3, 61.8],
+ ],
+ // USS Spruance DDG-111 - screen
+ '369970111': [
+ [-12, 22.7, 62.3], [-8, 23.2, 61.3], [-4, 22.7, 62.3], [0, 23.2, 61.8],
+ [4, 22.7, 62.3], [8, 23.2, 61.3], [12, 22.7, 62.3],
+ ],
+ // USS Michael Murphy DDG-112 - screen
+ '369970112': [
+ [-12, 23.5, 62.5], [-8, 24.0, 61.5], [-4, 23.5, 62.5], [0, 24.0, 62.0],
+ [4, 23.5, 62.5], [8, 24.0, 61.5], [12, 23.5, 62.5],
+ ],
+ // USS Gerald R. Ford CVN-78 - Red Sea
+ '369970078': [
+ [-12, 18.5, 39.0], [-8, 19.0, 38.5], [-4, 18.5, 39.0], [0, 19.0, 38.5],
+ [4, 18.5, 39.0], [8, 19.0, 38.5], [12, 18.5, 39.0],
+ ],
+ // USS McFaul DDG-74 - Arabian Sea
+ '369970074': [
+ [-12, 22.0, 60.0], [-8, 22.5, 59.5], [-4, 22.0, 60.0], [0, 22.5, 59.5],
+ [4, 22.0, 60.0], [8, 22.5, 59.5], [12, 22.0, 60.0],
+ ],
+ // USS John Finn DDG-113 - Arabian Sea
+ '369970113': [
+ [-12, 24.0, 59.0], [-8, 24.5, 58.5], [-4, 24.0, 59.0], [0, 24.5, 58.5],
+ [4, 24.0, 59.0], [8, 24.5, 58.5], [12, 24.0, 59.0],
+ ],
+ // USS Milius DDG-69 - Northern Arabian Sea
+ '369970069': [
+ [-12, 25.0, 58.0], [-8, 25.5, 57.5], [-4, 25.0, 58.0], [0, 25.5, 57.5],
+ [4, 25.0, 58.0], [8, 25.5, 57.5], [12, 25.0, 58.0],
+ ],
+ // USS Delbert D. Black DDG-119 - Aegis BMD station
+ '369970119': [
+ [-12, 26.0, 57.0], [-8, 26.5, 56.5], [-4, 26.0, 57.0], [0, 26.5, 56.5],
+ [4, 26.0, 57.0], [8, 26.5, 56.5], [12, 26.0, 57.0],
+ ],
+ // USS Pinckney DDG-91 - Arabian Sea patrol
+ '369970091': [
+ [-12, 21.0, 61.0], [-8, 21.5, 60.5], [-4, 21.0, 61.0], [0, 21.5, 60.5],
+ [4, 21.0, 61.0], [8, 21.5, 60.5], [12, 21.0, 61.0],
+ ],
+ // USS Mitscher DDG-57 - Aegis BMD station
+ '369970057': [
+ [-12, 25.5, 59.0], [-8, 26.0, 58.5], [-4, 25.5, 59.0], [0, 26.0, 58.5],
+ [4, 25.5, 59.0], [8, 26.0, 58.5], [12, 25.5, 59.0],
+ ],
+ // USS Canberra LCS-30 - Persian Gulf patrol (central Gulf, away from Qatar coast)
+ '369970030': [
+ [-12, 27.2, 51.5], [-8, 27.5, 51.0], [-4, 27.2, 51.5], [0, 27.5, 51.0],
+ [4, 27.2, 51.5], [8, 27.5, 51.0], [12, 27.2, 51.5],
+ ],
+ // USS Tulsa LCS-16 - Persian Gulf patrol (east of Qatar, open water)
+ '369970016': [
+ [-12, 26.2, 53.0], [-8, 26.5, 52.5], [-4, 26.2, 53.0], [0, 26.5, 52.5],
+ [4, 26.2, 53.0], [8, 26.5, 52.5], [12, 26.2, 53.0],
+ ],
+ // HMS Diamond D34 - re-deployed to Gulf
+ '232001034': [
+ [-6, 25.0, 57.0], [-3, 25.5, 56.5], [0, 26.0, 56.5], [3, 26.2, 56.0],
+ [6, 25.8, 56.5], [9, 26.2, 56.0], [12, 25.8, 56.5],
+ ],
+ // HMS Middleton M34 - minesweeper, leaving early (Gulf of Oman → Arabian Sea)
+ '232001041': [
+ [-12, 26.0, 56.8], [-8, 25.5, 57.0], [-4, 25.0, 57.5], [0, 24.5, 58.0],
+ ],
+ // FS Languedoc D653 - off Cyprus
+ '227001653': [
+ [-8, 34.8, 33.0], [-4, 34.5, 33.5], [0, 34.2, 33.0], [4, 34.5, 33.5],
+ [8, 34.2, 33.0], [12, 34.5, 33.5],
+ ],
+ // FS Charles de Gaulle R91 - 크레타 → 동부 지중해 진입 (T0+4h~)
+ '227001091': [
+ [4, 35.0, 28.0], [6, 34.8, 29.5], [8, 34.5, 31.0], [10, 34.2, 32.5],
+ [12, 34.0, 33.5],
+ ],
+ // IRGCN Fast Attack 1 - Strait of Hormuz
+ '422001001': [
+ [-10, 26.2, 54.5], [-6, 26.5, 54.0], [-3, 26.8, 53.5], [0, 26.5, 54.0],
+ [2, 26.8, 54.5], [4, 27.0, 55.0],
+ ],
+ // IRGCN Fast Attack 2
+ '422001002': [
+ [-10, 27.0, 54.0], [-6, 26.8, 53.5], [-3, 26.5, 53.0], [0, 26.8, 53.5],
+ [2, 27.0, 53.8],
+ ],
+ // IRGCN Fast Attack 3
+ '422001003': [
+ [-10, 26.0, 55.0], [-6, 26.3, 54.5], [-3, 26.6, 54.0], [0, 26.3, 54.5],
+ [2, 26.0, 55.0],
+ ],
+ // IRIS Dena F75 - 호르무즈 해협 인근 이란 해군 초계
+ '422010001': [
+ [-12, 25.8, 57.2], [-8, 25.5, 57.5], [-4, 25.2, 57.8], [0, 25.5, 58.2],
+ [4, 25.8, 57.8], [8, 26.0, 57.5], [12, 25.5, 57.2],
+ ],
+ // Eagle Vellore - Korean VLCC escaping Hormuz before blockade
+ // Fixed: start from Al Basra oil terminal (sea), route through Persian Gulf → Hormuz → Arabian Sea
+ '440107001': [
+ [-6, 29.8, 48.8], [-5, 28.5, 49.5], [-4, 27.2, 51.5], [-3, 26.8, 53.0],
+ [-2, 26.5, 55.0], [-1, 26.0, 56.3], [0, 25.5, 57.5],
+ [2, 25.0, 58.5], [4, 24.0, 60.0], [8, 22.0, 62.0], [12, 20.0, 64.0],
+ ],
+ // ROKS Choi Young DDH-981 - Cheonghae Unit, Hormuz area
+ '440001981': [
+ [-12, 25.5, 57.0], [-8, 26.0, 56.5], [-4, 25.5, 57.0], [0, 26.0, 56.5],
+ [4, 25.5, 57.0], [8, 26.0, 56.5], [12, 25.5, 57.0],
+ ],
+};
+
+// ── Ship Propagation ─────────────────────────────────
+
+export function propagateShips(
+ baseShips: Ship[],
+ currentTime: number,
+ isLive = false,
+): Ship[] {
+ const hoursFromT0 = (currentTime - T0) / HOUR;
+
+ const active = baseShips.filter(ship => {
+ // In live mode, skip time-window filtering — show all ships from API
+ if (isLive) return true;
+ if (ship.activeStart != null && currentTime < ship.activeStart) return false;
+ if (ship.activeEnd != null && currentTime > ship.activeEnd) return false;
+ return true;
+ });
+
+ return active.map(ship => {
+ const plan = SHIP_PLANS[ship.mmsi];
+
+ if (plan) {
+ const pos = interpWaypoints(plan, hoursFromT0);
+ const trail = generateTrail(plan, hoursFromT0, 4, 30); // 4 points, 30min intervals
+
+ return {
+ ...ship,
+ lat: pos.lat,
+ lng: pos.lng,
+ heading: pos.heading,
+ trail,
+ };
+ }
+
+ return ship;
+ });
+}
diff --git a/src/services/ships.ts b/src/services/ships.ts
new file mode 100644
index 0000000..c782598
--- /dev/null
+++ b/src/services/ships.ts
@@ -0,0 +1,761 @@
+import type { Ship, ShipCategory } from '../types';
+
+// ═══ S&P Global Maritime AIS API v1.3 ═══
+// Base URL: https://aisapi.maritime.spglobal.com
+// All methods use HTTP POST with Basic Authentication
+// Content-Type: application/json
+
+// Use Vite dev proxy to avoid CORS (proxied to https://aisapi.maritime.spglobal.com)
+const SPG_BASE = '/api/ais';
+const SPG_USERNAME = import.meta.env.VITE_SPG_USERNAME as string | undefined;
+const SPG_PASSWORD = import.meta.env.VITE_SPG_PASSWORD as string | undefined;
+
+// Middle East bounding box for GetTargetsInAreaEnhanced
+const ME_BOUNDS = {
+ minLat: 10,
+ maxLat: 42,
+ minLng: 30,
+ maxLng: 65,
+};
+
+// Korea region bounding box: Vladivostok → South China Sea
+const KR_BOUNDS = {
+ minLat: 10,
+ maxLat: 45,
+ minLng: 115,
+ maxLng: 145,
+};
+
+// How far back to look for vessel positions (seconds)
+const SINCE_SECONDS = 3600; // last 1 hour
+
+// MMSI country prefix → flag code (MID = Maritime Identification Digits)
+const MMSI_FLAG_MAP: Record = {
+ '440': 'KR', '441': 'KR', // South Korea
+ '338': 'US', '303': 'US', '366': 'US', '367': 'US', '368': 'US', '369': 'US',
+ '232': 'UK', '233': 'UK', '234': 'UK', '235': 'UK',
+ '226': 'FR', '227': 'FR', '228': 'FR',
+ '211': 'DE',
+ '422': 'IR',
+ '431': 'JP', '432': 'JP',
+ '412': 'CN', '413': 'CN', '414': 'CN',
+ '525': 'ID',
+ '533': 'MY',
+ '548': 'PH',
+ '477': 'HK',
+ '538': 'MH',
+ '636': 'LR',
+ '352': 'PA', '353': 'PA', '354': 'PA', '355': 'PA', '356': 'PA', '357': 'PA',
+};
+
+// Known naval vessel MMSI prefixes
+const NAVAL_MMSI_PREFIXES: Record = {
+ '338': { flag: 'US', category: 'warship' },
+ '369': { flag: 'US', category: 'warship' },
+ '303': { flag: 'US', category: 'warship' },
+};
+
+// Known vessel name patterns
+const MILITARY_NAME_PATTERNS: [RegExp, ShipCategory][] = [
+ [/\bCVN\b|NIMITZ|FORD|EISENHOWER|LINCOLN|REAGAN|VINSON|STENNIS|TRUMAN|WASHINGTON|BUSH/i, 'carrier'],
+ [/\bDDG\b|\bDDH\b|DESTROYER|ARLEIGH|BURKE|ZUMWALT|SEJONG|CHUNGMUGONG|GWANGGAETO/i, 'destroyer'],
+ [/\bSSN\b|\bSS\b.*SUBMARINE/i, 'submarine'],
+ [/\bCG\b|CRUISER|TICONDEROGA/i, 'warship'],
+ [/\bLHD\b|\bLHA\b|AMPHIBIOUS|WASP|AMERICA|BATAAN|DOKDO|MARADO/i, 'warship'],
+ [/\bPATROL\b|\bPC\b|\bMCM\b|\bLCS\b|\bPCC\b|\bFFG\b|\bFF\b|FRIGATE|INCHEON|DAEGU|ULSAN|COAST\s*GUARD|해경|KCG|해양경찰|SAMBONG|TAEPYEONGYANG/i, 'patrol'],
+ [/\bROKS\b|\bUSS\b|\bHMS\b|\bFS\b|\bIRGCN\b/i, 'warship'],
+];
+
+// S&P Global AIS VesselType string → our ShipCategory
+const SPG_VESSEL_TYPE_MAP: Record = {
+ 'Cargo': 'cargo',
+ 'Bulk Carrier': 'cargo',
+ 'Container Ship': 'cargo',
+ 'General Cargo': 'cargo',
+ 'Tanker': 'tanker',
+ 'Passenger': 'civilian',
+ 'Tug': 'civilian',
+ 'Fishing': 'civilian',
+ 'Pilot Boat': 'civilian',
+ 'Tender': 'civilian',
+ 'Vessel': 'civilian',
+ 'High Speed Craft': 'civilian',
+ 'Search And Rescue': 'patrol',
+ 'Law Enforcement': 'patrol',
+ 'Anti Pollution': 'civilian',
+ 'Wing In Ground-effect': 'civilian',
+ 'Medical Transport': 'civilian',
+ 'N/A': 'unknown',
+};
+
+export function classifyShip(name: string, mmsi: string, vesselType?: string): ShipCategory {
+ // 1. Check name patterns first (most reliable for military)
+ for (const [pattern, cat] of MILITARY_NAME_PATTERNS) {
+ if (pattern.test(name)) {
+ if (cat === 'carrier') console.log(`[CARRIER by name] "${name}" mmsi=${mmsi} vesselType=${vesselType}`);
+ return cat;
+ }
+ }
+ // 2. Check S&P AIS VesselType
+ if (vesselType) {
+ const mapped = SPG_VESSEL_TYPE_MAP[vesselType];
+ if (mapped) return mapped;
+ // Partial match fallback
+ const lower = vesselType.toLowerCase();
+ if (lower.includes('tanker')) return 'tanker';
+ if (lower.includes('cargo') || lower.includes('container') || lower.includes('bulk') || lower.includes('carrier')) return 'cargo';
+ if (lower.includes('passenger') || lower.includes('cruise') || lower.includes('ferry')) return 'civilian';
+ if (lower.includes('naval') || lower.includes('military')) return 'warship';
+ if (lower.includes('patrol') || lower.includes('law enforcement')) return 'patrol';
+ }
+ // 3. Check MMSI prefix
+ const prefix = mmsi.slice(0, 3);
+ if (NAVAL_MMSI_PREFIXES[prefix]) return NAVAL_MMSI_PREFIXES[prefix].category;
+ return 'civilian';
+}
+
+function getFlagFromMMSI(mmsi: string): string | undefined {
+ const mid = mmsi.slice(0, 3);
+ return MMSI_FLAG_MAP[mid];
+}
+
+// ═══ S&P Global AIS API Response Types (Appendix D) ═══
+
+interface SPGAISTarget {
+ // Core AIS fields
+ MMSI: string;
+ IMO: string;
+ Name: string;
+ Callsign: string;
+ VesselType: string; // "Cargo", "Tanker", "Passenger", etc.
+ Lat: number;
+ Lon: number;
+ Heading: number; // True Heading or CoG if True Heading absent
+ CoG: number; // Course Over Ground (max 359.9)
+ SoG: number; // Speed Over Ground (knots)
+ Status: string; // "Under way using engine", "Anchored", "Moored", etc.
+ Destination: string;
+ DestinationTidied: string;
+ ETA: string; // ISO format
+ Draught: number;
+ Length: number;
+ Width: number;
+
+ // Timestamps
+ TimestampUTC: number; // UTC Seconds only
+ MessageTimestamp: string; // ISO format
+ ReceivedDate: string; // ISO format
+ AgeMinutes: number;
+
+ // Enhanced data
+ DWT: number; // Deadweight tonnage (-999 = unknown)
+ STAT5CODE: string; // 7-char ship type code
+ Source: string; // "ORB", "EXA", or "AIS"
+ ExtraInfo: string; // "Military Operations", "Fishing", etc.
+ ImoVerified: string;
+ TonnesCargo: number;
+
+ // Position data
+ PositionAccuracy: number;
+ PositionFixType: number;
+ RoT: number; // Rate of Turn
+ BandFlag: number;
+ RAIMFlag: number;
+
+ // Port/Zone
+ ZoneId: number;
+ LPCCode: string; // Last Port of Call Code
+ DestinationPortID: number;
+ DestinationUNLOCODE: string;
+
+ // Additional
+ DTE: string;
+ AISVersion: number;
+ RadioStatus: number;
+ RepeatIndicator: number;
+ Anomalous: boolean;
+ OnBerth: boolean;
+ InSTS: number;
+ StationId: number;
+ LengthBow: number;
+ LengthStern: number;
+ WidthPort: number;
+ WidthStarboard: number;
+ LastStaticUpdateReceived: string;
+ MessageType: string;
+ Regional: number;
+ Regional2: number;
+ Spare: number;
+ Spare2: number;
+}
+
+interface SPGAPIResponse {
+ APSStatus: {
+ CompletedOK: boolean;
+ ErrorLevel: string;
+ ErrorMessage: string;
+ Guid: string;
+ JobRunDate: string;
+ RemedialAction: string;
+ SystemDate: string;
+ SystemVersion: string;
+ };
+ // API returns different field names depending on endpoint
+ targetEnhancedArr?: SPGAISTarget[]; // GetTargetsInAreaEnhanced, GetTargetsByMMSIsEnhanced
+ targets?: SPGAISTarget[]; // fallback
+}
+
+// Build Basic Auth header
+function buildAuthHeader(): string {
+ const credentials = `${SPG_USERNAME}:${SPG_PASSWORD}`;
+ return `Basic ${btoa(credentials)}`;
+}
+
+// Generic S&P Global AIS API POST call
+async function callSPGAPI(endpoint: string, body: Record): Promise {
+ const url = `${SPG_BASE}/AISSvc.svc/AIS/${endpoint}`;
+
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Authorization': buildAuthHeader(),
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ throw new Error(`S&P AIS API ${res.status}: ${res.statusText}`);
+ }
+
+ const data: SPGAPIResponse = await res.json();
+
+ if (!data.APSStatus?.CompletedOK) {
+ throw new Error(`S&P AIS API error: ${data.APSStatus?.ErrorMessage || 'Unknown error'}`);
+ }
+
+ // API returns targetEnhancedArr for Enhanced endpoints
+ return data.targetEnhancedArr || data.targets || [];
+}
+
+// Parse S&P AIS target into our Ship type
+function parseAISTarget(t: SPGAISTarget): Ship {
+ const name = (t.Name || '').trim();
+ const mmsi = String(t.MMSI || '');
+ const flag = getFlagFromMMSI(mmsi);
+ const category = classifyShip(name, mmsi, t.VesselType);
+
+ return {
+ mmsi,
+ name: name || `MMSI-${mmsi}`,
+ lat: t.Lat,
+ lng: t.Lon,
+ heading: t.Heading ?? t.CoG ?? 0,
+ speed: t.SoG ?? 0,
+ course: t.CoG ?? t.Heading ?? 0,
+ category,
+ flag,
+ typecode: t.STAT5CODE || t.VesselType || undefined,
+ typeDesc: t.VesselType || undefined,
+ imo: t.IMO || undefined,
+ callSign: t.Callsign || undefined,
+ status: t.Status || undefined,
+ destination: (t.DestinationTidied || t.Destination || '').trim() || undefined,
+ eta: t.ETA || undefined,
+ draught: t.Draught > 0 ? t.Draught : undefined,
+ length: t.Length > 0 ? t.Length : undefined,
+ width: t.Width > 0 ? t.Width : undefined,
+ lastSeen: t.MessageTimestamp
+ ? new Date(t.MessageTimestamp).getTime()
+ : t.TimestampUTC
+ ? t.TimestampUTC * 1000
+ : Date.now(),
+ };
+}
+
+// ═══ Primary API: GetTargetsInAreaEnhanced ═══
+// Returns vessels within a bounding box updated in the last N seconds
+export async function fetchShipsFromSPG(): Promise {
+ if (!SPG_USERNAME || !SPG_PASSWORD) {
+ console.warn('VITE_SPG_USERNAME / VITE_SPG_PASSWORD not set, using sample data');
+ return [];
+ }
+
+ try {
+ const targets = await callSPGAPI('GetTargetsInAreaEnhanced', {
+ sinceSeconds: SINCE_SECONDS,
+ minLat: ME_BOUNDS.minLat,
+ maxLat: ME_BOUNDS.maxLat,
+ minLong: ME_BOUNDS.minLng,
+ maxLong: ME_BOUNDS.maxLng,
+ });
+
+ return targets
+ .filter(t => t.Lat != null && t.Lon != null && t.Lat !== 0 && t.Lon !== 0)
+ .map(parseAISTarget);
+ } catch (err) {
+ console.warn('S&P AIS API (GetTargetsInAreaEnhanced) failed:', err);
+ return [];
+ }
+}
+
+// ═══ Supplementary: fetch specific vessels by MMSI ═══
+// Useful for tracking known military vessels that may not appear in area query
+export async function fetchShipsByMMSI(mmsiList: string[]): Promise {
+ if (!SPG_USERNAME || !SPG_PASSWORD || mmsiList.length === 0) return [];
+
+ try {
+ const targets = await callSPGAPI('GetTargetsByMMSIsEnhanced', {
+ MMSI: mmsiList.join(','),
+ });
+
+ return targets
+ .filter(t => t.Lat != null && t.Lon != null)
+ .map(parseAISTarget);
+ } catch (err) {
+ console.warn('S&P AIS API (GetTargetsByMMSIsEnhanced) failed:', err);
+ return [];
+ }
+}
+
+// ═══ Supplementary: fetch tankers only ═══
+export async function fetchTankers(): Promise {
+ if (!SPG_USERNAME || !SPG_PASSWORD) return [];
+
+ try {
+ const targets = await callSPGAPI('GetTankerSubsetOneEnhanced', {});
+ return targets
+ .filter(t => t.Lat != null && t.Lon != null)
+ .filter(t => {
+ // Filter to Middle East region
+ return t.Lat >= ME_BOUNDS.minLat && t.Lat <= ME_BOUNDS.maxLat
+ && t.Lon >= ME_BOUNDS.minLng && t.Lon <= ME_BOUNDS.maxLng;
+ })
+ .map(parseAISTarget);
+ } catch (err) {
+ console.warn('S&P AIS API (GetTankerSubsetOneEnhanced) failed:', err);
+ return [];
+ }
+}
+
+// ═══ Main fetch function ═══
+// Tries S&P Global AIS API first, merges with sample military ships
+export async function fetchShips(): Promise {
+ // Always include sample military/scenario ships (warships have AIS off)
+ const sampleShips = getSampleShips();
+
+ // Try area-based query for all commercial vessels in Middle East
+ const areaShips = await fetchShipsFromSPG();
+
+ if (areaShips.length > 0) {
+ console.log(`S&P AIS API: ${areaShips.length} real vessels in Middle East area`);
+
+ // Keep sample military ships that aren't in AIS data
+ const sampleMilitary = sampleShips.filter(s =>
+ s.category !== 'civilian' && s.category !== 'cargo' && s.category !== 'tanker' && s.category !== 'unknown'
+ );
+ // Keep sample Korean commercial ships (scenario-specific stranded vessels)
+ const sampleKorean = sampleShips.filter(s => s.flag === 'KR' && s.category !== 'destroyer');
+
+ const sampleMMSIs = new Set([...sampleMilitary, ...sampleKorean].map(s => s.mmsi));
+ const sampleToKeep = [...sampleMilitary, ...sampleKorean];
+
+ // Merge: real AIS ships + sample military/Korean ships (avoid duplicates)
+ const merged = [
+ ...areaShips.filter(s => !sampleMMSIs.has(s.mmsi)),
+ ...sampleToKeep,
+ ];
+ return merged;
+ }
+
+ // Fallback to sample data only
+ console.warn('S&P AIS API returned no data, using sample data');
+ return sampleShips;
+}
+
+
+// T0 = main strike moment
+const T0 = new Date('2026-03-01T12:01:00Z').getTime();
+const HOUR = 3600_000;
+
+function getSampleShips(): Ship[] {
+ const now = Date.now();
+ return [
+ // ═══ USS ABRAHAM LINCOLN CSG — Arabian Sea (deployed Nov 2025, rerouted to ME) ═══
+ {
+ mmsi: '369970072', name: 'USS ABRAHAM LINCOLN (CVN-72)', lat: 23.5, lng: 59.8,
+ heading: 315, speed: 20, course: 315, category: 'carrier', flag: 'US', typecode: 'CVN',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970121', name: 'USS FRANK E. PETERSEN JR. (DDG-121)', lat: 23.8, lng: 59.5,
+ heading: 300, speed: 22, course: 300, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970111', name: 'USS SPRUANCE (DDG-111)', lat: 23.2, lng: 60.1,
+ heading: 340, speed: 20, course: 340, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970112', name: 'USS MICHAEL MURPHY (DDG-112)', lat: 23.0, lng: 60.4,
+ heading: 280, speed: 18, course: 280, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ USS GERALD R. FORD CSG — Red Sea (deployed Feb 2026) ═══
+ {
+ mmsi: '369970078', name: 'USS GERALD R. FORD (CVN-78)', lat: 20.5, lng: 38.5,
+ heading: 170, speed: 18, course: 170, category: 'carrier', flag: 'US', typecode: 'CVN',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ Independent US destroyers — Arabian Sea ═══
+ {
+ mmsi: '369970074', name: 'USS McFAUL (DDG-74)', lat: 24.0, lng: 58.0,
+ heading: 45, speed: 22, course: 45, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970113', name: 'USS JOHN FINN (DDG-113)', lat: 22.0, lng: 61.5,
+ heading: 350, speed: 24, course: 350, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970069', name: 'USS MILIUS (DDG-69)', lat: 25.5, lng: 57.5,
+ heading: 270, speed: 20, course: 270, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970119', name: 'USS DELBERT D. BLACK (DDG-119)', lat: 24.5, lng: 56.8,
+ heading: 90, speed: 18, course: 90, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970091', name: 'USS PINCKNEY (DDG-91)', lat: 21.5, lng: 60.8,
+ heading: 30, speed: 22, course: 30, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970057', name: 'USS MITSCHER (DDG-57)', lat: 26.0, lng: 55.5,
+ heading: 180, speed: 20, course: 180, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ US LCS — Persian Gulf ═══
+ {
+ mmsi: '369970030', name: 'USS CANBERRA (LCS-30)', lat: 27.2, lng: 51.5,
+ heading: 60, speed: 15, course: 60, category: 'patrol', flag: 'US', typecode: 'LCS',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '369970016', name: 'USS TULSA (LCS-16)', lat: 26.2, lng: 53.0,
+ heading: 120, speed: 14, course: 120, category: 'patrol', flag: 'US', typecode: 'LCS',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ UK Royal Navy ═══
+ {
+ mmsi: '232001034', name: 'HMS DIAMOND (D34)', lat: 25.0, lng: 57.0,
+ heading: 0, speed: 16, course: 0, category: 'destroyer', flag: 'UK', typecode: 'DDG',
+ lastSeen: now, activeStart: T0 - 8 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '232001041', name: 'HMS MIDDLETON (M34)', lat: 26.0, lng: 56.8,
+ heading: 270, speed: 8, course: 270, category: 'patrol', flag: 'UK', typecode: 'MCM',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 6 * HOUR,
+ },
+
+ // ═══ French Navy ═══
+ {
+ mmsi: '227001653', name: 'FS LANGUEDOC (D653)', lat: 34.8, lng: 33.5,
+ heading: 120, speed: 16, course: 120, category: 'destroyer', flag: 'FR', typecode: 'FREMM',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '227001091', name: 'FS CHARLES DE GAULLE (R91)', lat: 35.0, lng: 28.0,
+ heading: 90, speed: 22, course: 90, category: 'carrier', flag: 'FR', typecode: 'CVN',
+ lastSeen: now, activeStart: T0 + 4 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ IRGCN — Iranian patrol boats ═══
+ {
+ mmsi: '422001001', name: 'IRGCN FAST ATTACK-1', lat: 26.55, lng: 56.25,
+ heading: 180, speed: 35, course: 180, category: 'patrol', flag: 'IR', typecode: 'PC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ {
+ mmsi: '422001002', name: 'IRGCN FAST ATTACK-2', lat: 26.65, lng: 56.10,
+ heading: 210, speed: 32, course: 210, category: 'patrol', flag: 'IR', typecode: 'PC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 4 * HOUR,
+ },
+ {
+ mmsi: '422001003', name: 'IRGCN FAST ATTACK-3', lat: 26.70, lng: 56.40,
+ heading: 240, speed: 30, course: 240, category: 'patrol', flag: 'IR', typecode: 'PC',
+ lastSeen: now, activeStart: T0 - 10 * HOUR, activeEnd: T0 + 2 * HOUR,
+ },
+ {
+ mmsi: '422010001', name: 'IRIS DENA (F75)', lat: 25.8, lng: 57.2,
+ heading: 210, speed: 14, course: 210, category: 'warship', flag: 'IR', typecode: 'FFG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ Republic of Korea Navy ═══
+ {
+ mmsi: '440001981', name: 'ROKS CHOI YOUNG (DDH-981)', lat: 25.3, lng: 57.5,
+ heading: 45, speed: 16, course: 45, category: 'destroyer', flag: 'KR', typecode: 'DDH',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ KOREAN VESSELS — ships stranded at Fujairah anchorage (Gulf of Oman) ═══
+ // Fujairah Offshore Anchorage: ~25.1-25.35°N, 56.30-56.55°E (open water)
+ {
+ mmsi: '440101001', name: 'HYUNDAI SUPREME', lat: 25.18, lng: 56.38,
+ heading: 130, speed: 0, course: 130, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440101002', name: 'HYUNDAI PRESTIGE', lat: 25.22, lng: 56.42,
+ heading: 110, speed: 0, course: 110, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440102001', name: 'GS VOYAGER', lat: 25.15, lng: 56.35,
+ heading: 90, speed: 0, course: 90, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440103001', name: 'SINOKOR ENERGY', lat: 25.25, lng: 56.45,
+ heading: 150, speed: 0, course: 150, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440103002', name: 'SINOKOR PIONEER', lat: 25.20, lng: 56.50,
+ heading: 200, speed: 0, course: 200, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440104001', name: 'SK HARMONY', lat: 25.28, lng: 56.40,
+ heading: 170, speed: 0, course: 170, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440105001', name: 'S-OIL CROWN', lat: 25.12, lng: 56.33,
+ heading: 0, speed: 0, course: 0, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440501234', name: 'HYUNDAI BRAVE', lat: 25.30, lng: 56.48,
+ heading: 130, speed: 0, course: 130, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440501235', name: 'HMM ROTTERDAM', lat: 25.16, lng: 56.52,
+ heading: 100, speed: 0, course: 100, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440501236', name: 'PAN OCEAN STAR', lat: 25.35, lng: 56.44,
+ heading: 80, speed: 0, course: 80, category: 'cargo', flag: 'KR', typecode: 'BULK',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440501237', name: 'POLARIS VICTORY', lat: 25.23, lng: 56.36,
+ heading: 160, speed: 0, course: 160, category: 'cargo', flag: 'KR', typecode: 'BULK',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440501238', name: 'KSS BUSAN', lat: 25.32, lng: 56.55,
+ heading: 120, speed: 0, course: 120, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440106001', name: 'SK SPLENDOR', lat: 25.19, lng: 56.46,
+ heading: 140, speed: 0, course: 140, category: 'tanker', flag: 'KR', typecode: 'LNG',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '440107001', name: 'EAGLE VELLORE', lat: 24.0, lng: 59.0,
+ heading: 120, speed: 14, course: 120, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 6 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+
+ // ═══ Other Commercial vessels ═══
+ {
+ mmsi: '538006789', name: 'PACIFIC HARMONY', lat: 25.27, lng: 56.58,
+ heading: 300, speed: 0, course: 300, category: 'tanker', flag: 'MH', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '228001234', name: 'CMA CGM TROCADERO', lat: 26.3, lng: 53.5,
+ heading: 90, speed: 0, course: 90, category: 'cargo', flag: 'FR', typecode: 'CONT',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ {
+ mmsi: '636012345', name: 'FRONT ALTAIR', lat: 25.5, lng: 57.2,
+ heading: 0, speed: 0, course: 0, category: 'tanker', flag: 'LR', typecode: 'VLCC',
+ lastSeen: now, activeStart: T0 - 12 * HOUR, activeEnd: T0 + 12 * HOUR,
+ },
+ ];
+}
+
+// ═══════════════════════════════════════
+// KOREA REGION — separate data pipeline
+// ═══════════════════════════════════════
+
+// S&P AIS API for Korea region (Vladivostok → South China Sea)
+async function fetchShipsFromSPGKorea(): Promise {
+ if (!SPG_USERNAME || !SPG_PASSWORD) return [];
+ try {
+ const targets = await callSPGAPI('GetTargetsInAreaEnhanced', {
+ sinceSeconds: SINCE_SECONDS,
+ minLat: KR_BOUNDS.minLat,
+ maxLat: KR_BOUNDS.maxLat,
+ minLong: KR_BOUNDS.minLng,
+ maxLong: KR_BOUNDS.maxLng,
+ });
+ return targets
+ .filter(t => t.Lat != null && t.Lon != null && t.Lat !== 0 && t.Lon !== 0)
+ .map(parseAISTarget);
+ } catch (err) {
+ console.warn('S&P AIS API (Korea region) failed:', err);
+ return [];
+ }
+}
+
+export async function fetchShipsKorea(): Promise {
+ const sample = getSampleShipsKorea();
+ const real = await fetchShipsFromSPGKorea();
+ if (real.length > 0) {
+ console.log(`S&P AIS API: ${real.length} vessels in Korea region`);
+ const sampleMMSIs = new Set(sample.map(s => s.mmsi));
+ return [...real.filter(s => !sampleMMSIs.has(s.mmsi)), ...sample];
+ }
+ return sample;
+}
+
+function getSampleShipsKorea(): Ship[] {
+ const now = Date.now();
+ return [
+ // ═══ ROKN — Korean Navy (본토 방어 강화) ═══
+ { mmsi: '440991001', name: 'ROKS SEJONG THE GREAT (DDG-991)', lat: 35.05, lng: 129.08,
+ heading: 180, speed: 0, course: 180, category: 'destroyer', flag: 'KR', typecode: 'DDG',
+ lastSeen: now },
+ { mmsi: '440991002', name: 'ROKS YULGOK YI I (DDG-992)', lat: 37.50, lng: 126.55,
+ heading: 270, speed: 12, course: 270, category: 'destroyer', flag: 'KR', typecode: 'DDG',
+ lastSeen: now },
+ { mmsi: '440991003', name: 'ROKS DOKDO (LPH-6111)', lat: 33.95, lng: 128.60,
+ heading: 90, speed: 14, course: 90, category: 'warship', flag: 'KR', typecode: 'LPH',
+ lastSeen: now },
+ { mmsi: '440991004', name: 'ROKS MARADO (LPH-6112)', lat: 34.80, lng: 128.50,
+ heading: 150, speed: 16, course: 150, category: 'warship', flag: 'KR', typecode: 'LPH',
+ lastSeen: now },
+ { mmsi: '440991005', name: 'ROKS DAEGU (FFG-818)', lat: 35.95, lng: 129.60,
+ heading: 45, speed: 10, course: 45, category: 'patrol', flag: 'KR', typecode: 'FFG',
+ lastSeen: now },
+ { mmsi: '440991006', name: 'ROKS INCHEON (FFG-811)', lat: 37.45, lng: 126.40,
+ heading: 310, speed: 8, course: 310, category: 'patrol', flag: 'KR', typecode: 'FFG',
+ lastSeen: now },
+ { mmsi: '440991007', name: 'ROKS GANG GAMCHAN (DDH-979)', lat: 33.52, lng: 126.53,
+ heading: 200, speed: 14, course: 200, category: 'destroyer', flag: 'KR', typecode: 'DDH',
+ lastSeen: now },
+
+ // ═══ 부산항 — 컨테이너/화물선 ═══
+ { mmsi: '440201001', name: 'HMM ALGECIRAS', lat: 35.08, lng: 129.06,
+ heading: 0, speed: 0, course: 0, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+ { mmsi: '440201002', name: 'HMM OSLO', lat: 35.06, lng: 129.08,
+ heading: 45, speed: 0, course: 45, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+ { mmsi: '440201003', name: 'HMM GDANSK', lat: 35.04, lng: 129.10,
+ heading: 90, speed: 0, course: 90, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+ { mmsi: '441201001', name: 'SINOKOR INCHEON', lat: 35.10, lng: 129.04,
+ heading: 180, speed: 0, course: 180, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+
+ // ═══ 울산항 — 유조선 (원유 수입 대기) ═══
+ { mmsi: '440301001', name: 'SK ENERGY NO.1', lat: 35.50, lng: 129.42,
+ heading: 0, speed: 0, course: 0, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now },
+ { mmsi: '440301002', name: 'GS CALTEX ULSAN', lat: 35.48, lng: 129.45,
+ heading: 30, speed: 0, course: 30, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now },
+ { mmsi: '440301003', name: 'S-OIL PIONEER', lat: 35.52, lng: 129.40,
+ heading: 270, speed: 0, course: 270, category: 'tanker', flag: 'KR', typecode: 'VLCC',
+ lastSeen: now },
+
+ // ═══ 여수/광양 — LNG/석유화학 ═══
+ { mmsi: '440401001', name: 'KOGAS LNG YEOSU', lat: 34.73, lng: 127.74,
+ heading: 180, speed: 0, course: 180, category: 'tanker', flag: 'KR', typecode: 'LNG',
+ lastSeen: now },
+ { mmsi: '440401002', name: 'SK GAS CARRIER', lat: 34.75, lng: 127.70,
+ heading: 0, speed: 0, course: 0, category: 'tanker', flag: 'KR', typecode: 'LPG',
+ lastSeen: now },
+
+ // ═══ 인천항 ═══
+ { mmsi: '440501001', name: 'PAN OCEAN INCHEON', lat: 37.44, lng: 126.58,
+ heading: 90, speed: 0, course: 90, category: 'cargo', flag: 'KR', typecode: 'BULK',
+ lastSeen: now },
+ { mmsi: '440501002', name: 'POLARIS STAR', lat: 37.46, lng: 126.60,
+ heading: 0, speed: 0, course: 0, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+
+ // ═══ 동해 — 일본/러시아 항로 화물선 ═══
+ { mmsi: '440601001', name: 'HYUNDAI MIPO', lat: 37.10, lng: 130.50,
+ heading: 30, speed: 12, course: 30, category: 'cargo', flag: 'KR', typecode: 'BULK',
+ lastSeen: now },
+ { mmsi: '431201001', name: 'NIPPON MARU', lat: 38.20, lng: 131.50,
+ heading: 200, speed: 14, course: 200, category: 'cargo', flag: 'JP', typecode: 'BULK',
+ lastSeen: now },
+
+ // ═══ 대한해협 통과 선박 ═══
+ { mmsi: '412801001', name: 'COSCO BUSAN', lat: 34.30, lng: 129.20,
+ heading: 60, speed: 16, course: 60, category: 'cargo', flag: 'CN', typecode: 'CONT',
+ lastSeen: now },
+ { mmsi: '538201001', name: 'PACIFIC VENUS', lat: 34.50, lng: 128.80,
+ heading: 240, speed: 18, course: 240, category: 'tanker', flag: 'MH', typecode: 'VLCC',
+ lastSeen: now },
+ { mmsi: '636201001', name: 'NORDIC STAR', lat: 34.10, lng: 128.50,
+ heading: 70, speed: 14, course: 70, category: 'tanker', flag: 'LR', typecode: 'VLCC',
+ lastSeen: now },
+
+ // ═══ 남중국해 → 부산 항로 ═══
+ { mmsi: '440701001', name: 'HMM SINGAPORE', lat: 25.00, lng: 122.00,
+ heading: 30, speed: 18, course: 30, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+ { mmsi: '440701002', name: 'HYUNDAI BANGKOK', lat: 20.50, lng: 118.00,
+ heading: 45, speed: 16, course: 45, category: 'cargo', flag: 'KR', typecode: 'CONT',
+ lastSeen: now },
+ { mmsi: '477201001', name: 'OOCL HONG KONG', lat: 22.30, lng: 120.50,
+ heading: 20, speed: 20, course: 20, category: 'cargo', flag: 'HK', typecode: 'CONT',
+ lastSeen: now },
+
+ // ═══ 블라디보스톡 근해 ═══
+ { mmsi: '440801001', name: 'KORYO TRADER', lat: 42.80, lng: 132.50,
+ heading: 180, speed: 10, course: 180, category: 'cargo', flag: 'KR', typecode: 'BULK',
+ lastSeen: now },
+
+ // ═══ 제주 해역 ═══
+ { mmsi: '440901001', name: 'JEJU WORLD', lat: 33.30, lng: 126.30,
+ heading: 90, speed: 16, course: 90, category: 'civilian', flag: 'KR', typecode: 'PASS',
+ lastSeen: now },
+
+ // ═══ USN 7th Fleet (주일미군 / 한반도 근해) ═══
+ { mmsi: '369971001', name: 'USS RONALD REAGAN (CVN-76)', lat: 35.30, lng: 139.60,
+ heading: 180, speed: 0, course: 180, category: 'carrier', flag: 'US', typecode: 'CVN',
+ lastSeen: now },
+ { mmsi: '369971002', name: 'USS BARRY (DDG-52)', lat: 34.00, lng: 130.50,
+ heading: 270, speed: 22, course: 270, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now },
+ { mmsi: '369971003', name: 'USS BENFOLD (DDG-65)', lat: 35.50, lng: 130.00,
+ heading: 180, speed: 18, course: 180, category: 'destroyer', flag: 'US', typecode: 'DDG',
+ lastSeen: now },
+
+ // ═══ JMSDF (해상자위대) ═══
+ { mmsi: '431991001', name: 'JS IZUMO (DDH-183)', lat: 34.60, lng: 137.20,
+ heading: 90, speed: 14, course: 90, category: 'warship', flag: 'JP', typecode: 'DDH',
+ lastSeen: now },
+ ];
+}
diff --git a/src/services/submarineCable.ts b/src/services/submarineCable.ts
new file mode 100644
index 0000000..e970c1c
--- /dev/null
+++ b/src/services/submarineCable.ts
@@ -0,0 +1,419 @@
+// ═══ Korean Submarine Cable Data ═══
+// Source: TeleGeography / submarinecablemap.com
+// Major submarine cables landing in South Korea
+
+export interface SubmarineCable {
+ id: string;
+ name: string;
+ color: string;
+ landingPoints: string[]; // city names
+ rfsYear?: number; // ready for service year
+ length?: string; // e.g. "36,500 km"
+ owners?: string;
+ route: [number, number][]; // [lng, lat] pairs for the cable path
+}
+
+// ── Route reference ──
+// Korea Strait: 129.5°E between Korea & Tsushima (water)
+// Tsushima-Kyushu channel: ~129.8-130.2°E, 33.5-34°N (water)
+// West of Kyushu (Goto-Nagasaki gap): ~129.3°E, 31-33°N (water)
+// South of Kyushu (Cape Sata): below 30.8°N when 130-131°E
+// South of Shikoku: below 32.5°N when 132-135°E
+// South of Kii/Izu: below 33°N when 135-139°E
+// Korea south coast: below 33.8°N between 126-129°E = open sea
+
+export const KOREA_SUBMARINE_CABLES: SubmarineCable[] = [
+ // ═══ Southbound cables (Busan → Korea Strait → East China Sea → south) ═══
+ {
+ id: 'apcn-2',
+ name: 'APCN-2',
+ color: '#e91e63',
+ landingPoints: ['부산', '일본', '중국', '대만', '필리핀', '싱가포르', '말레이시아'],
+ rfsYear: 2001,
+ length: '19,000 km',
+ owners: 'KT, NTT, China Telecom 등',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.5, 34.5], // Korea Strait (water between Korea & Tsushima)
+ [129.8, 33.8], // Tsushima-Kyushu channel (water)
+ [129.3, 32.5], // West of Kyushu (Goto-Nagasaki gap, water)
+ [129.0, 31.0], // West of Kyushu south (water)
+ [128.5, 29.5], // South of Kyushu (open sea)
+ [127.0, 27.0], // East China Sea
+ [125.0, 25.0], // East China Sea
+ [123.5, 23.0], // East of Taiwan (sea)
+ [121.5, 21.0], // South of Taiwan (sea)
+ [119.5, 18.5], // Luzon Strait
+ [117.5, 15.0], // South China Sea
+ [115.0, 10.5], // South China Sea
+ [111.5, 6.0], // South China Sea
+ [107.0, 3.0], // Approach Singapore
+ [104.0, 1.3], // Singapore coast
+ ],
+ },
+ {
+ id: 'apg',
+ name: 'APG (Asia Pacific Gateway)',
+ color: '#2196f3',
+ landingPoints: ['부산', '일본', '중국', '대만', '홍콩', '베트남', '태국', '말레이시아', '싱가포르'],
+ rfsYear: 2016,
+ length: '10,400 km',
+ owners: 'KT, NTT, China Telecom, VNPT 등',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.4, 34.4], // Korea Strait
+ [129.7, 33.7], // Tsushima-Kyushu channel
+ [129.2, 32.3], // West of Kyushu (water)
+ [128.8, 30.8], // South of Kyushu (water)
+ [127.5, 28.5], // East China Sea
+ [125.5, 26.0], // East China Sea
+ [123.5, 24.0], // East of Taiwan (sea)
+ [121.8, 21.5], // South Taiwan (sea)
+ [119.0, 19.5], // Luzon Strait
+ [116.5, 18.5], // South China Sea
+ [114.5, 22.0], // Hong Kong approach (sea)
+ [113.0, 18.0], // South China Sea
+ [110.0, 14.0], // Off Vietnam (sea)
+ [108.5, 11.0], // Off South Vietnam (sea)
+ [106.5, 7.0], // South China Sea
+ [104.5, 2.5], // Approach Singapore
+ [104.0, 1.3], // Singapore coast
+ ],
+ },
+ {
+ id: 'eac-c2c',
+ name: 'EAC-C2C',
+ color: '#ff9800',
+ landingPoints: ['부산', '일본', '중국', '필리핀', '싱가포르'],
+ rfsYear: 2002,
+ length: '36,500 km',
+ owners: 'KT 등',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.6, 34.6], // Korea Strait
+ [130.0, 33.9], // Tsushima-Kyushu channel
+ [129.5, 32.8], // West Kyushu coast (water)
+ [129.0, 31.2], // West of Kyushu south (water)
+ [128.0, 29.0], // East China Sea
+ [126.0, 26.5], // East China Sea
+ [124.0, 24.0], // East of Taiwan (sea)
+ [122.5, 21.0], // South Taiwan (sea)
+ [121.0, 18.0], // Luzon Strait
+ [120.0, 14.5], // West of Luzon (sea)
+ [117.5, 10.5], // South China Sea
+ [113.0, 6.0], // South China Sea
+ [108.0, 3.5], // South China Sea
+ [104.0, 1.3], // Singapore coast
+ ],
+ },
+ {
+ id: 'flag-north-asia',
+ name: 'FLAG/REACH North Asia Loop',
+ color: '#9c27b0',
+ landingPoints: ['부산', '일본', '홍콩', '중국'],
+ rfsYear: 2002,
+ length: '11,500 km',
+ owners: 'Reliance Globalcom',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.5, 34.5], // Korea Strait
+ [129.9, 33.8], // Tsushima-Kyushu channel
+ [129.4, 32.5], // West Kyushu (water)
+ [129.0, 31.0], // South of Kyushu (water)
+ [127.0, 28.0], // East China Sea
+ [124.5, 25.5], // East China Sea
+ [122.5, 23.0], // East of Taiwan (sea)
+ [121.0, 21.0], // South Taiwan (sea)
+ [118.0, 19.5], // South China Sea
+ [115.5, 21.0], // Approach Hong Kong
+ [114.2, 22.0], // Hong Kong coast
+ ],
+ },
+ {
+ id: 'sjc',
+ name: 'SJC (SE Asia-Japan Cable)',
+ color: '#8bc34a',
+ landingPoints: ['부산', '일본', '중국', '홍콩', '필리핀', '싱가포르'],
+ rfsYear: 2013,
+ length: '8,900 km',
+ owners: 'KT, Google, China Mobile 등',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.4, 34.4], // Korea Strait
+ [129.7, 33.6], // Tsushima-Kyushu channel
+ [129.2, 32.0], // West Kyushu (water)
+ [128.5, 30.5], // South of Kyushu (water)
+ [126.5, 27.5], // East China Sea
+ [124.0, 25.0], // East China Sea
+ [122.5, 22.5], // East of Taiwan (sea)
+ [121.0, 20.5], // South Taiwan (sea)
+ [118.0, 19.0], // South China Sea
+ [115.0, 21.5], // Approach Hong Kong
+ [114.2, 22.0], // Hong Kong coast
+ [113.5, 18.5], // South China Sea
+ [112.0, 13.0], // South China Sea
+ [110.0, 7.5], // South China Sea
+ [106.5, 3.5], // Approach Singapore
+ [104.0, 1.3], // Singapore coast
+ ],
+ },
+ {
+ id: 'sjc2',
+ name: 'SJC2',
+ color: '#03a9f4',
+ landingPoints: ['부산', '일본', '중국', '대만', '싱가포르', '태국'],
+ rfsYear: 2022,
+ length: '10,500 km',
+ owners: 'KT, China Mobile, Facebook, KDDI 등',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.5, 34.5], // Korea Strait
+ [129.8, 33.7], // Tsushima-Kyushu channel
+ [129.3, 32.3], // West Kyushu (water)
+ [128.8, 30.8], // South of Kyushu (water)
+ [127.0, 28.0], // East China Sea
+ [125.0, 25.5], // East China Sea
+ [123.0, 23.5], // East of Taiwan (sea)
+ [121.5, 21.5], // South Taiwan (sea)
+ [119.0, 19.5], // South China Sea
+ [116.0, 21.5], // Approach Hong Kong
+ [114.5, 22.0], // Hong Kong area
+ [113.0, 17.5], // South China Sea
+ [110.0, 12.5], // South China Sea
+ [107.5, 8.0], // South China Sea
+ [105.0, 3.5], // Approach Singapore
+ [104.0, 1.3], // Singapore coast
+ ],
+ },
+
+ // ═══ Japan direct cable ═══
+ {
+ id: 'kjcn',
+ name: 'KJCN (Korea-Japan Cable)',
+ color: '#4caf50',
+ landingPoints: ['부산', '기타큐슈(일본)'],
+ rfsYear: 2003,
+ length: '230 km',
+ owners: 'KT, KDDI',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.5, 34.8], // Korea Strait mid
+ [129.9, 34.4], // Strait (water)
+ [130.3, 34.1], // Approaching Kyushu north coast
+ [130.9, 33.9], // Kitakyushu coast
+ ],
+ },
+
+ // ═══ Pacific cables (Busan → Korea Strait → south of Japan → Pacific) ═══
+ {
+ id: 'tpe',
+ name: 'TPE (Trans-Pacific Express)',
+ color: '#00bcd4',
+ landingPoints: ['거제', '일본', '중국', '대만', '미국'],
+ rfsYear: 2008,
+ length: '17,700 km',
+ owners: 'KT, China Telecom, Verizon 등',
+ route: [
+ [128.62, 34.88], // Geoje coast
+ [129.3, 34.3], // Korea Strait
+ [129.7, 33.7], // Tsushima-Kyushu channel
+ [129.3, 32.2], // West Kyushu (water)
+ [129.0, 31.0], // SW of Kyushu (water)
+ [129.5, 30.0], // South of Cape Sata (open sea)
+ [131.5, 30.0], // SE of Kyushu (open sea)
+ [134.0, 30.5], // South of Shikoku (sea, <32.5°N)
+ [137.0, 31.5], // South of Kii (sea, <33°N)
+ [140.0, 32.0], // South of Izu (sea, <33°N)
+ [143.0, 33.0], // Off east Honshu (sea)
+ [148.0, 35.5], // North Pacific
+ [158.0, 38.0], // North Pacific
+ [170.0, 40.0], // Mid Pacific
+ [-170.0, 42.0], // Mid Pacific
+ [-155.0, 41.0], // Mid Pacific
+ [-140.0, 39.0], // East Pacific
+ [-130.0, 38.0], // Approach US
+ [-122.4, 37.8], // US West Coast
+ ],
+ },
+ {
+ id: 'ncp',
+ name: 'NCP (New Cross Pacific)',
+ color: '#ffeb3b',
+ landingPoints: ['부산', '일본', '미국'],
+ rfsYear: 2018,
+ length: '13,600 km',
+ owners: 'KT, Amazon, Microsoft 등',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [129.5, 34.5], // Korea Strait
+ [129.8, 33.8], // Tsushima-Kyushu channel
+ [129.4, 32.5], // West Kyushu (water)
+ [129.0, 31.0], // SW of Kyushu (water)
+ [129.8, 30.0], // South of Cape Sata (sea)
+ [132.0, 30.0], // SE of Kyushu (sea)
+ [135.0, 30.5], // South of Shikoku (sea)
+ [138.0, 31.0], // South of Kii (sea)
+ [141.0, 32.0], // South of Izu (sea)
+ [144.0, 33.5], // Off east Honshu (sea)
+ [150.0, 37.0], // North Pacific
+ [160.0, 40.0], // North Pacific
+ [172.0, 43.0], // Mid Pacific
+ [-170.0, 45.5], // Mid Pacific
+ [-155.0, 45.0], // Mid Pacific
+ [-140.0, 43.0], // East Pacific
+ [-130.0, 42.0], // Approach US
+ [-125.0, 40.5], // Near US
+ [-122.3, 37.4], // US West Coast
+ ],
+ },
+
+ // ═══ GOKI (Incheon → Yellow Sea → south → Okinawa → Guam) ═══
+ {
+ id: 'goki',
+ name: 'GOKI',
+ color: '#ff5722',
+ landingPoints: ['인천', '오키나와', '괌'],
+ rfsYear: 2020,
+ length: '3,400 km',
+ owners: 'KT 등',
+ route: [
+ [126.59, 37.46], // Incheon coast
+ [126.0, 36.5], // Yellow Sea (open water, west of Korea)
+ [125.5, 35.5], // Yellow Sea
+ [125.0, 34.5], // Yellow Sea south
+ [125.0, 33.5], // South of Korea (open sea, south of Jeju)
+ [125.5, 32.0], // East China Sea
+ [126.5, 30.0], // East China Sea
+ [127.5, 27.5], // Ryukyu arc
+ [127.8, 26.3], // Okinawa coast
+ [129.0, 23.0], // Philippine Sea
+ [133.0, 19.0], // Philippine Sea
+ [138.0, 16.0], // Philippine Sea
+ [144.8, 13.5], // Guam coast
+ ],
+ },
+
+ // ═══ China direct cables (Yellow Sea crossing) ═══
+ {
+ id: 'c2c',
+ name: 'C2C (China-Korea)',
+ color: '#f44336',
+ landingPoints: ['태안', '칭다오(중국)'],
+ rfsYear: 2001,
+ length: '1,100 km',
+ owners: 'KT, China Netcom',
+ route: [
+ [126.35, 36.75], // Taean coast (west coast Korea)
+ [125.5, 36.5], // Yellow Sea
+ [124.0, 36.0], // Yellow Sea mid
+ [122.5, 35.8], // Yellow Sea
+ [121.0, 36.0], // Approach Qingdao
+ [120.4, 36.1], // Qingdao coast
+ ],
+ },
+ {
+ id: 'ckc',
+ name: 'CKC (China-Korea Cable)',
+ color: '#ff7043',
+ landingPoints: ['태안', '상하이(중국)'],
+ rfsYear: 2006,
+ length: '1,300 km',
+ owners: 'KT, China Telecom',
+ route: [
+ [126.35, 36.75], // Taean coast (west coast Korea)
+ [125.5, 36.0], // Yellow Sea
+ [124.5, 35.0], // Yellow Sea mid
+ [123.5, 34.0], // Yellow Sea south
+ [123.0, 33.0], // East China Sea
+ [122.5, 32.0], // East China Sea
+ [122.0, 31.5], // Approach Shanghai
+ [121.8, 31.2], // Shanghai coast
+ ],
+ },
+ {
+ id: 'flag-fea',
+ name: 'FEA (Flag Europe Asia)',
+ color: '#ab47bc',
+ landingPoints: ['부산', '상하이(중국)', '홍콩'],
+ rfsYear: 2001,
+ length: '28,000 km',
+ owners: 'Reliance Globalcom',
+ route: [
+ [129.08, 35.18], // Busan coast
+ [128.0, 34.8], // South Sea (water, south of coast)
+ [127.0, 34.5], // South Sea
+ [126.0, 33.8], // South of Jeju area (water)
+ [125.0, 33.0], // East China Sea
+ [124.0, 32.0], // East China Sea
+ [123.0, 31.5], // Approach Shanghai
+ [121.8, 31.2], // Shanghai coast
+ ],
+ },
+
+ // ═══ Domestic cables ═══
+ {
+ id: 'jeju-mainland-2',
+ name: '제주-본토 해저케이블 2',
+ color: '#e0e0e0',
+ landingPoints: ['제주', '해남(전남)'],
+ rfsYear: 2013,
+ owners: 'KT',
+ route: [
+ [126.53, 33.51], // Jeju north coast
+ [126.50, 33.75], // Jeju Strait
+ [126.48, 34.00], // Jeju Strait mid
+ [126.50, 34.25], // Approach mainland
+ [126.57, 34.57], // Haenam coast
+ ],
+ },
+ {
+ id: 'jeju-mainland-3',
+ name: '제주-본토 해저케이블 3',
+ color: '#bdbdbd',
+ landingPoints: ['제주', '진도(전남)'],
+ rfsYear: 2019,
+ owners: 'KT',
+ route: [
+ [126.53, 33.51], // Jeju north coast
+ [126.30, 33.80], // Jeju Strait west
+ [126.15, 34.10], // Jeju Strait mid
+ [126.20, 34.35], // Approach Jindo
+ [126.26, 34.49], // Jindo coast
+ ],
+ },
+ {
+ id: 'ulleung-mainland',
+ name: '울릉-본토 해저케이블',
+ color: '#90a4ae',
+ landingPoints: ['울릉도', '포항'],
+ rfsYear: 2016,
+ owners: 'KT',
+ route: [
+ [130.91, 37.48], // Ulleungdo coast
+ [130.50, 37.15], // East Sea
+ [130.10, 36.75], // East Sea
+ [129.70, 36.35], // Approach Pohang
+ [129.34, 36.02], // Pohang coast
+ ],
+ },
+];
+
+// Landing points in Korea for marker display
+export interface CableLandingPoint {
+ name: string;
+ lat: number;
+ lng: number;
+ cables: string[]; // cable IDs
+}
+
+export const KOREA_LANDING_POINTS: CableLandingPoint[] = [
+ { name: '부산', lat: 35.18, lng: 129.08, cables: ['apcn-2', 'apg', 'eac-c2c', 'kjcn', 'flag-north-asia', 'ncp', 'sjc', 'sjc2', 'flag-fea'] },
+ { name: '거제', lat: 34.88, lng: 128.62, cables: ['tpe'] },
+ { name: '태안', lat: 36.75, lng: 126.35, cables: ['c2c', 'ckc'] },
+ { name: '인천', lat: 37.46, lng: 126.59, cables: ['goki'] },
+ { name: '제주', lat: 33.51, lng: 126.53, cables: ['jeju-mainland-2', 'jeju-mainland-3'] },
+ { name: '해남', lat: 34.57, lng: 126.57, cables: ['jeju-mainland-2'] },
+ { name: '진도', lat: 34.49, lng: 126.26, cables: ['jeju-mainland-3'] },
+ { name: '울릉도', lat: 37.48, lng: 130.91, cables: ['ulleung-mainland'] },
+ { name: '포항', lat: 36.02, lng: 129.34, cables: ['ulleung-mainland'] },
+];
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..e72d853
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,146 @@
+export interface GeoEvent {
+ id: string;
+ timestamp: number; // unix ms
+ lat: number;
+ lng: number;
+ type: 'airstrike' | 'explosion' | 'missile_launch' | 'intercept' | 'alert' | 'impact' | 'osint';
+ source?: 'US' | 'IL' | 'IR' | 'proxy'; // 공격 주체: 미국, 이스라엘, 이란, 대리세력
+ label: string;
+ description?: string;
+ intensity?: number; // 0-100
+ imageUrl?: string; // 뉴스/위성 사진 URL
+ imageCaption?: string; // 사진 설명
+}
+
+export interface SensorLog {
+ timestamp: number;
+ seismic: number; // seismic activity level 0-100
+ airPressure: number; // hPa
+ noiseLevel: number; // dB
+ radiationLevel: number; // uSv/h
+}
+
+export interface ReplayState {
+ isPlaying: boolean;
+ currentTime: number; // unix ms
+ startTime: number; // unix ms
+ endTime: number; // unix ms
+ speed: number; // 1x, 2x, 4x, 8x
+}
+
+export interface ApiConfig {
+ eventsEndpoint: string;
+ sensorEndpoint: string;
+ pollIntervalMs: number;
+}
+
+// Aircraft tracking
+export type AircraftCategory = 'military' | 'tanker' | 'surveillance' | 'fighter' | 'cargo' | 'civilian' | 'unknown';
+
+export interface Aircraft {
+ icao24: string; // ICAO 24-bit hex address
+ callsign: string;
+ lat: number;
+ lng: number;
+ altitude: number; // meters
+ velocity: number; // m/s
+ heading: number; // degrees
+ verticalRate: number; // m/s
+ onGround: boolean;
+ category: AircraftCategory;
+ typecode?: string; // e.g. "KC135", "RC135", "RQ4"
+ typeDesc?: string; // e.g. "AIRBUS A-350-1000"
+ registration?: string; // e.g. "A6-XWC"
+ operator?: string; // e.g. "United Arab Emirates"
+ squawk?: string; // transponder code
+ trail?: [number, number][]; // recent positions [lat, lng]
+ lastSeen: number; // unix ms
+ activeStart?: number; // unix ms - when aircraft enters area
+ activeEnd?: number; // unix ms - when aircraft leaves area
+}
+
+// Satellite tracking
+export interface Satellite {
+ noradId: number;
+ name: string;
+ tle1: string;
+ tle2: string;
+ category: 'reconnaissance' | 'communications' | 'navigation' | 'weather' | 'other';
+}
+
+export interface SatellitePosition {
+ noradId: number;
+ name: string;
+ lat: number;
+ lng: number;
+ altitude: number; // km
+ category: Satellite['category'];
+ groundTrack?: [number, number][]; // predicted ground track
+}
+
+// Ship tracking (AIS)
+export type ShipCategory = 'warship' | 'carrier' | 'destroyer' | 'submarine' | 'cargo' | 'tanker' | 'patrol' | 'civilian' | 'unknown';
+
+export interface Ship {
+ mmsi: string; // Maritime Mobile Service Identity
+ name: string;
+ lat: number;
+ lng: number;
+ heading: number; // degrees
+ speed: number; // knots
+ course: number; // course over ground
+ category: ShipCategory;
+ flag?: string; // country code
+ typecode?: string;
+ typeDesc?: string; // e.g. "Cargo ship"
+ imo?: string; // IMO number
+ callSign?: string;
+ status?: string; // "Under way using engine", "Anchored", etc.
+ destination?: string;
+ eta?: string; // ISO date
+ draught?: number; // meters
+ length?: number; // meters
+ width?: number; // meters
+ trail?: [number, number][];
+ lastSeen: number; // unix ms
+ activeStart?: number; // unix ms - when ship enters area
+ activeEnd?: number; // unix ms - when ship leaves area
+}
+
+// Iran oil/gas facility
+export type OilFacilityType = 'refinery' | 'oilfield' | 'gasfield' | 'terminal' | 'petrochemical' | 'desalination';
+
+export interface OilFacility {
+ id: string;
+ name: string;
+ nameKo: string;
+ lat: number;
+ lng: number;
+ type: OilFacilityType;
+ capacityBpd?: number; // barrels per day (oil)
+ capacityMcfd?: number; // million cubic feet per day (gas)
+ capacityMgd?: number; // million gallons per day (desalination)
+ reservesBbl?: number; // billion barrels (oil reserves)
+ reservesTcf?: number; // trillion cubic feet (gas reserves)
+ operator?: string;
+ description?: string;
+ damaged?: boolean; // hit during strikes
+ damagedAt?: number; // unix ms — time when facility was struck
+ planned?: boolean; // planned US strike target
+ plannedLabel?: string; // planned strike description
+}
+
+// Layer visibility
+export interface LayerVisibility {
+ events: boolean;
+ aircraft: boolean;
+ satellites: boolean;
+ ships: boolean;
+ koreanShips: boolean;
+ airports: boolean;
+ sensorCharts: boolean;
+ oilFacilities: boolean;
+ militaryOnly: boolean;
+}
+
+export type AppMode = 'replay' | 'live';
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..e741ec4
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,83 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api/ais': {
+ target: 'https://aisapi.maritime.spglobal.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/ais/, ''),
+ secure: true,
+ },
+ '/api/rss': {
+ target: 'https://news.google.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/rss/, ''),
+ secure: true,
+ },
+ '/api/gdelt': {
+ target: 'https://api.gdeltproject.org',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/gdelt/, ''),
+ secure: true,
+ },
+ '/api/nitter1': {
+ target: 'https://xcancel.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/nitter1/, ''),
+ secure: true,
+ },
+ '/api/nitter2': {
+ target: 'https://nitter.privacydev.net',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/nitter2/, ''),
+ secure: true,
+ },
+ '/api/nitter3': {
+ target: 'https://nitter.poast.org',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/nitter3/, ''),
+ secure: true,
+ },
+ '/api/rsshub': {
+ target: 'https://rsshub.app',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/rsshub/, ''),
+ secure: true,
+ },
+ '/api/overpass': {
+ target: 'https://overpass-api.de',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/overpass/, ''),
+ secure: true,
+ },
+ '/api/khoa-hls': {
+ target: 'https://www.khoa.go.kr',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/khoa-hls/, ''),
+ secure: true,
+ },
+ '/api/kbs-hls': {
+ target: 'https://kbsapi.loomex.net',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/kbs-hls/, ''),
+ secure: true,
+ },
+ '/api/twsyndication': {
+ target: 'https://syndication.twitter.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/twsyndication/, ''),
+ secure: true,
+ },
+ '/api/publish-twitter': {
+ target: 'https://publish.twitter.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api\/publish-twitter/, ''),
+ secure: true,
+ },
+ },
+ },
+})