- frontend/ 폴더로 프론트엔드 전체 이관 - signal-batch API 연동 (한국 선박 위치 데이터) - Tailwind CSS 4 + CSS 변수 테마 토큰 (dark/light) - i18next 다국어 (ko/en) 인프라 + 28개 컴포넌트 적용 - 레이어 패널 트리 구조 재설계 (카테고리별 온/오프, 범례) - Google OAuth 로그인 화면 + DEV LOGIN 우회 - 외부 API CORS 프록시 전환 (Airplanes.live, OpenSky, CelesTrak) - ShipLayer 이미지 탭 전환 (signal-batch / MarineTraffic) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
4.2 KiB
TypeScript
96 lines
4.2 KiB
TypeScript
import { useState } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { Marker, Popup } from 'react-map-gl/maplibre';
|
||
import { PIRACY_ZONES, PIRACY_LEVEL_COLOR, PIRACY_LEVEL_LABEL } from '../services/piracy';
|
||
import type { PiracyZone } from '../services/piracy';
|
||
|
||
function SkullIcon({ color, size }: { color: string; size: number }) {
|
||
return (
|
||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||
<ellipse cx="12" cy="10" rx="8" ry="9" fill="rgba(0,0,0,0.6)" stroke={color} strokeWidth="1.5" />
|
||
<ellipse cx="8.5" cy="9" rx="2" ry="2.2" fill={color} opacity="0.9" />
|
||
<ellipse cx="15.5" cy="9" rx="2" ry="2.2" fill={color} opacity="0.9" />
|
||
<path d="M11 13 L12 14.5 L13 13" stroke={color} strokeWidth="1" fill="none" />
|
||
<path d="M7 17 Q12 21 17 17" stroke={color} strokeWidth="1.2" fill="none" />
|
||
<line x1="4" y1="20" x2="20" y2="24" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
|
||
<line x1="20" y1="20" x2="4" y2="24" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
export function PiracyLayer() {
|
||
const [selected, setSelected] = useState<PiracyZone | null>(null);
|
||
const { t } = useTranslation();
|
||
|
||
return (
|
||
<>
|
||
{PIRACY_ZONES.map(zone => {
|
||
const color = PIRACY_LEVEL_COLOR[zone.level];
|
||
const size = zone.level === 'critical' ? 28 : zone.level === 'high' ? 24 : 20;
|
||
return (
|
||
<Marker key={zone.id} longitude={zone.lng} latitude={zone.lat} anchor="center"
|
||
onClick={(e) => { e.originalEvent.stopPropagation(); setSelected(zone); }}>
|
||
<div style={{
|
||
cursor: 'pointer',
|
||
filter: `drop-shadow(0 0 8px ${color}aa)`,
|
||
animation: zone.level === 'critical' ? 'pulse 2s ease-in-out infinite' : undefined,
|
||
}} className="flex flex-col items-center">
|
||
<SkullIcon color={color} size={size} />
|
||
<div style={{
|
||
fontSize: 7, color,
|
||
textShadow: `0 0 3px ${color}, 0 0 2px #000`,
|
||
}} className="mt-px whitespace-nowrap font-mono font-bold tracking-wide">
|
||
{PIRACY_LEVEL_LABEL[zone.level]}
|
||
</div>
|
||
</div>
|
||
</Marker>
|
||
);
|
||
})}
|
||
|
||
{selected && (
|
||
<Popup longitude={selected.lng} latitude={selected.lat}
|
||
onClose={() => setSelected(null)} closeOnClick={false}
|
||
anchor="bottom" maxWidth="340px" className="gl-popup">
|
||
<div className="min-w-[260px] font-mono text-xs">
|
||
<div style={{
|
||
background: PIRACY_LEVEL_COLOR[selected.level],
|
||
}} className="-mx-2.5 -mt-2.5 mb-2 flex items-center gap-1.5 rounded-t px-2.5 py-1.5 text-xs font-bold text-white">
|
||
<span className="text-sm">☠️</span>
|
||
{selected.nameKo}
|
||
</div>
|
||
|
||
<div className="mb-1.5 flex flex-wrap gap-1">
|
||
<span style={{
|
||
background: PIRACY_LEVEL_COLOR[selected.level],
|
||
}} className="rounded-sm px-1.5 py-px text-[10px] font-bold text-white">
|
||
{PIRACY_LEVEL_LABEL[selected.level]}
|
||
</span>
|
||
<span className="rounded-sm bg-kcg-card px-1.5 py-px text-[10px] text-kcg-muted">
|
||
{selected.name}
|
||
</span>
|
||
{selected.recentIncidents != null && (
|
||
<span style={{
|
||
color: PIRACY_LEVEL_COLOR[selected.level],
|
||
border: `1px solid ${PIRACY_LEVEL_COLOR[selected.level]}44`,
|
||
}} className="rounded-sm bg-kcg-card px-1.5 py-px text-[10px] font-bold">
|
||
{t('piracy.recentIncidents', { count: selected.recentIncidents })}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mb-1.5 text-[11px] leading-relaxed text-kcg-text-secondary">
|
||
{selected.description}
|
||
</div>
|
||
<div className="text-[10px] leading-snug text-[#999]">
|
||
{selected.detail}
|
||
</div>
|
||
<div className="mt-1.5 text-[9px] text-kcg-dim">
|
||
{selected.lat.toFixed(2)}°N, {selected.lng.toFixed(2)}°E
|
||
</div>
|
||
</div>
|
||
</Popup>
|
||
)}
|
||
</>
|
||
);
|
||
}
|