- 국적 분류 필터 추가 (한국/중국/북한/일본/미분류) - S&P Global / MarineTraffic 탭 디자인 개선 - CCTV 백엔드 프록시 연결 (CctvProxyController) - 풍력단지 레이어 (8개소 해상풍력) - 항구 레이어 (한국/중국/일본/북한/대만 46개) - 공항 확장 (중국 20, 일본 18, 북한 5, 대만 9개 추가) - 군사시설 레이어 (중국/일본/북한/대만 38개소) - 정부기관 레이어 (중국/일본 32개소) - 북한 발사/포병진지 레이어 (19개소) - 북한 미사일 낙하 시각화 (2026년 4건, 궤적 라인, 인근 선박 감지) - 항행정보/팝업 공통 스타일 정리 - 선박 현황 정렬 스타일 개선 - 레이어 패널 폰트 축소 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
131 lines
4.9 KiB
TypeScript
131 lines
4.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Marker, Popup } from 'react-map-gl/maplibre';
|
|
import { NAV_WARNINGS, NW_LEVEL_LABEL, NW_ORG_LABEL } from '../../services/navWarning';
|
|
import type { NavWarning, NavWarningLevel, TrainingOrg } from '../../services/navWarning';
|
|
|
|
const LEVEL_COLOR: Record<NavWarningLevel, string> = {
|
|
danger: '#ef4444',
|
|
caution: '#eab308',
|
|
info: '#3b82f6',
|
|
};
|
|
|
|
const ORG_COLOR: Record<TrainingOrg, string> = {
|
|
'해군': '#8b5cf6',
|
|
'해병대': '#22c55e',
|
|
'공군': '#f97316',
|
|
'육군': '#ef4444',
|
|
'해경': '#3b82f6',
|
|
'국과연': '#eab308',
|
|
};
|
|
|
|
function WarningIcon({ level, org, size }: { level: NavWarningLevel; org: TrainingOrg; size: number }) {
|
|
const color = ORG_COLOR[org];
|
|
|
|
if (level === 'danger') {
|
|
return (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 3 L22 20 L2 20 Z" fill="rgba(0,0,0,0.5)" stroke={color} strokeWidth="1.5" />
|
|
<line x1="12" y1="9" x2="12" y2="14" stroke={color} strokeWidth="2" strokeLinecap="round" />
|
|
<circle cx="12" cy="17" r="1" fill={color} />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
<path d="M12 2 L22 12 L12 22 L2 12 Z" fill="rgba(0,0,0,0.5)" stroke={color} strokeWidth="1.2" />
|
|
<line x1="12" y1="8" x2="12" y2="13" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
|
|
<circle cx="12" cy="16" r="1" fill={color} />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function NavWarningLayer() {
|
|
const [selected, setSelected] = useState<NavWarning | null>(null);
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
{NAV_WARNINGS.map(w => {
|
|
const color = ORG_COLOR[w.org];
|
|
const size = w.level === 'danger' ? 16 : 14;
|
|
return (
|
|
<Marker key={w.id} longitude={w.lng} latitude={w.lat} anchor="center"
|
|
onClick={(e) => { e.originalEvent.stopPropagation(); setSelected(w); }}>
|
|
<div style={{
|
|
cursor: 'pointer',
|
|
filter: `drop-shadow(0 0 4px ${color}88)`,
|
|
}} className="flex flex-col items-center">
|
|
<WarningIcon level={w.level} org={w.org} size={size} />
|
|
<div style={{
|
|
fontSize: 5, color,
|
|
textShadow: `0 0 3px ${color}, 0 0 2px #000`,
|
|
}} className="whitespace-nowrap font-bold tracking-wide">
|
|
{w.id}
|
|
</div>
|
|
</div>
|
|
</Marker>
|
|
);
|
|
})}
|
|
|
|
{selected && (
|
|
<Popup longitude={selected.lng} latitude={selected.lat}
|
|
onClose={() => setSelected(null)} closeOnClick={false}
|
|
anchor="bottom" maxWidth="320px" className="gl-popup">
|
|
<div className="font-mono" style={{ minWidth: 240, fontSize: 12 }}>
|
|
<div style={{
|
|
background: ORG_COLOR[selected.org],
|
|
color: '#fff',
|
|
padding: '4px 8px',
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
margin: '-10px -10px 0',
|
|
borderRadius: '5px 5px 0 0',
|
|
}}>
|
|
{selected.title}
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 10, marginBottom: 6 }}>
|
|
<span style={{
|
|
background: LEVEL_COLOR[selected.level],
|
|
color: '#fff',
|
|
padding: '1px 6px', fontSize: 10, borderRadius: 3, fontWeight: 700,
|
|
}}>
|
|
{NW_LEVEL_LABEL[selected.level]}
|
|
</span>
|
|
<span style={{
|
|
background: ORG_COLOR[selected.org] + '33',
|
|
color: ORG_COLOR[selected.org],
|
|
border: `1px solid ${ORG_COLOR[selected.org]}44`,
|
|
padding: '1px 6px', fontSize: 10, borderRadius: 3, fontWeight: 700,
|
|
}}>
|
|
{NW_ORG_LABEL[selected.org]}
|
|
</span>
|
|
<span style={{
|
|
background: 'var(--kcg-card)', color: 'var(--kcg-muted)',
|
|
padding: '1px 6px', fontSize: 10, borderRadius: 3,
|
|
}}>
|
|
{selected.area}
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: 11, color: '#ccc', lineHeight: 1.4, marginBottom: 6 }}>
|
|
{selected.description}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, fontSize: 9, color: '#666' }}>
|
|
<div>{t('navWarning.altitude')}: {selected.altitude}</div>
|
|
<div>{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E</div>
|
|
<div>{t('navWarning.source')}: {selected.source}</div>
|
|
</div>
|
|
<a
|
|
href="https://www.khoa.go.kr/nwb/mainPage.do?lang=ko"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ display: 'block', marginTop: 6, fontSize: 10, color: '#3b82f6', textDecoration: 'underline' }}
|
|
>{t('navWarning.khoaLink')}</a>
|
|
</div>
|
|
</Popup>
|
|
)}
|
|
</>
|
|
);
|
|
}
|