diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 2c40b73..3701952 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -1,6 +1,6 @@ import { useRef, useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Map, NavigationControl, Marker, Popup, Source, Layer } from 'react-map-gl/maplibre'; +import { Map, NavigationControl, Marker, Source, Layer } from 'react-map-gl/maplibre'; import type { MapRef } from 'react-map-gl/maplibre'; import { ScatterplotLayer, TextLayer } from '@deck.gl/layers'; import { DeckGLOverlay } from '../layers/DeckGLOverlay'; @@ -20,6 +20,7 @@ import { EezLayer } from './EezLayer'; // PiracyLayer, WindFarmLayer, PortLayer, MilitaryBaseLayer, GovBuildingLayer, // NKLaunchLayer, NKMissileEventLayer → useStaticDeckLayers로 전환됨 import { ChineseFishingOverlay } from './ChineseFishingOverlay'; +import { StaticFacilityPopup } from './StaticFacilityPopup'; // HazardFacilityLayer, CnFacilityLayer, JpFacilityLayer → useStaticDeckLayers로 전환됨 import { AnalysisOverlay } from './AnalysisOverlay'; import { FleetClusterLayer } from './FleetClusterLayer'; @@ -641,203 +642,9 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF ...analysisDeckLayers, ].filter(Boolean)} /> {/* 정적 마커 클릭 Popup — 통합 리치 디자인 */} - {staticPickInfo && (() => { - const obj = staticPickInfo.object; - const kind = staticPickInfo.kind; - const lat = obj.lat ?? obj.launchLat ?? 0; - const lng = obj.lng ?? obj.launchLng ?? 0; - if (!lat || !lng) return null; - - // ── kind + subType 기반 메타 결정 ── - const SUB_META: Record> = { - hazard: { - petrochemical: { icon: '🏭', color: '#f97316', label: '석유화학단지' }, - lng: { icon: '🔵', color: '#06b6d4', label: 'LNG저장기지' }, - oilTank: { icon: '🛢️', color: '#eab308', label: '유류저장탱크' }, - hazardPort: { icon: '⚠️', color: '#ef4444', label: '위험물항만하역' }, - nuclear: { icon: '☢️', color: '#a855f7', label: '원자력발전소' }, - thermal: { icon: '🔥', color: '#64748b', label: '화력발전소' }, - shipyard: { icon: '🚢', color: '#0ea5e9', label: '조선소' }, - wastewater: { icon: '💧', color: '#10b981', label: '폐수처리장' }, - heavyIndustry: { icon: '⚙️', color: '#94a3b8', label: '시멘트/제철소' }, - }, - overseas: { - nuclear: { icon: '☢️', color: '#ef4444', label: '핵발전소' }, - thermal: { icon: '🔥', color: '#f97316', label: '화력발전소' }, - naval: { icon: '⚓', color: '#3b82f6', label: '해군기지' }, - airbase: { icon: '✈️', color: '#22d3ee', label: '공군기지' }, - army: { icon: '🪖', color: '#22c55e', label: '육군기지' }, - shipyard:{ icon: '🚢', color: '#94a3b8', label: '조선소' }, - }, - militaryBase: { - naval: { icon: '⚓', color: '#3b82f6', label: '해군기지' }, - airforce:{ icon: '✈️', color: '#22d3ee', label: '공군기지' }, - army: { icon: '🪖', color: '#22c55e', label: '육군기지' }, - missile: { icon: '🚀', color: '#ef4444', label: '미사일기지' }, - joint: { icon: '⭐', color: '#f59e0b', label: '합동사령부' }, - }, - govBuilding: { - executive: { icon: '🏛️', color: '#f59e0b', label: '행정부' }, - legislature: { icon: '🏛️', color: '#3b82f6', label: '입법부' }, - military_hq: { icon: '⭐', color: '#ef4444', label: '군사령부' }, - intelligence: { icon: '🔍', color: '#8b5cf6', label: '정보기관' }, - foreign: { icon: '🌐', color: '#06b6d4', label: '외교부' }, - maritime: { icon: '⚓', color: '#0ea5e9', label: '해양기관' }, - defense: { icon: '🛡️', color: '#dc2626', label: '국방부' }, - }, - nkLaunch: { - icbm: { icon: '🚀', color: '#dc2626', label: 'ICBM 발사장' }, - irbm: { icon: '🚀', color: '#ef4444', label: 'IRBM/MRBM' }, - srbm: { icon: '🎯', color: '#f97316', label: '단거리탄도미사일' }, - slbm: { icon: '🔱', color: '#3b82f6', label: 'SLBM 발사' }, - cruise: { icon: '✈️', color: '#8b5cf6', label: '순항미사일' }, - artillery: { icon: '💥', color: '#eab308', label: '해안포/장사정포' }, - mlrs: { icon: '💥', color: '#f59e0b', label: '방사포(MLRS)' }, - }, - coastGuard: { - hq: { icon: '🏢', color: '#3b82f6', label: '본청' }, - regional: { icon: '🏢', color: '#60a5fa', label: '지방청' }, - station: { icon: '🚔', color: '#22d3ee', label: '해양경찰서' }, - substation: { icon: '🏠', color: '#94a3b8', label: '파출소' }, - vts: { icon: '📡', color: '#f59e0b', label: 'VTS센터' }, - navy: { icon: '⚓', color: '#3b82f6', label: '해군부대' }, - }, - airport: { - international: { icon: '✈️', color: '#a78bfa', label: '국제공항' }, - domestic: { icon: '🛩️', color: '#60a5fa', label: '국내공항' }, - military: { icon: '✈️', color: '#ef4444', label: '군용비행장' }, - }, - navWarning: { - danger: { icon: '⚠️', color: '#ef4444', label: '위험' }, - caution: { icon: '⚠️', color: '#eab308', label: '주의' }, - info: { icon: 'ℹ️', color: '#3b82f6', label: '정보' }, - }, - piracy: { - critical: { icon: '☠️', color: '#ef4444', label: '극고위험' }, - high: { icon: '☠️', color: '#f97316', label: '고위험' }, - moderate: { icon: '☠️', color: '#eab308', label: '주의' }, - }, - }; - - const KIND_DEFAULT: Record = { - port: { icon: '⚓', color: '#3b82f6', label: '항구' }, - windFarm: { icon: '🌀', color: '#00bcd4', label: '풍력단지' }, - militaryBase: { icon: '🪖', color: '#ef4444', label: '군사시설' }, - govBuilding: { icon: '🏛️', color: '#f59e0b', label: '정부기관' }, - nkLaunch: { icon: '🚀', color: '#dc2626', label: '북한 발사장' }, - nkMissile: { icon: '💣', color: '#ef4444', label: '미사일 낙하' }, - coastGuard: { icon: '🚔', color: '#4dabf7', label: '해양경찰' }, - airport: { icon: '✈️', color: '#a78bfa', label: '공항' }, - navWarning: { icon: '⚠️', color: '#eab308', label: '항행경보' }, - piracy: { icon: '☠️', color: '#ef4444', label: '해적위험' }, - infra: { icon: '⚡', color: '#ffeb3b', label: '발전/변전' }, - hazard: { icon: '⚠️', color: '#ef4444', label: '위험시설' }, - cnFacility: { icon: '📍', color: '#ef4444', label: '중국시설' }, - jpFacility: { icon: '📍', color: '#f472b6', label: '일본시설' }, - }; - - // subType 키 결정 - const subKey = obj.type ?? obj.subType ?? obj.level ?? ''; - const subGroup = kind === 'cnFacility' || kind === 'jpFacility' ? 'overseas' : kind; - const meta = SUB_META[subGroup]?.[subKey] ?? KIND_DEFAULT[kind] ?? { icon: '📍', color: '#64748b', label: kind }; - - // 국가 플래그 - const COUNTRY_FLAG: Record = { CN: '🇨🇳', JP: '🇯🇵', KP: '🇰🇵', KR: '🇰🇷', TW: '🇹🇼' }; - const flag = kind === 'cnFacility' ? '🇨🇳' : kind === 'jpFacility' ? '🇯🇵' : COUNTRY_FLAG[obj.country] ?? ''; - const countryName = kind === 'cnFacility' ? '중국' : kind === 'jpFacility' ? '일본' - : { CN: '중국', JP: '일본', KP: '북한', KR: '한국', TW: '대만' }[obj.country] ?? ''; - - // 이름 결정 - const title = obj.nameKo || obj.name || obj.launchNameKo || obj.title || kind; - - return ( - setStaticPickInfo(null)} closeOnClick={false} - maxWidth="280px" className="gl-popup" - > -
- {/* 컬러 헤더 */} -
- {meta.icon} {title} -
- {/* 배지 행 */} -
- - {meta.label} - - {flag && ( - - {flag} {countryName} - - )} - {kind === 'hazard' && ( - ⚠️ 위험시설 - )} - {kind === 'port' && ( - - {obj.type === 'major' ? '주요항' : '중소항'} - - )} - {kind === 'airport' && obj.intl && ( - 국제선 - )} -
- {/* 설명 */} - {obj.description && ( -
{obj.description}
- )} - {obj.detail && ( -
{obj.detail}
- )} - {obj.note && ( -
{obj.note}
- )} - {/* 필드 그리드 */} -
- {obj.operator &&
운영: {obj.operator}
} - {obj.capacity &&
규모: {obj.capacity}
} - {obj.output &&
출력: {obj.output}
} - {obj.source &&
연료: {obj.source}
} - {obj.capacityMW &&
용량: {obj.capacityMW}MW
} - {obj.turbines &&
터빈: {obj.turbines}기
} - {obj.status &&
상태: {obj.status}
} - {obj.year &&
연도: {obj.year}년
} - {obj.region &&
지역: {obj.region}
} - {obj.org &&
기관: {obj.org}
} - {obj.area &&
해역: {obj.area}
} - {obj.altitude &&
고도: {obj.altitude}
} - {obj.address &&
주소: {obj.address}
} - {obj.recentUse &&
최근 사용: {obj.recentUse}
} - {obj.recentIncidents != null &&
최근 1년: {obj.recentIncidents}건
} - {obj.icao &&
ICAO: {obj.icao}
} - {kind === 'nkMissile' && ( - <> - {obj.typeKo &&
미사일: {obj.typeKo}
} - {obj.date &&
발사일: {obj.date} {obj.time}
} - {obj.distanceKm &&
사거리: {obj.distanceKm}km
} - {obj.altitudeKm &&
최고고도: {obj.altitudeKm}km
} - {obj.flightMin &&
비행시간: {obj.flightMin}분
} - {obj.launchNameKo &&
발사지: {obj.launchNameKo}
} - - )} - {obj.name && obj.nameKo && obj.name !== obj.nameKo && ( -
영문: {obj.name}
- )} -
- {lat.toFixed(4)}°N, {lng.toFixed(4)}°E -
-
-
-
- ); - })()} + {staticPickInfo && ( + setStaticPickInfo(null)} /> + )} {layers.osint && } {layers.eez && } diff --git a/frontend/src/components/korea/StaticFacilityPopup.tsx b/frontend/src/components/korea/StaticFacilityPopup.tsx new file mode 100644 index 0000000..9d0f8ba --- /dev/null +++ b/frontend/src/components/korea/StaticFacilityPopup.tsx @@ -0,0 +1,207 @@ +import { Popup } from 'react-map-gl/maplibre'; +import type { StaticPickInfo } from '../../hooks/layers/types'; + +interface StaticFacilityPopupProps { + pickInfo: StaticPickInfo; + onClose: () => void; +} + +const StaticFacilityPopup = ({ pickInfo, onClose }: StaticFacilityPopupProps) => { + const obj = pickInfo.object as any; // eslint-disable-line @typescript-eslint/no-explicit-any -- StaticPickedObject union requires loose access + const kind = pickInfo.kind; + const lat = obj.lat ?? obj.launchLat ?? 0; + const lng = obj.lng ?? obj.launchLng ?? 0; + if (!lat || !lng) return null; + + // ── kind + subType 기반 메타 결정 ── + const SUB_META: Record> = { + hazard: { + petrochemical: { icon: '🏭', color: '#f97316', label: '석유화학단지' }, + lng: { icon: '🔵', color: '#06b6d4', label: 'LNG저장기지' }, + oilTank: { icon: '🛢️', color: '#eab308', label: '유류저장탱크' }, + hazardPort: { icon: '⚠️', color: '#ef4444', label: '위험물항만하역' }, + nuclear: { icon: '☢️', color: '#a855f7', label: '원자력발전소' }, + thermal: { icon: '🔥', color: '#64748b', label: '화력발전소' }, + shipyard: { icon: '🚢', color: '#0ea5e9', label: '조선소' }, + wastewater: { icon: '💧', color: '#10b981', label: '폐수처리장' }, + heavyIndustry: { icon: '⚙️', color: '#94a3b8', label: '시멘트/제철소' }, + }, + overseas: { + nuclear: { icon: '☢️', color: '#ef4444', label: '핵발전소' }, + thermal: { icon: '🔥', color: '#f97316', label: '화력발전소' }, + naval: { icon: '⚓', color: '#3b82f6', label: '해군기지' }, + airbase: { icon: '✈️', color: '#22d3ee', label: '공군기지' }, + army: { icon: '🪖', color: '#22c55e', label: '육군기지' }, + shipyard:{ icon: '🚢', color: '#94a3b8', label: '조선소' }, + }, + militaryBase: { + naval: { icon: '⚓', color: '#3b82f6', label: '해군기지' }, + airforce:{ icon: '✈️', color: '#22d3ee', label: '공군기지' }, + army: { icon: '🪖', color: '#22c55e', label: '육군기지' }, + missile: { icon: '🚀', color: '#ef4444', label: '미사일기지' }, + joint: { icon: '⭐', color: '#f59e0b', label: '합동사령부' }, + }, + govBuilding: { + executive: { icon: '🏛️', color: '#f59e0b', label: '행정부' }, + legislature: { icon: '🏛️', color: '#3b82f6', label: '입법부' }, + military_hq: { icon: '⭐', color: '#ef4444', label: '군사령부' }, + intelligence: { icon: '🔍', color: '#8b5cf6', label: '정보기관' }, + foreign: { icon: '🌐', color: '#06b6d4', label: '외교부' }, + maritime: { icon: '⚓', color: '#0ea5e9', label: '해양기관' }, + defense: { icon: '🛡️', color: '#dc2626', label: '국방부' }, + }, + nkLaunch: { + icbm: { icon: '🚀', color: '#dc2626', label: 'ICBM 발사장' }, + irbm: { icon: '🚀', color: '#ef4444', label: 'IRBM/MRBM' }, + srbm: { icon: '🎯', color: '#f97316', label: '단거리탄도미사일' }, + slbm: { icon: '🔱', color: '#3b82f6', label: 'SLBM 발사' }, + cruise: { icon: '✈️', color: '#8b5cf6', label: '순항미사일' }, + artillery: { icon: '💥', color: '#eab308', label: '해안포/장사정포' }, + mlrs: { icon: '💥', color: '#f59e0b', label: '방사포(MLRS)' }, + }, + coastGuard: { + hq: { icon: '🏢', color: '#3b82f6', label: '본청' }, + regional: { icon: '🏢', color: '#60a5fa', label: '지방청' }, + station: { icon: '🚔', color: '#22d3ee', label: '해양경찰서' }, + substation: { icon: '🏠', color: '#94a3b8', label: '파출소' }, + vts: { icon: '📡', color: '#f59e0b', label: 'VTS센터' }, + navy: { icon: '⚓', color: '#3b82f6', label: '해군부대' }, + }, + airport: { + international: { icon: '✈️', color: '#a78bfa', label: '국제공항' }, + domestic: { icon: '🛩️', color: '#60a5fa', label: '국내공항' }, + military: { icon: '✈️', color: '#ef4444', label: '군용비행장' }, + }, + navWarning: { + danger: { icon: '⚠️', color: '#ef4444', label: '위험' }, + caution: { icon: '⚠️', color: '#eab308', label: '주의' }, + info: { icon: 'ℹ️', color: '#3b82f6', label: '정보' }, + }, + piracy: { + critical: { icon: '☠️', color: '#ef4444', label: '극고위험' }, + high: { icon: '☠️', color: '#f97316', label: '고위험' }, + moderate: { icon: '☠️', color: '#eab308', label: '주의' }, + }, + }; + + const KIND_DEFAULT: Record = { + port: { icon: '⚓', color: '#3b82f6', label: '항구' }, + windFarm: { icon: '🌀', color: '#00bcd4', label: '풍력단지' }, + militaryBase: { icon: '🪖', color: '#ef4444', label: '군사시설' }, + govBuilding: { icon: '🏛️', color: '#f59e0b', label: '정부기관' }, + nkLaunch: { icon: '🚀', color: '#dc2626', label: '북한 발사장' }, + nkMissile: { icon: '💣', color: '#ef4444', label: '미사일 낙하' }, + coastGuard: { icon: '🚔', color: '#4dabf7', label: '해양경찰' }, + airport: { icon: '✈️', color: '#a78bfa', label: '공항' }, + navWarning: { icon: '⚠️', color: '#eab308', label: '항행경보' }, + piracy: { icon: '☠️', color: '#ef4444', label: '해적위험' }, + infra: { icon: '⚡', color: '#ffeb3b', label: '발전/변전' }, + hazard: { icon: '⚠️', color: '#ef4444', label: '위험시설' }, + cnFacility: { icon: '📍', color: '#ef4444', label: '중국시설' }, + jpFacility: { icon: '📍', color: '#f472b6', label: '일본시설' }, + }; + + // subType 키 결정 + const subKey = obj.type ?? obj.subType ?? obj.level ?? ''; + const subGroup = kind === 'cnFacility' || kind === 'jpFacility' ? 'overseas' : kind; + const meta = SUB_META[subGroup]?.[subKey] ?? KIND_DEFAULT[kind] ?? { icon: '📍', color: '#64748b', label: kind }; + + // 국가 플래그 + const COUNTRY_FLAG: Record = { CN: '🇨🇳', JP: '🇯🇵', KP: '🇰🇵', KR: '🇰🇷', TW: '🇹🇼' }; + const flag = kind === 'cnFacility' ? '🇨🇳' : kind === 'jpFacility' ? '🇯🇵' : COUNTRY_FLAG[obj.country] ?? ''; + const countryName = kind === 'cnFacility' ? '중국' : kind === 'jpFacility' ? '일본' + : { CN: '중국', JP: '일본', KP: '북한', KR: '한국', TW: '대만' }[obj.country] ?? ''; + + // 이름 결정 + const title = obj.nameKo || obj.name || obj.launchNameKo || obj.title || kind; + + return ( + +
+ {/* 컬러 헤더 */} +
+ {meta.icon} {title} +
+ {/* 배지 행 */} +
+ + {meta.label} + + {flag && ( + + {flag} {countryName} + + )} + {kind === 'hazard' && ( + ⚠️ 위험시설 + )} + {kind === 'port' && ( + + {obj.type === 'major' ? '주요항' : '중소항'} + + )} + {kind === 'airport' && obj.intl && ( + 국제선 + )} +
+ {/* 설명 */} + {obj.description && ( +
{obj.description}
+ )} + {obj.detail && ( +
{obj.detail}
+ )} + {obj.note && ( +
{obj.note}
+ )} + {/* 필드 그리드 */} +
+ {obj.operator &&
운영: {obj.operator}
} + {obj.capacity &&
규모: {obj.capacity}
} + {obj.output &&
출력: {obj.output}
} + {obj.source &&
연료: {obj.source}
} + {obj.capacityMW &&
용량: {obj.capacityMW}MW
} + {obj.turbines &&
터빈: {obj.turbines}기
} + {obj.status &&
상태: {obj.status}
} + {obj.year &&
연도: {obj.year}년
} + {obj.region &&
지역: {obj.region}
} + {obj.org &&
기관: {obj.org}
} + {obj.area &&
해역: {obj.area}
} + {obj.altitude &&
고도: {obj.altitude}
} + {obj.address &&
주소: {obj.address}
} + {obj.recentUse &&
최근 사용: {obj.recentUse}
} + {obj.recentIncidents != null &&
최근 1년: {obj.recentIncidents}건
} + {obj.icao &&
ICAO: {obj.icao}
} + {kind === 'nkMissile' && ( + <> + {obj.typeKo &&
미사일: {obj.typeKo}
} + {obj.date &&
발사일: {obj.date} {obj.time}
} + {obj.distanceKm &&
사거리: {obj.distanceKm}km
} + {obj.altitudeKm &&
최고고도: {obj.altitudeKm}km
} + {obj.flightMin &&
비행시간: {obj.flightMin}분
} + {obj.launchNameKo &&
발사지: {obj.launchNameKo}
} + + )} + {obj.name && obj.nameKo && obj.name !== obj.nameKo && ( +
영문: {obj.name}
+ )} +
+ {lat.toFixed(4)}°N, {lng.toFixed(4)}°E +
+
+
+
+ ); +}; + +export { StaticFacilityPopup };