React 19 + TypeScript + Vite + MapLibre 기반 해양 모니터링 대시보드. 선박 AIS, 항공기, CCTV, 위성, 해양 인프라 등 다중 레이어 지원. ESLint React Compiler 규칙 조정 및 lint 에러 수정 포함.
124 lines
4.8 KiB
TypeScript
124 lines
4.8 KiB
TypeScript
import { useState } from 'react';
|
|
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>
|
|
);
|
|
}
|
|
|
|
// caution (해경 등)
|
|
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);
|
|
|
|
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', display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
filter: `drop-shadow(0 0 4px ${color}88)`,
|
|
}}>
|
|
<WarningIcon level={w.level} org={w.org} size={size} />
|
|
<div style={{
|
|
fontSize: 5, color, marginTop: 0,
|
|
textShadow: `0 0 3px ${color}, 0 0 2px #000`,
|
|
whiteSpace: 'nowrap', fontWeight: 700, letterSpacing: 0.3,
|
|
}}>
|
|
{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 style={{ fontFamily: 'monospace', fontSize: 12, minWidth: 240 }}>
|
|
<div style={{
|
|
background: ORG_COLOR[selected.org], color: '#fff',
|
|
padding: '4px 8px', borderRadius: '4px 4px 0 0',
|
|
margin: '-10px -10px 8px -10px',
|
|
fontWeight: 700, fontSize: 12,
|
|
display: 'flex', alignItems: 'center', gap: 6,
|
|
}}>
|
|
{selected.title}
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 4, marginBottom: 6, flexWrap: 'wrap' }}>
|
|
<span style={{
|
|
background: LEVEL_COLOR[selected.level], color: '#fff',
|
|
padding: '1px 6px', borderRadius: 3, fontSize: 10, fontWeight: 700,
|
|
}}>{NW_LEVEL_LABEL[selected.level]}</span>
|
|
<span style={{
|
|
background: ORG_COLOR[selected.org] + '33', color: ORG_COLOR[selected.org],
|
|
padding: '1px 6px', borderRadius: 3, fontSize: 10, fontWeight: 700,
|
|
border: `1px solid ${ORG_COLOR[selected.org]}44`,
|
|
}}>{NW_ORG_LABEL[selected.org]}</span>
|
|
<span style={{
|
|
background: '#1a1a2e', color: '#888',
|
|
padding: '1px 6px', borderRadius: 3, fontSize: 10,
|
|
}}>{selected.area}</span>
|
|
</div>
|
|
<div style={{ fontSize: 11, color: '#ccc', marginBottom: 6, lineHeight: 1.4 }}>
|
|
{selected.description}
|
|
</div>
|
|
<div style={{ fontSize: 9, color: '#666', display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<div>사용고도: {selected.altitude}</div>
|
|
<div>{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E</div>
|
|
<div>출처: {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',
|
|
}}
|
|
>KHOA 항행경보 상황판 바로가기</a>
|
|
</div>
|
|
</Popup>
|
|
)}
|
|
</>
|
|
);
|
|
}
|