- 어선 분류 개선: AIS Ship Type 30 + category fallback + 선박명 패턴 - 어구/어망 카테고리 신설: 선박명_숫자_ / 선박명% 패턴으로 분류 - 중국어선 조업분석: GC-KCG-2026-001 + CSSA 보고서 기반 (안강망 추가) - 중국어선 선단 탐지: 본선-부속선 쌍, 운반선 환적, 선망 선단 - 어구/어망 → 모선 연결선 시각화 - 어구 SVG 아이콘 5종 (트롤/자망/안강망/선망/기본) - 이란 주변국 시설 레이어 (MEFacilityLayer 35개소) - 사우스파르스 가스전 피격 + 카타르 라스라판 보복 공격 반영 - 한국 해군부대 10개소 추가 - 레이어 재구성: 선박(최상위) → 항공망(항공기+위성) → 해양안전 → 국가기관망 - 어선 국적별 하위 분류 (선박 분류 내 어선 펼치기) - 오른쪽 패널 접기/펼치기 (한국현황, 중국현황, 조업분석, OSINT) - 항공망 기본 접힘 처리 - 센서차트 기본 숨김 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
import { memo, useState } from 'react';
|
|
import { Marker, Popup } from 'react-map-gl/maplibre';
|
|
import { ME_FACILITIES, ME_FACILITY_TYPE_META } from '../../data/middleEastFacilities';
|
|
import type { MEFacility } from '../../data/middleEastFacilities';
|
|
|
|
export const MEFacilityLayer = memo(function MEFacilityLayer() {
|
|
const [selected, setSelected] = useState<MEFacility | null>(null);
|
|
|
|
return (
|
|
<>
|
|
{ME_FACILITIES.map(f => {
|
|
const meta = ME_FACILITY_TYPE_META[f.type];
|
|
return (
|
|
<Marker key={f.id} longitude={f.lng} latitude={f.lat} anchor="center"
|
|
onClick={(e) => { e.originalEvent.stopPropagation(); setSelected(f); }}>
|
|
<div
|
|
className="flex flex-col items-center cursor-pointer"
|
|
style={{ filter: `drop-shadow(0 0 3px ${meta.color}88)` }}
|
|
>
|
|
<div style={{
|
|
width: 16, height: 16, borderRadius: 3,
|
|
background: 'rgba(0,0,0,0.7)', border: `1.5px solid ${meta.color}`,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: 9,
|
|
}}>
|
|
{meta.icon}
|
|
</div>
|
|
<div style={{
|
|
fontSize: 5, color: meta.color, marginTop: 1,
|
|
textShadow: '0 0 3px #000, 0 0 3px #000',
|
|
whiteSpace: 'nowrap', fontWeight: 700,
|
|
}}>
|
|
{f.nameKo.length > 12 ? f.nameKo.slice(0, 12) + '..' : f.nameKo}
|
|
</div>
|
|
</div>
|
|
</Marker>
|
|
);
|
|
})}
|
|
|
|
{selected && (() => {
|
|
const meta = ME_FACILITY_TYPE_META[selected.type];
|
|
return (
|
|
<Popup longitude={selected.lng} latitude={selected.lat}
|
|
onClose={() => setSelected(null)} closeOnClick={false}
|
|
anchor="bottom" maxWidth="320px" className="gl-popup">
|
|
<div className="popup-body-sm" style={{ minWidth: 240 }}>
|
|
<div className="popup-header" style={{ background: meta.color, color: '#fff', gap: 6, padding: '6px 10px' }}>
|
|
<span style={{ fontSize: 16 }}>{selected.flag}</span>
|
|
<strong style={{ fontSize: 13 }}>{meta.icon} {selected.nameKo}</strong>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 4, marginBottom: 6, flexWrap: 'wrap' }}>
|
|
<span style={{
|
|
background: meta.color, color: '#fff',
|
|
padding: '2px 8px', borderRadius: 3, fontSize: 10, fontWeight: 700,
|
|
}}>
|
|
{meta.label}
|
|
</span>
|
|
<span style={{
|
|
background: '#333', color: '#ccc',
|
|
padding: '2px 8px', borderRadius: 3, fontSize: 10,
|
|
}}>
|
|
{selected.country}
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: 11, color: '#ccc', lineHeight: 1.4, marginBottom: 6 }}>
|
|
{selected.description}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: 'var(--kcg-muted)', marginBottom: 4 }}>
|
|
{selected.name}
|
|
</div>
|
|
<div style={{ fontSize: 10, color: '#999' }}>
|
|
{selected.lat.toFixed(4)}°{selected.lat >= 0 ? 'N' : 'S'}, {selected.lng.toFixed(4)}°E
|
|
</div>
|
|
</div>
|
|
</Popup>
|
|
);
|
|
})()}
|
|
</>
|
|
);
|
|
});
|