diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a46dd8c..9239ad7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -101,6 +101,10 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { militaryOnly: false, overseasChina: false, overseasJapan: false, + cnPower: false, + cnMilitary: false, + jpPower: false, + jpMilitary: false, hazardPetrochemical: false, hazardLng: false, hazardOilTank: false, @@ -640,8 +644,20 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { { key: 'industryHeavy', label: '시멘트/제철소', color: '#94a3b8', count: 5, group: '산업공정/제조시설' }, ]} overseasItems={[ - { key: 'overseasChina', label: '🇨🇳 중국', color: '#ef4444' }, - { key: 'overseasJapan', label: '🇯🇵 일본', color: '#f472b6' }, + { + key: 'overseasChina', label: '🇨🇳 중국', color: '#ef4444', + children: [ + { key: 'cnPower', label: '발전소', color: '#a855f7' }, + { key: 'cnMilitary', label: '주요군사시설', color: '#ef4444' }, + ], + }, + { + key: 'overseasJapan', label: '🇯🇵 일본', color: '#f472b6', + children: [ + { key: 'jpPower', label: '발전소', color: '#a855f7' }, + { key: 'jpMilitary', label: '주요군사시설', color: '#ef4444' }, + ], + }, ]} hiddenAcCategories={hiddenAcCategories} hiddenShipCategories={hiddenShipCategories} diff --git a/frontend/src/components/common/LayerPanel.tsx b/frontend/src/components/common/LayerPanel.tsx index fc85524..ee0c1da 100644 --- a/frontend/src/components/common/LayerPanel.tsx +++ b/frontend/src/components/common/LayerPanel.tsx @@ -124,6 +124,7 @@ interface OverseasItem { key: string; label: string; color: string; + children?: OverseasItem[]; } interface LayerPanelProps { @@ -552,14 +553,32 @@ export function LayerPanel({ {expanded.has('militaryOnly') && overseasItems && overseasItems.length > 0 && (
{overseasItems.map(item => ( - onToggle(item.key)} - /> +
+ onToggle(item.key)} + onExpand={() => toggleExpand(`overseas-${item.key}`)} + /> + {item.children?.length && expanded.has(`overseas-${item.key}`) && ( +
+ {item.children.map(child => ( + onToggle(child.key)} + /> + ))} +
+ )} +
))}
)} diff --git a/frontend/src/components/korea/CnFacilityLayer.tsx b/frontend/src/components/korea/CnFacilityLayer.tsx new file mode 100644 index 0000000..9209555 --- /dev/null +++ b/frontend/src/components/korea/CnFacilityLayer.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { Marker, Popup } from 'react-map-gl/maplibre'; +import { CN_POWER_PLANTS, CN_MILITARY_FACILITIES, type CnFacility } from '../../data/cnFacilities'; + +interface Props { + type: 'power' | 'military'; +} + +const SUBTYPE_META: Record = { + nuclear: { color: '#a855f7', icon: '☢', label: '핵발전' }, + thermal: { color: '#f97316', icon: '⚡', label: '화력발전' }, + naval: { color: '#3b82f6', icon: '⚓', label: '해군기지' }, + airbase: { color: '#22d3ee', icon: '✈', label: '공군기지' }, + army: { color: '#ef4444', icon: '★', label: '육군/사령부' }, + shipyard: { color: '#f59e0b', icon: '⚙', label: '조선소' }, +}; + +export function CnFacilityLayer({ type }: Props) { + const [popup, setPopup] = useState(null); + const facilities = type === 'power' ? CN_POWER_PLANTS : CN_MILITARY_FACILITIES; + + return ( + <> + {facilities.map(f => { + const meta = SUBTYPE_META[f.subType]; + return ( + { e.originalEvent.stopPropagation(); setPopup(f); }} + > +
+ {meta.icon} +
+
+ ); + })} + + {popup && ( + setPopup(null)} + closeOnClick={false} + maxWidth="220px" + > +
+
{popup.name}
+
+ {SUBTYPE_META[popup.subType].label} +
+ {popup.operator && ( +
운영: {popup.operator}
+ )} + {popup.description && ( +
{popup.description}
+ )} +
+
+ )} + + ); +} diff --git a/frontend/src/components/korea/JpFacilityLayer.tsx b/frontend/src/components/korea/JpFacilityLayer.tsx new file mode 100644 index 0000000..0725ec0 --- /dev/null +++ b/frontend/src/components/korea/JpFacilityLayer.tsx @@ -0,0 +1,81 @@ +import { useState } from 'react'; +import { Marker, Popup } from 'react-map-gl/maplibre'; +import { JP_POWER_PLANTS, JP_MILITARY_FACILITIES, type JpFacility } from '../../data/jpFacilities'; + +interface Props { + type: 'power' | 'military'; +} + +const SUBTYPE_META: Record = { + nuclear: { color: '#a855f7', icon: '☢', label: '핵발전' }, + thermal: { color: '#f97316', icon: '⚡', label: '화력발전' }, + naval: { color: '#3b82f6', icon: '⚓', label: '해군기지' }, + airbase: { color: '#22d3ee', icon: '✈', label: '공군기지' }, + army: { color: '#ef4444', icon: '★', label: '육군' }, +}; + +export function JpFacilityLayer({ type }: Props) { + const [popup, setPopup] = useState(null); + const facilities = type === 'power' ? JP_POWER_PLANTS : JP_MILITARY_FACILITIES; + + return ( + <> + {facilities.map(f => { + const meta = SUBTYPE_META[f.subType]; + return ( + { e.originalEvent.stopPropagation(); setPopup(f); }} + > +
+ {meta.icon} +
+
+ ); + })} + + {popup && ( + setPopup(null)} + closeOnClick={false} + maxWidth="220px" + > +
+
{popup.name}
+
+ {SUBTYPE_META[popup.subType].label} +
+ {popup.operator && ( +
운영: {popup.operator}
+ )} + {popup.description && ( +
{popup.description}
+ )} +
+
+ )} + + ); +} diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 6991a87..6a79913 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -22,6 +22,8 @@ import { NKLaunchLayer } from './NKLaunchLayer'; import { NKMissileEventLayer } from './NKMissileEventLayer'; import { ChineseFishingOverlay } from './ChineseFishingOverlay'; import { HazardFacilityLayer } from './HazardFacilityLayer'; +import { CnFacilityLayer } from './CnFacilityLayer'; +import { JpFacilityLayer } from './JpFacilityLayer'; import { fetchKoreaInfra } from '../../services/infra'; import type { PowerFacility } from '../../services/infra'; import type { Ship, Aircraft, SatellitePosition } from '../../types'; @@ -279,6 +281,10 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre {layers.industryShipyard && } {layers.industryWastewater && } {layers.industryHeavy && } + {layers.cnPower && } + {layers.cnMilitary && } + {layers.jpPower && } + {layers.jpMilitary && } {layers.airports && } {layers.coastGuard && } {layers.navWarning && } diff --git a/frontend/src/data/cnFacilities.ts b/frontend/src/data/cnFacilities.ts new file mode 100644 index 0000000..c7b9dec --- /dev/null +++ b/frontend/src/data/cnFacilities.ts @@ -0,0 +1,141 @@ +export interface CnFacility { + id: string; + name: string; + subType: 'nuclear' | 'thermal' | 'naval' | 'airbase' | 'army' | 'shipyard'; + lat: number; + lng: number; + operator?: string; + description?: string; +} + +export const CN_POWER_PLANTS: CnFacility[] = [ + { + id: 'cn-npp-hongyanhe', + name: '홍옌허(红沿河) 핵발전소', + subType: 'nuclear', + lat: 40.87, + lng: 121.02, + operator: '中国大唐集团', + description: '가압경수로 6기, 라오닝성 — 한반도 최근접 핵발전소', + }, + { + id: 'cn-npp-tianwan', + name: '톈완(田湾) 핵발전소', + subType: 'nuclear', + lat: 34.71, + lng: 119.45, + operator: '江苏核电', + description: '러시아 VVER-1000 설계, 장쑤성', + }, + { + id: 'cn-npp-qinshan', + name: '진산(秦山) 핵발전소', + subType: 'nuclear', + lat: 30.44, + lng: 120.96, + operator: '中核集团', + description: '중국 최초 상업 핵발전소, 저장성', + }, + { + id: 'cn-npp-ningde', + name: '닝더(宁德) 핵발전소', + subType: 'nuclear', + lat: 26.73, + lng: 120.12, + operator: '中国大唐集团', + description: '가압경수로 4기, 푸젠성', + }, + { + id: 'cn-thermal-dalian', + name: '다롄 화력발전소', + subType: 'thermal', + lat: 38.85, + lng: 121.55, + operator: '大连电力', + description: '석탄화력, 라오닝성', + }, + { + id: 'cn-thermal-qinhuangdao', + name: '친황다오 화력발전소', + subType: 'thermal', + lat: 39.93, + lng: 119.58, + operator: '华能国际', + description: '석탄화력 대형 기지, 허베이성', + }, + { + id: 'cn-thermal-tianjin', + name: '톈진 화력발전소', + subType: 'thermal', + lat: 39.08, + lng: 117.20, + operator: '华能集团', + description: '석탄화력, 톈진시', + }, +]; + +export const CN_MILITARY_FACILITIES: CnFacility[] = [ + { + id: 'cn-mil-qingdao', + name: '칭다오 해군기지', + subType: 'naval', + lat: 36.07, + lng: 120.26, + operator: '해군 북부전구', + description: '항모전단 모항, 핵잠수함 기지', + }, + { + id: 'cn-mil-lushun', + name: '뤼순(旅順) 해군기지', + subType: 'naval', + lat: 38.85, + lng: 121.24, + operator: '해군 북부전구', + description: '잠수함·구축함 기지, 보하이만 입구', + }, + { + id: 'cn-mil-dalian-shipyard', + name: '다롄 조선소 (항모건조)', + subType: 'shipyard', + lat: 38.92, + lng: 121.62, + operator: '中国船舶重工', + description: '랴오닝함·산둥함 건조, 항모 4번함 건조 중', + }, + { + id: 'cn-mil-shenyang-cmd', + name: '북부전구 사령부', + subType: 'army', + lat: 41.80, + lng: 123.42, + operator: '해방군 북부전구', + description: '한반도·동북아 담당, 선양시', + }, + { + id: 'cn-mil-shenyang-air', + name: '선양 공군기지', + subType: 'airbase', + lat: 41.77, + lng: 123.49, + operator: '공군 북부전구', + description: 'J-16 전투기 배치, 북부전구 핵심기지', + }, + { + id: 'cn-mil-dandong', + name: '단둥 군사시설', + subType: 'army', + lat: 40.13, + lng: 124.38, + operator: '해방군 육군', + description: '북중 접경 전진기지, 한반도 작전 담당', + }, + { + id: 'cn-mil-zhoushan', + name: '저우산 해군기지', + subType: 'naval', + lat: 30.00, + lng: 122.10, + operator: '해군 동부전구', + description: '동중국해 주력 함대 기지', + }, +]; diff --git a/frontend/src/data/jpFacilities.ts b/frontend/src/data/jpFacilities.ts new file mode 100644 index 0000000..e7d4f60 --- /dev/null +++ b/frontend/src/data/jpFacilities.ts @@ -0,0 +1,150 @@ +export interface JpFacility { + id: string; + name: string; + subType: 'nuclear' | 'thermal' | 'naval' | 'airbase' | 'army'; + lat: number; + lng: number; + operator?: string; + description?: string; +} + +export const JP_POWER_PLANTS: JpFacility[] = [ + { + id: 'jp-npp-genkai', + name: '겐카이(玄海) 핵발전소', + subType: 'nuclear', + lat: 33.52, + lng: 129.84, + operator: '규슈전력', + description: '가압경수로 4기, 사가현 — 한반도 최근접 원전', + }, + { + id: 'jp-npp-sendai', + name: '센다이(川内) 핵발전소', + subType: 'nuclear', + lat: 31.84, + lng: 130.19, + operator: '규슈전력', + description: '가압경수로 2기, 가고시마현', + }, + { + id: 'jp-npp-ohi', + name: '오이(大飯) 핵발전소', + subType: 'nuclear', + lat: 35.53, + lng: 135.65, + operator: '간사이전력', + description: '가압경수로 4기, 후쿠이현 — 일본 최대 출력', + }, + { + id: 'jp-npp-takahama', + name: '다카하마(高浜) 핵발전소', + subType: 'nuclear', + lat: 35.51, + lng: 135.50, + operator: '간사이전력', + description: '가압경수로 4기, 후쿠이현', + }, + { + id: 'jp-npp-shika', + name: '시카(志賀) 핵발전소', + subType: 'nuclear', + lat: 37.07, + lng: 136.72, + operator: '호쿠리쿠전력', + description: '비등수형경수로 2기, 이시카와현 (2024 지진 피해)', + }, + { + id: 'jp-npp-higashidori', + name: '히가시도리(東通) 핵발전소', + subType: 'nuclear', + lat: 41.18, + lng: 141.37, + operator: '도호쿠전력', + description: '비등수형경수로, 아오모리현', + }, + { + id: 'jp-thermal-matsuura', + name: '마쓰우라(松浦) 화력발전소', + subType: 'thermal', + lat: 33.33, + lng: 129.73, + operator: '전원개발(J-Power)', + description: '석탄화력, 나가사키현 — 대한해협 인접', + }, + { + id: 'jp-thermal-hekinan', + name: '헤키난(碧南) 화력발전소', + subType: 'thermal', + lat: 34.87, + lng: 136.95, + operator: '주부전력', + description: '석탄화력, 아이치현 — 일본 최대 석탄화력', + }, +]; + +export const JP_MILITARY_FACILITIES: JpFacility[] = [ + { + id: 'jp-mil-sasebo', + name: '사세보(佐世保) 해군기지', + subType: 'naval', + lat: 33.16, + lng: 129.72, + operator: '미 해군 / 해상자위대', + description: '미 7함대 상륙전단 모항, 한국 최근접 미군기지', + }, + { + id: 'jp-mil-maizuru', + name: '마이즈루(舞鶴) 해군기지', + subType: 'naval', + lat: 35.47, + lng: 135.38, + operator: '해상자위대', + description: '동해 방면 주력기지, 호위함대 사령부', + }, + { + id: 'jp-mil-yokosuka', + name: '요코스카(横須賀) 해군기지', + subType: 'naval', + lat: 35.29, + lng: 139.67, + operator: '미 해군 / 해상자위대', + description: '미 7함대 사령부, 항모 로널드 레이건 모항', + }, + { + id: 'jp-mil-iwakuni', + name: '이와쿠니(岩国) 공군기지', + subType: 'airbase', + lat: 34.15, + lng: 132.24, + operator: '미 해병대 / 항공자위대', + description: 'F/A-18 및 F-35B 배치, 야마구치현', + }, + { + id: 'jp-mil-kadena', + name: '가데나(嘉手納) 공군기지', + subType: 'airbase', + lat: 26.36, + lng: 127.77, + operator: '미 공군', + description: 'F-15C/D, KC-135 배치, 아시아 최대 미 공군기지', + }, + { + id: 'jp-mil-ashiya', + name: '아시야(芦屋) 항공기지', + subType: 'airbase', + lat: 33.88, + lng: 130.66, + operator: '항공자위대', + description: '대한해협 인접, 후쿠오카현', + }, + { + id: 'jp-mil-naha', + name: '나하(那覇) 항공기지', + subType: 'airbase', + lat: 26.21, + lng: 127.65, + operator: '항공자위대', + description: 'F-15 배치, 남서항공방면대 사령부', + }, +];