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:
부모
444b7a4a8d
커밋
6e37bc1f2d
@ -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>
|
||||
)}
|
||||
|
||||
183
frontend/src/components/iran/MEEnergyHazardLayer.tsx
Normal file
183
frontend/src/components/iran/MEEnergyHazardLayer.tsx
Normal file
@ -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>
|
||||
|
||||
180
frontend/src/data/meEnergyHazardFacilities.ts
Normal file
180
frontend/src/data/meEnergyHazardFacilities.ts
Normal file
@ -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';
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user