feat(iran): 해외시설 에너지/위험 3단계 레이어 + 나탄즈-디모나 리플레이 이벤트

- 해외시설 10개국 에너지/위험시설 데이터 56개소 (meEnergyHazardFacilities.ts)
- 이란 발전소 8→20개 확장 (화력/수력/원자력/풍력/태양광)
- 3단계 레이어 트리: 국가 → 에너지/위험 → 세부시설 (발전소/풍력/원자력/화력/석유화학/LNG/유류/위험물)
- 해외시설 총합 카운트 표시 + 각 단계별 시설 수 자동 계산
- MEEnergyHazardLayer: 시설별 SVG/이모지 아이콘 + 팝업
- 풍력단지 아이콘 한국 현황과 동일 (WindTurbineIcon export)
- 풍력단지 색상 진하게 (#00bcd4 → #0891b2)
- 풍력단지 팝업 공통 스타일 적용
- 영국 → 이스라엘 교체 (overseasUK → overseasIsrael)
- LayerVisibility 인덱스 시그니처 추가 (동적 레이어 키 지원)
- D+20 나탄즈-디모나 핵시설 교차공격 리플레이 이벤트 6건
- 에쉬콜 발전소 좌표 수정 (아슈도드 정확 위치)
- Java 17 호환: Thread.ofVirtual() → new Thread() (로컬 빌드용)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-23 10:01:27 +09:00
부모 444b7a4a8d
커밋 6e37bc1f2d
14개의 변경된 파일566개의 추가작업 그리고 79개의 파일을 삭제

파일 보기

@ -19,7 +19,7 @@
<description>KCG Monitoring Dashboard Backend</description>
<properties>
<java.version>21</java.version>
<java.version>17</java.version>
<jjwt.version>0.12.6</jjwt.version>
</properties>

파일 보기

@ -86,7 +86,7 @@ public class AirplanesLiveCollector {
@PostConstruct
public void init() {
Thread.ofVirtual().name("aircraft-init").start(() -> {
new Thread(() -> {
doInitialLoad("iran", IRAN_QUERIES, iranRegionBuffers);
iranInitDone = true;
mergePointResults("iran", iranRegionBuffers);
@ -96,7 +96,7 @@ public class AirplanesLiveCollector {
koreaInitDone = true;
mergePointResults("korea", koreaRegionBuffers);
log.info("Airplanes.live 한국 초기 로드 완료");
});
}, "aircraft-init").start();
}
private void doInitialLoad(String region, List<RegionQuery> queries, Map<String, List<AircraftDto>> buffers) {

파일 보기

@ -58,12 +58,12 @@ public class OsintCollector {
@PostConstruct
public void init() {
Thread.ofVirtual().name("osint-init").start(() -> {
new Thread(() -> {
log.info("OSINT 초기 캐시 로드 시작");
refreshCache("iran");
refreshCache("korea");
log.info("OSINT 초기 캐시 로드 완료");
});
}, "osint-init").start();
}
@Scheduled(initialDelay = 30_000, fixedDelay = 10_000)

파일 보기

@ -49,11 +49,11 @@ public class SatelliteCollector {
@PostConstruct
public void init() {
Thread.ofVirtual().name("satellite-init").start(() -> {
new Thread(() -> {
log.info("위성 TLE 초기 캐시 로드 시작");
loadCacheFromDb();
log.info("위성 TLE 초기 캐시 로드 완료");
});
}, "satellite-init").start();
}
@Scheduled(initialDelay = 60_000, fixedDelay = 600_000)

파일 보기

@ -38,10 +38,10 @@ public class PressureCollector {
@PostConstruct
public void init() {
Thread.ofVirtual().name("pressure-init").start(() -> {
new Thread(() -> {
log.info("Open-Meteo 기압 데이터 초기 로드");
collect();
});
}, "pressure-init").start();
}
@Scheduled(initialDelay = 45_000, fixedDelay = 600_000)

파일 보기

@ -31,10 +31,10 @@ public class SeismicCollector {
@PostConstruct
public void init() {
Thread.ofVirtual().name("seismic-init").start(() -> {
new Thread(() -> {
log.info("USGS 지진 데이터 초기 로드");
collect();
});
}, "seismic-init").start();
}
@Scheduled(initialDelay = 60_000, fixedDelay = 300_000)

파일 보기

@ -22,6 +22,7 @@ import { useTranslation } from 'react-i18next';
import LoginPage from './components/auth/LoginPage';
import CollectorMonitor from './components/common/CollectorMonitor';
import { FieldAnalysisModal } from './components/korea/FieldAnalysisModal';
import { filterFacilities } from './data/meEnergyHazardFacilities';
import './App.css';
function App() {
@ -66,7 +67,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
meFacilities: true,
militaryOnly: false,
overseasUS: false,
overseasUK: false,
overseasIsrael: false,
overseasIran: false,
overseasUAE: false,
overseasSaudi: false,
@ -220,8 +221,8 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
currentTime,
);
const toggleLayer = useCallback((key: keyof LayerVisibility) => {
setLayers(prev => ({ ...prev, [key]: !prev[key] }));
const toggleLayer = useCallback((key: string) => {
setLayers(prev => ({ ...prev, [key]: !prev[key as keyof typeof prev] }));
}, []);
// Handle event card click from timeline: fly to location on map
@ -481,18 +482,45 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
{ key: 'meFacilities', label: '주요시설/군사', color: '#ef4444', count: 35 },
{ key: 'sensorCharts', label: t('layers.sensorCharts'), color: '#22c55e' },
]}
overseasItems={[
{ key: 'overseasUS', label: '🇺🇸 미국', color: '#3b82f6' },
{ key: 'overseasUK', label: '🇬🇧 영국', color: '#dc2626' },
{ key: 'overseasIran', label: '🇮🇷 이란', color: '#22c55e' },
{ key: 'overseasUAE', label: '🇦🇪 UAE', color: '#f59e0b' },
{ key: 'overseasSaudi', label: '🇸🇦 사우디아라비아', color: '#84cc16' },
{ key: 'overseasOman', label: '🇴🇲 오만', color: '#e11d48' },
{ key: 'overseasQatar', label: '🇶🇦 카타르', color: '#8b5cf6' },
{ key: 'overseasKuwait', label: '🇰🇼 쿠웨이트', color: '#f97316' },
{ key: 'overseasIraq', label: '🇮🇶 이라크', color: '#65a30d' },
{ key: 'overseasBahrain', label: '🇧🇭 바레인', color: '#e11d48' },
]}
overseasItems={(() => {
const fc = (ck: string, st?: string) => filterFacilities(ck, st as never).length;
const energyChildren = (ck: string) => [
{ key: `${ck}Power`, label: '발전소', color: '#a855f7', count: fc(ck, 'power') },
{ key: `${ck}Wind`, label: '풍력단지', color: '#22d3ee', count: fc(ck, 'wind') },
{ key: `${ck}Nuclear`, label: '원자력발전소', color: '#f59e0b', count: fc(ck, 'nuclear') },
{ key: `${ck}Thermal`, label: '화력발전소', color: '#64748b', count: fc(ck, 'thermal') },
];
const hazardChildren = (ck: string) => [
{ key: `${ck}Petrochem`, label: '석유화학단지', color: '#f97316', count: fc(ck, 'petrochem') },
{ key: `${ck}Lng`, label: 'LNG저장기지', color: '#0ea5e9', count: fc(ck, 'lng') },
{ key: `${ck}OilTank`, label: '유류저장탱크', color: '#eab308', count: fc(ck, 'oil_tank') },
{ key: `${ck}HazPort`, label: '위험물항만하역시설', color: '#dc2626', count: fc(ck, 'haz_port') },
];
const fullCountry = (key: string, label: string, color: string, ck: string) => ({
key, label, color, children: [
{ key: `${ck}Energy`, label: '에너지/발전시설', color: '#a855f7', children: energyChildren(ck) },
{ key: `${ck}Hazard`, label: '위험시설', color: '#ef4444', children: hazardChildren(ck) },
],
});
const compactCountry = (key: string, label: string, color: string, ck: string) => ({
key, label, color, children: [
{ key: `${ck}Energy`, label: '에너지/발전시설', color: '#a855f7', count: filterFacilities(ck).filter(f => f.category === 'energy').length },
{ key: `${ck}Hazard`, label: '위험시설', color: '#ef4444', count: filterFacilities(ck).filter(f => f.category === 'hazard').length },
],
});
return [
fullCountry('overseasUS', '🇺🇸 미국', '#3b82f6', 'us'),
fullCountry('overseasIsrael', '🇮🇱 이스라엘', '#0ea5e9', 'il'),
fullCountry('overseasIran', '🇮🇷 이란', '#22c55e', 'ir'),
fullCountry('overseasUAE', '🇦🇪 UAE', '#f59e0b', 'ae'),
fullCountry('overseasSaudi', '🇸🇦 사우디아라비아', '#84cc16', 'sa'),
compactCountry('overseasOman', '🇴🇲 오만', '#e11d48', 'om'),
compactCountry('overseasQatar', '🇶🇦 카타르', '#8b5cf6', 'qa'),
compactCountry('overseasKuwait', '🇰🇼 쿠웨이트', '#f97316', 'kw'),
compactCountry('overseasIraq', '🇮🇶 이라크', '#65a30d', 'iq'),
compactCountry('overseasBahrain', '🇧🇭 바레인', '#e11d48', 'bh'),
];
})()}
hiddenAcCategories={hiddenAcCategories}
hiddenShipCategories={hiddenShipCategories}
onAcCategoryToggle={toggleAcCategory}

파일 보기

@ -124,9 +124,16 @@ interface OverseasItem {
key: string;
label: string;
color: string;
count?: number;
children?: OverseasItem[];
}
/** Recursively count leaf nodes (items without children) */
function countOverseasTree(item: OverseasItem): number {
if (!item.children?.length) return item.count ?? 0;
return item.children.reduce((sum, c) => sum + countOverseasTree(c), 0);
}
interface LayerPanelProps {
layers: Record<string, boolean>;
onToggle: (key: string) => void;
@ -194,6 +201,10 @@ export function LayerPanel({
.filter(([cat]) => cat !== 'civilian' && cat !== 'unknown')
.reduce((sum, [, c]) => sum + c, 0);
const overseasTotalCount = overseasItems
? overseasItems.reduce((sum, item) => sum + countOverseasTree(item), 0)
: 0;
return (
<div className="layer-panel">
<h3>LAYERS</h3>
@ -542,7 +553,7 @@ export function LayerPanel({
<LayerTreeItem
layerKey="militaryOnly"
label={t('layers.militaryOnly')}
count={militaryCount}
count={overseasTotalCount || militaryCount}
color="#f97316"
active={layers.militaryOnly ?? false}
expandable
@ -558,6 +569,7 @@ export function LayerPanel({
layerKey={item.key}
label={item.label}
color={item.color}
count={item.count ?? countOverseasTree(item)}
active={layers[item.key] ?? false}
expandable={!!item.children?.length}
isExpanded={expanded.has(`overseas-${item.key}`)}
@ -567,14 +579,34 @@ export function LayerPanel({
{item.children?.length && expanded.has(`overseas-${item.key}`) && (
<div className="layer-tree-children">
{item.children.map(child => (
<LayerTreeItem
key={child.key}
layerKey={child.key}
label={child.label}
color={child.color}
active={layers[child.key] ?? false}
onToggle={() => onToggle(child.key)}
/>
<div key={child.key}>
<LayerTreeItem
layerKey={child.key}
label={child.label}
color={child.color}
count={child.count ?? countOverseasTree(child)}
active={layers[child.key] ?? false}
expandable={!!child.children?.length}
isExpanded={expanded.has(`overseas-${child.key}`)}
onToggle={() => onToggle(child.key)}
onExpand={child.children?.length ? () => toggleExpand(`overseas-${child.key}`) : undefined}
/>
{child.children?.length && expanded.has(`overseas-${child.key}`) && (
<div className="layer-tree-children">
{child.children.map(gc => (
<LayerTreeItem
key={gc.key}
layerKey={gc.key}
label={gc.label}
color={gc.color}
count={gc.count}
active={layers[gc.key] ?? false}
onToggle={() => onToggle(gc.key)}
/>
))}
</div>
)}
</div>
))}
</div>
)}

파일 보기

@ -0,0 +1,183 @@
import { Marker, Popup } from 'react-map-gl/maplibre';
import { useState, useMemo } from 'react';
import {
ME_ENERGY_HAZARD_FACILITIES,
SUB_TYPE_META,
layerKeyToSubType,
layerKeyToCountry,
type EnergyHazardFacility,
} from '../../data/meEnergyHazardFacilities';
import { WindTurbineIcon } from '../korea/WindFarmLayer';
import type { FacilitySubType } from '../../data/meEnergyHazardFacilities';
function FacilityIcon({ subType, color, size = 18 }: { subType: FacilitySubType; color: string; size?: number }) {
const s = size;
switch (subType) {
case 'power':
return <span style={{ fontSize: s }}></span>;
case 'nuclear':
return <span style={{ fontSize: s }}></span>;
case 'thermal':
return <span style={{ fontSize: s }}>🏭</span>;
case 'petrochem': // Petrochemical - oil drum with pipe
return (
<svg width={s} height={s} viewBox="0 0 24 24" fill="none">
<ellipse cx="12" cy="6" rx="7" ry="3" fill={color} opacity="0.5" />
<rect x="5" y="6" width="14" height="14" rx="1" fill={color} opacity="0.6" />
<ellipse cx="12" cy="20" rx="7" ry="3" fill={color} opacity="0.5" />
<line x1="8" y1="6" x2="8" y2="20" stroke={color} strokeWidth="0.8" opacity="0.8" />
<line x1="16" y1="6" x2="16" y2="20" stroke={color} strokeWidth="0.8" opacity="0.8" />
<path d="M12 3 L12 1" stroke={color} strokeWidth="1.5" />
<circle cx="12" cy="0.5" r="0.5" fill={color} />
</svg>
);
case 'lng': // LNG - snowflake/cold tank
return (
<svg width={s} height={s} viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="13" r="8" fill={color} opacity="0.2" stroke={color} strokeWidth="1" />
<line x1="12" y1="5" x2="12" y2="21" stroke={color} strokeWidth="1.5" />
<line x1="4" y1="13" x2="20" y2="13" stroke={color} strokeWidth="1.5" />
<line x1="6.3" y1="7.3" x2="17.7" y2="18.7" stroke={color} strokeWidth="1.2" />
<line x1="17.7" y1="7.3" x2="6.3" y2="18.7" stroke={color} strokeWidth="1.2" />
<circle cx="12" cy="13" r="2" fill={color} opacity="0.6" />
</svg>
);
case 'oil_tank': // Oil tank - cylinder
return (
<svg width={s} height={s} viewBox="0 0 24 24" fill="none">
<ellipse cx="12" cy="7" rx="8" ry="4" fill={color} opacity="0.6" />
<rect x="4" y="7" width="16" height="12" fill={color} opacity="0.4" />
<ellipse cx="12" cy="19" rx="8" ry="4" fill={color} opacity="0.6" />
<path d="M4 7 v12" stroke={color} strokeWidth="1" />
<path d="M20 7 v12" stroke={color} strokeWidth="1" />
</svg>
);
case 'haz_port': // Hazardous port - warning triangle
return (
<svg width={s} height={s} viewBox="0 0 24 24" fill="none">
<path d="M12 2 L22 20 H2 Z" fill={color} opacity="0.3" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
<line x1="12" y1="8" x2="12" y2="14" stroke={color} strokeWidth="2" strokeLinecap="round" />
<circle cx="12" cy="17" r="1.2" fill={color} />
</svg>
);
default:
return <span style={{ fontSize: s * 0.8 }}>📍</span>;
}
}
interface Props {
layers: Record<string, boolean>;
}
export function MEEnergyHazardLayer({ layers }: Props) {
const [selected, setSelected] = useState<EnergyHazardFacility | null>(null);
// Collect active country+subType combos from layer keys
const visibleFacilities = useMemo(() => {
const active = new Set<string>(); // "countryKey:subType"
// Also check parent energy/hazard keys (e.g. omEnergy -> show all om energy)
const energySubTypes = ['power', 'wind', 'nuclear', 'thermal'] as const;
const hazardSubTypes = ['petrochem', 'lng', 'oil_tank', 'haz_port'] as const;
for (const [key, on] of Object.entries(layers)) {
if (!on) continue;
const ck = layerKeyToCountry(key);
const st = layerKeyToSubType(key);
if (ck && st) {
active.add(`${ck}:${st}`);
}
// Parent energy key (e.g. irEnergy) -> activate all energy subtypes for that country
if (ck && key.endsWith('Energy')) {
for (const s of energySubTypes) active.add(`${ck}:${s}`);
}
if (ck && key.endsWith('Hazard')) {
for (const s of hazardSubTypes) active.add(`${ck}:${s}`);
}
}
if (active.size === 0) return [];
return ME_ENERGY_HAZARD_FACILITIES.filter(f =>
active.has(`${f.countryKey}:${f.subType}`)
);
}, [layers]);
if (visibleFacilities.length === 0) return null;
return (
<>
{visibleFacilities.map(f => {
const meta = SUB_TYPE_META[f.subType];
return (
<Marker
key={f.id}
latitude={f.lat}
longitude={f.lng}
anchor="center"
onClick={e => { e.originalEvent.stopPropagation(); setSelected(f); }}
>
<div
title={f.nameKo}
style={{
cursor: 'pointer',
filter: 'drop-shadow(0 0 3px rgba(0,0,0,0.7))',
textAlign: 'center',
lineHeight: 1,
}}
>
{f.subType === 'wind' ? (
<WindTurbineIcon size={18} color={meta.color} />
) : (
<FacilityIcon subType={f.subType} color={meta.color} size={18} />
)}
</div>
</Marker>
);
})}
{selected && (
<Popup
latitude={selected.lat}
longitude={selected.lng}
anchor="bottom"
closeOnClick={false}
onClose={() => setSelected(null)}
maxWidth="260px"
className="facility-popup"
>
<div style={{
background: '#1a1e2e', color: '#e2e8f0', padding: '8px 10px',
borderRadius: 6, fontSize: 11, lineHeight: 1.5,
}}>
<div style={{ fontWeight: 700, fontSize: 13, marginBottom: 4 }}>
{SUB_TYPE_META[selected.subType].icon} {selected.nameKo}
</div>
<div style={{ fontSize: 10, color: '#94a3b8', marginBottom: 4 }}>{selected.name}</div>
<div style={{ display: 'flex', gap: 4, marginBottom: 4, flexWrap: 'wrap' }}>
<span style={{
padding: '1px 6px', borderRadius: 3, fontSize: 9, fontWeight: 600,
background: SUB_TYPE_META[selected.subType].color + '30',
color: SUB_TYPE_META[selected.subType].color,
border: `1px solid ${SUB_TYPE_META[selected.subType].color}50`,
}}>
{SUB_TYPE_META[selected.subType].label}
</span>
{selected.capacityMW && (
<span style={{
padding: '1px 6px', borderRadius: 3, fontSize: 9, fontWeight: 600,
background: 'rgba(255,255,255,0.08)', color: '#e2e8f0',
}}>
{selected.capacityMW.toLocaleString()} MW
</span>
)}
</div>
<div style={{ fontSize: 10, color: '#94a3b8' }}>{selected.description}</div>
<div style={{ fontSize: 9, color: '#64748b', marginTop: 4 }}>
{selected.lat.toFixed(4)}N, {selected.lng.toFixed(4)}E
</div>
</div>
</Popup>
)}
</>
);
}

파일 보기

@ -10,6 +10,7 @@ import { SeismicMarker } from '../layers/SeismicMarker';
import { OilFacilityLayer } from './OilFacilityLayer';
import { AirportLayer } from './AirportLayer';
import { MEFacilityLayer } from './MEFacilityLayer';
import { MEEnergyHazardLayer } from './MEEnergyHazardLayer';
import { iranOilFacilities } from '../../data/oilFacilities';
import { middleEastAirports } from '../../data/airports';
import type { GeoEvent, Aircraft, SatellitePosition, Ship, LayerVisibility } from '../../types';
@ -273,6 +274,7 @@ export function SatelliteMap({ events, currentTime, aircraft, satellites, ships,
{layers.oilFacilities && <OilFacilityLayer facilities={iranOilFacilities} currentTime={currentTime} />}
{layers.airports && <AirportLayer airports={middleEastAirports} />}
{layers.meFacilities && <MEFacilityLayer />}
<MEEnergyHazardLayer layers={layers} />
</Map>
);
}

파일 보기

@ -3,18 +3,22 @@ import { Marker, Popup } from 'react-map-gl/maplibre';
import { KOREA_WIND_FARMS } from '../../data/windFarms';
import type { WindFarm } from '../../data/windFarms';
const COLOR = '#00bcd4';
const COLOR = '#0891b2';
function WindTurbineIcon({ size = 18 }: { size?: number }) {
export function WindTurbineIcon({ size = 20, color }: { size?: number; color?: string }) {
const c = color || COLOR;
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<line x1="12" y1="10" x2="11" y2="23" stroke={COLOR} strokeWidth="1.5" />
<line x1="12" y1="10" x2="13" y2="23" stroke={COLOR} strokeWidth="1.5" />
<circle cx="12" cy="9" r="1.8" fill={COLOR} />
<path d="M12 9 L11.5 1 Q12 0 12.5 1 Z" fill={COLOR} opacity="0.9" />
<path d="M12 9 L18 14 Q18.5 13 17.5 12.5 Z" fill={COLOR} opacity="0.9" />
<path d="M12 9 L6 14 Q5.5 13 6.5 12.5 Z" fill={COLOR} opacity="0.9" />
<line x1="8" y1="23" x2="16" y2="23" stroke={COLOR} strokeWidth="1.5" />
<svg width={size} height={size} viewBox="0 0 32 32" fill="none">
<path d="M15 14 L14.2 29 L17.8 29 L17 14 Z" fill={c} opacity="0.7" />
<rect x="11" y="28.5" width="10" height="2" rx="1" fill={c} opacity="0.5" />
<ellipse cx="16" cy="12" rx="2.5" ry="1.5" fill={c} />
<circle cx="16" cy="12" r="1.2" fill="#fff" opacity="0.9" />
<circle cx="16" cy="12" r="0.6" fill={c} />
<path d="M16 12 L15 1.5 Q16 0.5 17 1.5 Z" fill={c} opacity="0.85" />
<path d="M16 12 L24.5 18 Q24 19.5 22.5 18.5 Z" fill={c} opacity="0.85" />
<path d="M16 12 L7.5 18 Q8 19.5 9.5 18.5 Z" fill={c} opacity="0.85" />
<path d="M3 30 Q5.5 28 8 30 Q10.5 32 13 30" stroke={c} strokeWidth="0.8" fill="none" opacity="0.4" />
<path d="M19 30 Q21.5 28 24 30 Q26.5 32 29 30" stroke={c} strokeWidth="0.8" fill="none" opacity="0.4" />
</svg>
);
}
@ -37,7 +41,7 @@ export function WindFarmLayer() {
className="flex flex-col items-center cursor-pointer"
style={{ filter: `drop-shadow(0 0 3px ${COLOR}88)` }}
>
<WindTurbineIcon size={18} />
<WindTurbineIcon size={22} />
<div style={{
fontSize: 6, color: COLOR, marginTop: 1,
textShadow: '0 0 3px #000, 0 0 3px #000',
@ -53,39 +57,53 @@ export function WindFarmLayer() {
<Popup longitude={selected.lng} latitude={selected.lat}
onClose={() => setSelected(null)} closeOnClick={false}
anchor="bottom" maxWidth="280px" className="gl-popup">
<div className="popup-body-sm" style={{ minWidth: 200 }}>
<div className="popup-header" style={{ background: COLOR, color: '#000', gap: 6, padding: '6px 10px' }}>
<span style={{ fontSize: 16 }}>🌀</span>
<strong>{selected.name}</strong>
<div style={{ background: '#1a1e2e', borderRadius: 6, overflow: 'hidden', minWidth: 200 }}>
{/* Header - full width */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
background: 'rgba(0,188,212,0.15)', padding: '6px 10px',
borderBottom: '1px solid rgba(0,188,212,0.3)',
}}>
<WindTurbineIcon size={16} />
<span style={{ fontSize: 13, fontWeight: 700, color: '#e2e8f0' }}>{selected.name}</span>
</div>
<div style={{ display: 'flex', gap: 4, marginBottom: 6 }}>
<span style={{
background: STATUS_COLOR[selected.status] || '#666', color: '#fff',
padding: '2px 8px', borderRadius: 3, fontSize: 10, fontWeight: 700,
}}>
{selected.status}
</span>
<span style={{
background: COLOR, color: '#000',
padding: '2px 8px', borderRadius: 3, fontSize: 10, fontWeight: 700,
}}>
</span>
<span style={{
background: '#333', color: '#ccc',
padding: '2px 8px', borderRadius: 3, fontSize: 10,
}}>
{selected.region}
</span>
</div>
<div className="popup-grid" style={{ gap: '2px 12px' }}>
<div><span className="popup-label"> : </span><strong>{selected.capacityMW} MW</strong></div>
<div><span className="popup-label"> : </span><strong>{selected.turbines}</strong></div>
{selected.year && <div><span className="popup-label"> : </span><strong>{selected.year}</strong></div>}
<div><span className="popup-label"> : </span>{selected.region}</div>
</div>
<div style={{ marginTop: 6, fontSize: 10, color: '#999' }}>
{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E
{/* Body */}
<div style={{ padding: '8px 10px' }}>
{/* Tags */}
<div style={{ display: 'flex', gap: 4, marginBottom: 8, flexWrap: 'wrap' }}>
<span style={{
background: STATUS_COLOR[selected.status] || '#666', color: '#fff',
padding: '2px 8px', borderRadius: 3, fontSize: 10, fontWeight: 700,
}}>
{selected.status}
</span>
<span style={{
background: 'rgba(0,188,212,0.2)', color: COLOR, border: `1px solid ${COLOR}50`,
padding: '2px 8px', borderRadius: 3, fontSize: 10, fontWeight: 700,
}}>
</span>
<span style={{
background: 'rgba(255,255,255,0.06)', color: '#94a3b8',
padding: '2px 8px', borderRadius: 3, fontSize: 10,
}}>
{selected.region}
</span>
</div>
{/* Info grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px 12px', fontSize: 11, color: '#cbd5e1' }}>
<div><span style={{ color: '#64748b' }}> </span><strong style={{ color: COLOR }}>{selected.capacityMW} MW</strong></div>
<div><span style={{ color: '#64748b' }}> </span><strong>{selected.turbines}</strong></div>
{selected.year && <div><span style={{ color: '#64748b' }}> </span><strong>{selected.year}</strong></div>}
<div><span style={{ color: '#64748b' }}> </span>{selected.region}</div>
</div>
{/* Coordinates */}
<div style={{ marginTop: 8, fontSize: 10, color: '#64748b' }}>
{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E
</div>
</div>
</div>
</Popup>

파일 보기

@ -0,0 +1,180 @@
// Middle East Energy & Hazard Facilities (OSINT + OpenStreetMap)
export type FacilitySubType =
| 'power' | 'wind' | 'nuclear' | 'thermal' // energy
| 'petrochem' | 'lng' | 'oil_tank' | 'haz_port'; // hazard
export interface EnergyHazardFacility {
id: string;
name: string;
nameKo: string;
lat: number;
lng: number;
country: string; // ISO-2
countryKey: string; // overseas layer key prefix (us, il, ir, ae, sa, om, qa, kw, iq, bh)
category: 'energy' | 'hazard';
subType: FacilitySubType;
capacityMW?: number;
description: string;
}
export const SUB_TYPE_META: Record<FacilitySubType, { label: string; color: string; icon: string }> = {
power: { label: '발전소', color: '#a855f7', icon: '⚡' },
wind: { label: '풍력단지', color: '#22d3ee', icon: '🌬' },
nuclear: { label: '원자력발전소', color: '#f59e0b', icon: '☢' },
thermal: { label: '화력발전소', color: '#64748b', icon: '🏭' },
petrochem: { label: '석유화학단지', color: '#f97316', icon: '🛢' },
lng: { label: 'LNG저장기지', color: '#0ea5e9', icon: '❄' },
oil_tank: { label: '유류저장탱크', color: '#eab308', icon: '🛢' },
haz_port: { label: '위험물항만하역시설', color: '#dc2626', icon: '⚠' },
};
// layer key -> subType mapping
export function layerKeyToSubType(key: string): FacilitySubType | null {
if (key.endsWith('Power')) return 'power';
if (key.endsWith('Wind')) return 'wind';
if (key.endsWith('Nuclear')) return 'nuclear';
if (key.endsWith('Thermal')) return 'thermal';
if (key.endsWith('Petrochem')) return 'petrochem';
if (key.endsWith('Lng')) return 'lng';
if (key.endsWith('OilTank')) return 'oil_tank';
if (key.endsWith('HazPort')) return 'haz_port';
return null;
}
export function layerKeyToCountry(key: string): string | null {
const m = key.match(/^(us|il|ir|ae|sa|om|qa|kw|iq|bh)/);
return m ? m[1] : null;
}
export const ME_ENERGY_HAZARD_FACILITIES: EnergyHazardFacility[] = [
// ════════════════════════════════════════════
// 🇺🇸 미국 (중동 주둔 시설 + 에너지 인프라)
// ════════════════════════════════════════════
{ id: 'US-E01', name: 'Al Udeid Power Plant', nameKo: '알우데이드 발전소', lat: 25.1175, lng: 51.3150, country: 'US', countryKey: 'us', category: 'energy', subType: 'power', capacityMW: 200, description: '미군 알우데이드 기지 전용 발전시설' },
{ id: 'US-H01', name: 'Bahrain NAVSUP Fuel Depot', nameKo: '바레인 미해군 유류저장소', lat: 26.2361, lng: 50.6036, country: 'US', countryKey: 'us', category: 'hazard', subType: 'oil_tank', description: 'NSA Bahrain 유류 보급 시설' },
{ id: 'US-H02', name: 'Jebel Ali US Navy Fuel Terminal', nameKo: '제벨알리 미해군 연료터미널', lat: 25.0100, lng: 55.0600, country: 'US', countryKey: 'us', category: 'hazard', subType: 'haz_port', description: '미 제5함대 연료 보급 항만' },
// ════════════════════════════════════════════
// 🇮🇱 이스라엘
// ════════════════════════════════════════════
// Energy
{ id: 'IL-E01', name: 'Orot Rabin Power Station', nameKo: '오롯 라빈 화력발전소', lat: 32.3915, lng: 34.8610, country: 'IL', countryKey: 'il', category: 'energy', subType: 'thermal', capacityMW: 2590, description: '이스라엘 최대 석탄/가스 복합 발전소 (하데라)' },
{ id: 'IL-E02', name: 'Rutenberg Power Station', nameKo: '루텐베르그 화력발전소', lat: 31.6200, lng: 34.5300, country: 'IL', countryKey: 'il', category: 'energy', subType: 'thermal', capacityMW: 2250, description: '아슈켈론 석탄 화력발전소' },
{ id: 'IL-E03', name: 'Eshkol Power Station', nameKo: '에쉬콜 발전소', lat: 31.7940, lng: 34.6350, country: 'IL', countryKey: 'il', category: 'energy', subType: 'thermal', capacityMW: 1096, description: '아슈도드 해안 천연가스 복합화력 (IEC 운영)' },
{ id: 'IL-E04', name: 'Hagit Power Station', nameKo: '하깃 발전소', lat: 32.5600, lng: 35.0800, country: 'IL', countryKey: 'il', category: 'energy', subType: 'power', capacityMW: 600, description: '북부 가스터빈 발전소' },
{ id: 'IL-E05', name: 'Dimona Nuclear Research Center', nameKo: '디모나 원자력연구센터', lat: 31.0014, lng: 35.1467, country: 'IL', countryKey: 'il', category: 'energy', subType: 'nuclear', description: '네게브 원자력연구시설 (IRR-2)' },
{ id: 'IL-E06', name: 'Ashalim Solar Power Station', nameKo: '아샬림 태양광발전소', lat: 31.1300, lng: 34.6600, country: 'IL', countryKey: 'il', category: 'energy', subType: 'power', capacityMW: 310, description: '네게브 사막 CSP+PV 복합 발전' },
// Hazard
{ id: 'IL-H01', name: 'Haifa Bay Petrochemical Complex', nameKo: '하이파만 석유화학단지', lat: 32.8100, lng: 35.0500, country: 'IL', countryKey: 'il', category: 'hazard', subType: 'petrochem', description: 'Oil Refineries Ltd. + Bazan Group 정유/석유화학 단지' },
{ id: 'IL-H02', name: 'Ashdod Oil Terminal', nameKo: '아시도드 유류터미널', lat: 31.8200, lng: 34.6350, country: 'IL', countryKey: 'il', category: 'hazard', subType: 'oil_tank', description: 'EAPC 원유 수입 터미널 + 저장탱크' },
{ id: 'IL-H03', name: 'Ashkelon Desalination & Energy Hub', nameKo: '아슈켈론 에너지허브', lat: 31.6100, lng: 34.5400, country: 'IL', countryKey: 'il', category: 'hazard', subType: 'haz_port', description: '해수담수화 + LNG 수입 터미널' },
{ id: 'IL-H04', name: 'Eilat-Ashkelon Pipeline Terminal', nameKo: 'EAPC 에일라트 터미널', lat: 29.5500, lng: 34.9500, country: 'IL', countryKey: 'il', category: 'hazard', subType: 'oil_tank', description: '홍해 원유 수입 파이프라인 터미널' },
// ════════════════════════════════════════════
// 🇮🇷 이란
// ════════════════════════════════════════════
// Energy
{ id: 'IR-E01', name: 'Bushehr Nuclear Power Plant', nameKo: '부셰르 원자력발전소', lat: 28.8267, lng: 50.8867, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', capacityMW: 1000, description: '이란 유일 상업 원전 (VVER-1000)' },
{ id: 'IR-E02', name: 'Ramin Thermal Power Plant', nameKo: '라민 화력발전소', lat: 31.3100, lng: 48.7400, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1890, description: '아바즈 인근 증기 화력발전소' },
{ id: 'IR-E03', name: 'Neka Thermal Power Plant', nameKo: '네카 화력발전소', lat: 36.6500, lng: 53.3300, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2074, description: '카스피해 연안 최대 화력발전소' },
{ id: 'IR-E04', name: 'Shahid Rajaee Power Plant', nameKo: '샤히드 라자이 발전소', lat: 36.3700, lng: 52.9900, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1780, description: '가즈빈 복합화력' },
{ id: 'IR-E05', name: 'Isfahan Nuclear Facility (UCF)', nameKo: '이스파한 핵시설 (UCF)', lat: 32.7200, lng: 51.7200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '우라늄전환시설' },
{ id: 'IR-E06', name: 'Natanz Enrichment Facility', nameKo: '나탄즈 우라늄농축시설', lat: 33.7250, lng: 51.7267, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '주요 원심분리기 농축시설 (지하)' },
{ id: 'IR-E07', name: 'Manjil-Rudbar Wind Farm', nameKo: '만질-루드바르 풍력단지', lat: 36.7400, lng: 49.4000, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'wind', capacityMW: 91, description: '이란 최대 풍력발전단지' },
{ id: 'IR-E08', name: 'Bandar Abbas Power Plant', nameKo: '반다르아바스 발전소', lat: 27.2000, lng: 56.2500, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1057, description: '호르무즈해협 연안 발전소' },
{ id: 'IR-E09', name: 'Shahid Montazeri Power Plant', nameKo: '샤히드 몬타제리 발전소', lat: 32.6500, lng: 51.6800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1600, description: '이스파한 복합화력 발전소' },
{ id: 'IR-E10', name: 'Shahid Salimi (Neka) Power Plant', nameKo: '샤히드 살리미 발전소', lat: 36.6300, lng: 53.3000, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2400, description: '마잔다란주 가스복합 발전소' },
{ id: 'IR-E11', name: 'Besat Power Plant', nameKo: '베사트 발전소', lat: 35.8300, lng: 50.9800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1000, description: '테헤란 남부 가스터빈 발전소' },
{ id: 'IR-E12', name: 'Parand Power Plant', nameKo: '파란드 복합화력발전소', lat: 35.4700, lng: 51.0100, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1536, description: '테헤란 서남부 복합화력' },
{ id: 'IR-E13', name: 'Shahid Rajaei Dam & Hydro', nameKo: '샤히드 라자이 수력발전소', lat: 36.1500, lng: 53.2800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 225, description: '사리 인근 수력발전 댐' },
{ id: 'IR-E14', name: 'Karun-3 Hydropower Plant', nameKo: '카룬-3 수력발전소', lat: 31.9200, lng: 49.8500, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 2000, description: '이란 최대 수력발전소 (후제스탄주)' },
{ id: 'IR-E15', name: 'Dez Dam Hydropower', nameKo: '데즈댐 수력발전소', lat: 32.6100, lng: 48.7700, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 520, description: '후제스탄주 데즈강 수력발전' },
{ id: 'IR-E16', name: 'Tabriz Thermal Power Plant', nameKo: '타브리즈 화력발전소', lat: 38.0600, lng: 46.3200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1386, description: '동아제르바이잔주 복합화력' },
{ id: 'IR-E17', name: 'Zarand Solar Power Plant', nameKo: '자란드 태양광발전소', lat: 30.8100, lng: 56.5600, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 10, description: '케르만주 태양광 시범단지' },
{ id: 'IR-E18', name: 'Fordow Enrichment Facility', nameKo: '포르도 우라늄농축시설', lat: 34.8800, lng: 51.5800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '지하 우라늄 농축시설 (FFEP, 쿰 인근)' },
{ id: 'IR-E19', name: 'Arak Heavy Water Reactor', nameKo: '아라크 중수로', lat: 34.0400, lng: 49.2400, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: 'IR-40 중수 연구용 원자로 (마르카지주)' },
{ id: 'IR-E20', name: 'Binaloud Wind Farm', nameKo: '비날루드 풍력단지', lat: 36.2300, lng: 58.6900, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'wind', capacityMW: 28, description: '라자비호라산주 풍력발전' },
// Hazard
{ id: 'IR-H01', name: 'South Pars Gas Complex (Assaluyeh)', nameKo: '사우스파르스 가스단지 (아살루예)', lat: 27.4800, lng: 52.6100, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'petrochem', description: '세계 최대 가스전 육상 처리시설 (20+ 페이즈)' },
{ id: 'IR-H02', name: 'Kharg Island Oil Terminal', nameKo: '하르그섬 원유터미널', lat: 29.2300, lng: 50.3100, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'oil_tank', description: '이란 원유 수출의 90% 처리 (저장 2,800만 배럴)' },
{ id: 'IR-H03', name: 'Bandar Imam Khomeini Petrochemical', nameKo: '반다르 이맘호메이니 석유화학', lat: 30.4300, lng: 49.0800, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'petrochem', description: 'Mahshahr 특별경제구역 석유화학단지' },
{ id: 'IR-H04', name: 'Tombak LNG Terminal', nameKo: '톰박 LNG터미널', lat: 27.5200, lng: 52.5500, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'lng', description: 'Iran LNG 수출 터미널 (건설중)' },
{ id: 'IR-H05', name: 'Bandar Abbas Oil Refinery', nameKo: '반다르아바스 정유소', lat: 27.2100, lng: 56.2800, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'petrochem', description: '일 320,000배럴 정유시설' },
{ id: 'IR-H06', name: 'Lavan Island Oil Terminal', nameKo: '라반섬 원유터미널', lat: 26.8100, lng: 53.3600, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'oil_tank', description: '페르시아만 원유 저장/선적 시설' },
// ════════════════════════════════════════════
// 🇦🇪 UAE
// ════════════════════════════════════════════
// Energy
{ id: 'AE-E01', name: 'Barakah Nuclear Power Plant', nameKo: '바라카 원자력발전소', lat: 23.9592, lng: 52.2567, country: 'AE', countryKey: 'ae', category: 'energy', subType: 'nuclear', capacityMW: 5600, description: '아랍 최초 상업 원전 (APR-1400 x4)' },
{ id: 'AE-E02', name: 'Jebel Ali Power & Desalination', nameKo: '제벨알리 발전/담수', lat: 25.0200, lng: 55.1100, country: 'AE', countryKey: 'ae', category: 'energy', subType: 'thermal', capacityMW: 8695, description: '세계 최대 복합 발전/담수 단지' },
{ id: 'AE-E03', name: 'Shams Solar Power Station', nameKo: '샴스 태양광발전소', lat: 23.5800, lng: 53.7100, country: 'AE', countryKey: 'ae', category: 'energy', subType: 'power', capacityMW: 100, description: '아부다비 CSP 태양열 발전' },
{ id: 'AE-E04', name: 'Hassyan Clean Coal Power Plant', nameKo: '하시안 청정석탄발전소', lat: 24.9600, lng: 55.0300, country: 'AE', countryKey: 'ae', category: 'energy', subType: 'thermal', capacityMW: 2400, description: '두바이 석탄→가스 전환 중' },
// Hazard
{ id: 'AE-H01', name: 'Ruwais Industrial Complex (ADNOC)', nameKo: '루와이스 산업단지 (ADNOC)', lat: 24.1100, lng: 52.7300, country: 'AE', countryKey: 'ae', category: 'hazard', subType: 'petrochem', description: 'ADNOC 정유/석유화학 통합단지 (세계 최대급)' },
{ id: 'AE-H02', name: 'Das Island LNG Terminal', nameKo: '다스섬 LNG터미널', lat: 25.1600, lng: 52.8700, country: 'AE', countryKey: 'ae', category: 'hazard', subType: 'lng', description: 'ADGAS LNG 수출 터미널 (연 570만톤)' },
{ id: 'AE-H03', name: 'Fujairah Oil Terminal (FOSC)', nameKo: '푸자이라 유류터미널', lat: 25.1200, lng: 56.3400, country: 'AE', countryKey: 'ae', category: 'hazard', subType: 'oil_tank', description: '세계 3대 벙커링 허브 (저장 1,400만m3)' },
{ id: 'AE-H04', name: 'Jebel Ali Free Zone Port', nameKo: '제벨알리 자유무역항', lat: 25.0000, lng: 55.0700, country: 'AE', countryKey: 'ae', category: 'hazard', subType: 'haz_port', description: '중동 최대 항만 (위험물 취급)' },
// ════════════════════════════════════════════
// 🇸🇦 사우디아라비아
// ════════════════════════════════════════════
// Energy
{ id: 'SA-E01', name: 'Shoaiba Power & Desalination', nameKo: '쇼아이바 발전/담수', lat: 20.7000, lng: 39.5100, country: 'SA', countryKey: 'sa', category: 'energy', subType: 'thermal', capacityMW: 5600, description: '홍해 연안 세계 최대급 복합 발전/담수' },
{ id: 'SA-E02', name: 'Rabigh Power Plant', nameKo: '라비그 발전소', lat: 22.8000, lng: 39.0200, country: 'SA', countryKey: 'sa', category: 'energy', subType: 'thermal', capacityMW: 2100, description: '홍해 연안 가스복합 발전소' },
{ id: 'SA-E03', name: 'Dumat Al Jandal Wind Farm', nameKo: '두마트알잔달 풍력단지', lat: 29.8100, lng: 39.8700, country: 'SA', countryKey: 'sa', category: 'energy', subType: 'wind', capacityMW: 400, description: '중동 최대 풍력단지' },
{ id: 'SA-E04', name: 'Jubail IWPP', nameKo: '주바일 발전소', lat: 27.0200, lng: 49.6200, country: 'SA', countryKey: 'sa', category: 'energy', subType: 'thermal', capacityMW: 2745, description: '동부 산업도시 복합 발전' },
// Hazard
{ id: 'SA-H01', name: 'Ras Tanura Oil Terminal', nameKo: '라스타누라 원유터미널', lat: 26.6400, lng: 50.1600, country: 'SA', countryKey: 'sa', category: 'hazard', subType: 'oil_tank', description: '세계 최대 해상 원유 선적 시설 (일 600만 배럴)' },
{ id: 'SA-H02', name: 'Jubail Industrial City (SABIC)', nameKo: '주바일 산업단지 (SABIC)', lat: 27.0000, lng: 49.6500, country: 'SA', countryKey: 'sa', category: 'hazard', subType: 'petrochem', description: '세계 최대 석유화학 산업단지' },
{ id: 'SA-H03', name: 'Yanbu Industrial City', nameKo: '얀부 산업단지', lat: 23.9600, lng: 38.2400, country: 'SA', countryKey: 'sa', category: 'hazard', subType: 'petrochem', description: '홍해 연안 정유/석유화학 단지' },
{ id: 'SA-H04', name: 'Ras Al-Khair LNG Import', nameKo: '라스알카이르 LNG', lat: 27.4800, lng: 49.2600, country: 'SA', countryKey: 'sa', category: 'hazard', subType: 'lng', description: 'LNG 수입/가스화 터미널' },
{ id: 'SA-H05', name: 'Abqaiq Oil Processing', nameKo: '아브카이크 원유처리시설', lat: 25.9400, lng: 49.6800, country: 'SA', countryKey: 'sa', category: 'hazard', subType: 'oil_tank', description: '세계 최대 원유 안정화 시설 (2019 공격 대상)' },
// ════════════════════════════════════════════
// 🇴🇲 오만
// ════════════════════════════════════════════
{ id: 'OM-E01', name: 'Barka Power & Desalination', nameKo: '바르카 발전/담수', lat: 23.6800, lng: 57.8700, country: 'OM', countryKey: 'om', category: 'energy', subType: 'thermal', capacityMW: 2007, description: 'GDF Suez 운영 복합발전' },
{ id: 'OM-E02', name: 'Dhofar Wind Farm', nameKo: '도파르 풍력단지', lat: 17.0200, lng: 54.1000, country: 'OM', countryKey: 'om', category: 'energy', subType: 'wind', capacityMW: 50, description: 'GCC 최초 대형 풍력단지' },
{ id: 'OM-H01', name: 'Sohar Industrial Port', nameKo: '소하르 산업항', lat: 24.3600, lng: 56.7400, country: 'OM', countryKey: 'om', category: 'hazard', subType: 'petrochem', description: '정유소+석유화학+알루미늄 제련단지' },
{ id: 'OM-H02', name: 'Qalhat LNG Terminal', nameKo: '칼하트 LNG터미널', lat: 22.9200, lng: 59.3700, country: 'OM', countryKey: 'om', category: 'hazard', subType: 'lng', description: 'Oman LNG 수출 (연 1,060만톤)' },
// ════════════════════════════════════════════
// 🇶🇦 카타르
// ════════════════════════════════════════════
{ id: 'QA-E01', name: 'Ras Laffan Power Plant', nameKo: '라스라판 발전소', lat: 25.9100, lng: 51.5500, country: 'QA', countryKey: 'qa', category: 'energy', subType: 'thermal', capacityMW: 2730, description: '카타르 최대 발전소' },
{ id: 'QA-H01', name: 'Ras Laffan Industrial City', nameKo: '라스라판 산업단지', lat: 25.9200, lng: 51.5300, country: 'QA', countryKey: 'qa', category: 'hazard', subType: 'lng', description: '세계 최대 LNG 수출기지 (QatarEnergy, 연 7,700만톤)' },
{ id: 'QA-H02', name: 'Mesaieed Industrial City', nameKo: '메사이드 산업단지', lat: 24.9900, lng: 51.5600, country: 'QA', countryKey: 'qa', category: 'hazard', subType: 'petrochem', description: 'QatarEnergy 정유/석유화학/비료 단지' },
{ id: 'QA-H03', name: 'Dukhan Oil Field Terminal', nameKo: '두칸 유전터미널', lat: 25.4300, lng: 50.7700, country: 'QA', countryKey: 'qa', category: 'hazard', subType: 'oil_tank', description: '서부 해안 육상 유전 터미널' },
// ════════════════════════════════════════════
// 🇰🇼 쿠웨이트
// ════════════════════════════════════════════
{ id: 'KW-E01', name: 'Az-Zour Power Plant', nameKo: '아즈주르 발전소', lat: 28.7200, lng: 48.3800, country: 'KW', countryKey: 'kw', category: 'energy', subType: 'thermal', capacityMW: 4800, description: '쿠웨이트 최대 발전/담수' },
{ id: 'KW-H01', name: 'Mina Al Ahmadi Refinery', nameKo: '미나알아흐마디 정유소', lat: 29.0600, lng: 48.1500, country: 'KW', countryKey: 'kw', category: 'hazard', subType: 'petrochem', description: 'KNPC 운영 (일 466,000배럴)' },
{ id: 'KW-H02', name: 'Az-Zour LNG Import Terminal', nameKo: '아즈주르 LNG터미널', lat: 28.7100, lng: 48.3500, country: 'KW', countryKey: 'kw', category: 'hazard', subType: 'lng', description: '쿠웨이트 LNG 수입 터미널' },
{ id: 'KW-H03', name: 'Mina Abdullah Oil Tank Farm', nameKo: '미나압둘라 유류저장기지', lat: 29.0000, lng: 48.1700, country: 'KW', countryKey: 'kw', category: 'hazard', subType: 'oil_tank', description: '남부 원유 저장/선적' },
// ════════════════════════════════════════════
// 🇮🇶 이라크
// ════════════════════════════════════════════
{ id: 'IQ-E01', name: 'Basra Gas Power Plant', nameKo: '바스라 가스발전소', lat: 30.5100, lng: 47.7800, country: 'IQ', countryKey: 'iq', category: 'energy', subType: 'thermal', capacityMW: 1500, description: '남부 이라크 최대 발전소' },
{ id: 'IQ-H01', name: 'Basra Oil Terminal (ABOT)', nameKo: '알바스라 원유터미널', lat: 29.6800, lng: 48.8000, country: 'IQ', countryKey: 'iq', category: 'hazard', subType: 'oil_tank', description: '이라크 원유 수출의 85% (페르시아만)' },
{ id: 'IQ-H02', name: 'Khor Al-Zubair Port', nameKo: '코르알주바이르 항', lat: 30.1700, lng: 47.8700, country: 'IQ', countryKey: 'iq', category: 'hazard', subType: 'haz_port', description: '이라크 주요 위험물 하역항' },
{ id: 'IQ-H03', name: 'Rumaila Oil Field', nameKo: '루마일라 유전', lat: 30.6300, lng: 47.4300, country: 'IQ', countryKey: 'iq', category: 'hazard', subType: 'oil_tank', description: '이라크 최대 유전 (일 150만 배럴)' },
// ════════════════════════════════════════════
// 🇧🇭 바레인
// ════════════════════════════════════════════
{ id: 'BH-E01', name: 'Al Dur Power & Water Plant', nameKo: '알두르 발전/담수', lat: 25.9400, lng: 50.6200, country: 'BH', countryKey: 'bh', category: 'energy', subType: 'thermal', capacityMW: 1234, description: '바레인 최대 발전소' },
{ id: 'BH-H01', name: 'Sitra Oil Refinery (BAPCO)', nameKo: '시트라 정유소 (BAPCO)', lat: 26.1500, lng: 50.6100, country: 'BH', countryKey: 'bh', category: 'hazard', subType: 'petrochem', description: '바레인 유일 정유시설 (일 267,000배럴)' },
{ id: 'BH-H02', name: 'Khalifa Bin Salman Port', nameKo: '칼리파빈살만항', lat: 26.0200, lng: 50.5500, country: 'BH', countryKey: 'bh', category: 'hazard', subType: 'haz_port', description: '바레인 주요 무역항 (위험물 하역)' },
];
// Helper: filter by country key and subType
export function filterFacilities(countryKey: string, subType?: FacilitySubType): EnergyHazardFacility[] {
return ME_ENERGY_HAZARD_FACILITIES.filter(f =>
f.countryKey === countryKey && (subType ? f.subType === subType : true)
);
}

파일 보기

@ -1429,6 +1429,48 @@ export const sampleEvents: GeoEvent[] = [
label: 'UN 안보리 — 호르무즈 해협 긴급회의 소집',
description: 'UN 안보리, 호르무즈 해협 상선 피격 관련 긴급회의 소집. 중국·러시아 즉각 휴전 촉구, 미국 항행자유 강조.',
},
// ═══ D+20 (2026-03-21) 나탄즈-디모나 핵시설 교차공격 ═══
{
id: 'd20-il1', timestamp: T0 + 20 * DAY + 4 * HOUR,
lat: 33.7250, lng: 51.7267, type: 'strike',
label: '나탄즈 — 이스라엘 핵시설 공습',
description: 'IAF, 이란 나탄즈 우라늄 농축시설 정밀 타격. 이란 원자력청 "나탄즈 농축시설이 공격 표적이 됐다" 확인. IAEA 방사능 유출 미확인.',
imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Natanz_nuclear.jpg/320px-Natanz_nuclear.jpg',
imageCaption: '나탄즈 핵시설 위성사진 (Wikimedia Commons)',
},
{
id: 'd20-ir-assess', timestamp: T0 + 20 * DAY + 6 * HOUR,
lat: 33.7250, lng: 51.7267, type: 'alert',
label: '나탄즈 — 이란 방사능 조사 착수',
description: '이란 원자력안전센터, 나탄즈 시설 인근 방사성 오염물질 배출 가능성 정밀 기술 조사. "현재까지 방사성 물질 누출 보고 없음, 인근 주민 위협 없음" 발표.',
},
{
id: 'd20-ir1', timestamp: T0 + 20 * DAY + 10 * HOUR,
lat: 31.0014, lng: 35.1467, type: 'strike',
label: '디모나 — 이란 보복 미사일 공격',
description: 'IRGC, 나탄즈 피격 보복으로 이스라엘 디모나 핵연구센터 겨냥 탄도미사일 발사. 이스라엘 방공 요격 실패, 최소 30명 이상 사상자 발생. 핵연구센터 직접 피해는 미확인.',
imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/Negev_Nuclear_Research_Center.jpg/320px-Negev_Nuclear_Research_Center.jpg',
imageCaption: '디모나 네게브 핵연구센터 (Wikimedia Commons)',
},
{
id: 'd20-il-def', timestamp: T0 + 20 * DAY + 10.5 * HOUR,
lat: 31.0014, lng: 35.1467, type: 'alert',
label: '디모나 — 요격 실패 조사 착수',
description: '이스라엘군, 이란발 탄도미사일 요격 실패 경위 조사 착수. 요격 미사일이 목표물 격추에 실패, 미사일이 마을에 충돌. 막대한 재산 피해.',
},
{
id: 'd20-iaea', timestamp: T0 + 20 * DAY + 12 * HOUR,
lat: 48.2082, lng: 16.3738, type: 'alert',
label: 'IAEA — 양측 핵시설 상황 파악 중',
description: 'IAEA, 나탄즈 및 디모나 핵시설 상황 파악 중. 그로시 사무총장 "핵사고 위험 회피 위해 군사행동 자제 거듭 촉구". 양측 시설 모두 비정상 방사능 수치 미감지.',
},
{
id: 'd20-p1', timestamp: T0 + 20 * DAY + 14 * HOUR,
lat: 38.8977, lng: -77.0365, type: 'alert',
label: '워싱턴 — 미국 핵시설 공격 우려 성명',
description: '미 국무부, 이란의 디모나 공격에 강력 규탄. "핵시설 겨냥 군사행동은 국제법 중대 위반" 경고. 이스라엘 방공체계 지원 강화 발표.',
},
];
// 24시간 동안 10분 간격 센서 데이터 생성

파일 보기

@ -145,7 +145,7 @@ export interface LayerVisibility {
meFacilities: boolean;
militaryOnly: boolean;
overseasUS: boolean;
overseasUK: boolean;
overseasIsrael: boolean;
overseasIran: boolean;
overseasUAE: boolean;
overseasSaudi: boolean;
@ -154,6 +154,8 @@ export interface LayerVisibility {
overseasKuwait: boolean;
overseasIraq: boolean;
overseasBahrain: boolean;
// Dynamic keys for energy/hazard sub-layers
[key: string]: boolean;
}
export type AppMode = 'replay' | 'live';