feat(layer): 해외시설 하위 중국·일본 발전소/군사시설 레이어 추가

- cnFacilities.ts: 중국 핵·화력발전소 7개, 군사시설 7개 데이터
- jpFacilities.ts: 일본 핵·화력발전소 8개, 군사시설 7개 데이터
- CnFacilityLayer / JpFacilityLayer: 마커+팝업 레이어 컴포넌트
- LayerPanel: OverseasItem에 children 계층 지원 추가
- App.tsx: cnPower/cnMilitary/jpPower/jpMilitary 레이어 상태 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nan Kyung Lee 2026-03-21 18:17:34 +09:00
부모 e18a1a4932
커밋 444b7a4a8d
7개의 변경된 파일505개의 추가작업 그리고 10개의 파일을 삭제

파일 보기

@ -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}

파일 보기

@ -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 && (
<div className="layer-tree-children">
{overseasItems.map(item => (
<LayerTreeItem
key={item.key}
layerKey={item.key}
label={item.label}
color={item.color}
active={layers[item.key] ?? false}
onToggle={() => onToggle(item.key)}
/>
<div key={item.key}>
<LayerTreeItem
layerKey={item.key}
label={item.label}
color={item.color}
active={layers[item.key] ?? false}
expandable={!!item.children?.length}
isExpanded={expanded.has(`overseas-${item.key}`)}
onToggle={() => onToggle(item.key)}
onExpand={() => toggleExpand(`overseas-${item.key}`)}
/>
{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>
)}
</div>
))}
</div>
)}

파일 보기

@ -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<CnFacility['subType'], { color: string; icon: string; label: string }> = {
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<CnFacility | null>(null);
const facilities = type === 'power' ? CN_POWER_PLANTS : CN_MILITARY_FACILITIES;
return (
<>
{facilities.map(f => {
const meta = SUBTYPE_META[f.subType];
return (
<Marker
key={f.id}
longitude={f.lng}
latitude={f.lat}
anchor="center"
onClick={e => { e.originalEvent.stopPropagation(); setPopup(f); }}
>
<div
title={f.name}
style={{
width: 18,
height: 18,
borderRadius: '50%',
background: meta.color,
border: '2px solid rgba(255,255,255,0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 9,
color: '#fff',
cursor: 'pointer',
boxShadow: `0 0 6px ${meta.color}88`,
}}
>
{meta.icon}
</div>
</Marker>
);
})}
{popup && (
<Popup
longitude={popup.lng}
latitude={popup.lat}
anchor="bottom"
onClose={() => setPopup(null)}
closeOnClick={false}
maxWidth="220px"
>
<div style={{ fontSize: 11, lineHeight: 1.6, padding: '2px 4px' }}>
<div style={{ fontWeight: 700, marginBottom: 4 }}>{popup.name}</div>
<div style={{ color: SUBTYPE_META[popup.subType].color, marginBottom: 4 }}>
{SUBTYPE_META[popup.subType].label}
</div>
{popup.operator && (
<div><span style={{ opacity: 0.6 }}>:</span> {popup.operator}</div>
)}
{popup.description && (
<div style={{ marginTop: 4, opacity: 0.85 }}>{popup.description}</div>
)}
</div>
</Popup>
)}
</>
);
}

파일 보기

@ -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<JpFacility['subType'], { color: string; icon: string; label: string }> = {
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<JpFacility | null>(null);
const facilities = type === 'power' ? JP_POWER_PLANTS : JP_MILITARY_FACILITIES;
return (
<>
{facilities.map(f => {
const meta = SUBTYPE_META[f.subType];
return (
<Marker
key={f.id}
longitude={f.lng}
latitude={f.lat}
anchor="center"
onClick={e => { e.originalEvent.stopPropagation(); setPopup(f); }}
>
<div
title={f.name}
style={{
width: 18,
height: 18,
borderRadius: '50%',
background: meta.color,
border: '2px solid rgba(255,255,255,0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 9,
color: '#fff',
cursor: 'pointer',
boxShadow: `0 0 6px ${meta.color}88`,
}}
>
{meta.icon}
</div>
</Marker>
);
})}
{popup && (
<Popup
longitude={popup.lng}
latitude={popup.lat}
anchor="bottom"
onClose={() => setPopup(null)}
closeOnClick={false}
maxWidth="220px"
>
<div style={{ fontSize: 11, lineHeight: 1.6, padding: '2px 4px' }}>
<div style={{ fontWeight: 700, marginBottom: 4 }}>{popup.name}</div>
<div style={{ color: SUBTYPE_META[popup.subType].color, marginBottom: 4 }}>
{SUBTYPE_META[popup.subType].label}
</div>
{popup.operator && (
<div><span style={{ opacity: 0.6 }}>:</span> {popup.operator}</div>
)}
{popup.description && (
<div style={{ marginTop: 4, opacity: 0.85 }}>{popup.description}</div>
)}
</div>
</Popup>
)}
</>
);
}

파일 보기

@ -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 && <HazardFacilityLayer type="shipyard" />}
{layers.industryWastewater && <HazardFacilityLayer type="wastewater" />}
{layers.industryHeavy && <HazardFacilityLayer type="heavyIndustry" />}
{layers.cnPower && <CnFacilityLayer type="power" />}
{layers.cnMilitary && <CnFacilityLayer type="military" />}
{layers.jpPower && <JpFacilityLayer type="power" />}
{layers.jpMilitary && <JpFacilityLayer type="military" />}
{layers.airports && <KoreaAirportLayer />}
{layers.coastGuard && <CoastGuardLayer />}
{layers.navWarning && <NavWarningLayer />}

파일 보기

@ -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: '동중국해 주력 함대 기지',
},
];

파일 보기

@ -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 배치, 남서항공방면대 사령부',
},
];