kcg-monitoring/frontend/src/components/korea/CoastGuardLayer.tsx
htlee 81cd094c56 fix(frontend): 컴포넌트 import 경로 수정 (vite build 실패 해결) (#42)
Co-authored-by: htlee <htlee@gcsc.co.kr>
Co-committed-by: htlee <htlee@gcsc.co.kr>
2026-03-18 08:21:42 +09:00

121 lines
4.6 KiB
TypeScript

import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Marker, Popup } from 'react-map-gl/maplibre';
import { COAST_GUARD_FACILITIES, CG_TYPE_LABEL } from '../../services/coastGuard';
import type { CoastGuardFacility, CoastGuardType } from '../../services/coastGuard';
const TYPE_COLOR: Record<CoastGuardType, string> = {
hq: '#ff6b6b',
regional: '#ffa94d',
station: '#4dabf7',
substation: '#69db7c',
vts: '#da77f2',
};
const TYPE_SIZE: Record<CoastGuardType, number> = {
hq: 24,
regional: 20,
station: 16,
substation: 13,
vts: 14,
};
/** 해경 로고 SVG — 작은 방패+앵커 심볼 */
function CoastGuardIcon({ type, size }: { type: CoastGuardType; size: number }) {
const color = TYPE_COLOR[type];
const isVts = type === 'vts';
if (isVts) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke={color} strokeWidth="1.2" />
<line x1="12" y1="18" x2="12" y2="10" stroke={color} strokeWidth="1.5" />
<circle cx="12" cy="9" r="2" fill="none" stroke={color} strokeWidth="1" />
<path d="M7 7 Q12 3 17 7" fill="none" stroke={color} strokeWidth="0.8" opacity="0.6" />
<path d="M9 5.5 Q12 3 15 5.5" fill="none" stroke={color} strokeWidth="0.8" opacity="0.4" />
</svg>
);
}
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M12 2 L20 6 L20 13 Q20 19 12 22 Q4 19 4 13 L4 6 Z"
fill="rgba(0,0,0,0.6)" stroke={color} strokeWidth="1.2" />
<circle cx="12" cy="9" r="2" fill="none" stroke="#fff" strokeWidth="1" />
<line x1="12" y1="11" x2="12" y2="18" stroke="#fff" strokeWidth="1" />
<path d="M8 16 Q12 20 16 16" fill="none" stroke="#fff" strokeWidth="1" />
<line x1="9" y1="13" x2="15" y2="13" stroke="#fff" strokeWidth="0.8" />
{(type === 'hq' || type === 'regional') && (
<circle cx="12" cy="9" r="1" fill={color} />
)}
</svg>
);
}
export function CoastGuardLayer() {
const [selected, setSelected] = useState<CoastGuardFacility | null>(null);
const { t } = useTranslation();
return (
<>
{COAST_GUARD_FACILITIES.map(f => {
const size = TYPE_SIZE[f.type];
return (
<Marker key={f.id} longitude={f.lng} latitude={f.lat} anchor="center"
onClick={(e) => { e.originalEvent.stopPropagation(); setSelected(f); }}>
<div style={{
cursor: 'pointer',
filter: `drop-shadow(0 0 3px ${TYPE_COLOR[f.type]}66)`,
}} className="flex flex-col items-center">
<CoastGuardIcon type={f.type} size={size} />
{(f.type === 'hq' || f.type === 'regional') && (
<div style={{
fontSize: 6,
textShadow: `0 0 3px ${TYPE_COLOR[f.type]}, 0 0 2px #000`,
}} className="mt-px whitespace-nowrap font-bold text-white">
{f.name.replace('해양경찰청', '').replace('지방', '').trim() || '본청'}
</div>
)}
{f.type === 'vts' && (
<div style={{
fontSize: 5,
textShadow: '0 0 3px #da77f2, 0 0 2px #000',
}} className="whitespace-nowrap font-bold tracking-wider text-[#da77f2]">
VTS
</div>
)}
</div>
</Marker>
);
})}
{selected && (
<Popup longitude={selected.lng} latitude={selected.lat}
onClose={() => setSelected(null)} closeOnClick={false}
anchor="bottom" maxWidth="280px" className="gl-popup">
<div className="min-w-[200px] font-mono text-xs">
<div style={{
background: TYPE_COLOR[selected.type],
}} className="-mx-2.5 -mt-2.5 mb-2 flex items-center gap-1.5 rounded-t px-2 py-1 text-[13px] font-bold text-black">
{selected.name}
</div>
<div className="mb-1.5 flex flex-wrap gap-1">
<span style={{
background: TYPE_COLOR[selected.type],
}} className="rounded-sm px-1.5 py-px text-[10px] font-bold text-black">
{CG_TYPE_LABEL[selected.type]}
</span>
<span className="rounded-sm bg-kcg-card px-1.5 py-px text-[10px] font-bold text-[#4dabf7]">
{t('coastGuard.agency')}
</span>
</div>
<div className="text-[9px] text-kcg-dim">
{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E
</div>
</div>
</Popup>
)}
</>
);
}