kcg-monitoring/frontend/src/components/PiracyLayer.tsx
htlee 2534faa488 feat: 프론트엔드 모노레포 이관 + signal-batch 연동 + Tailwind/i18n/테마 전환
- 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>
2026-03-17 13:54:41 +09:00

96 lines
4.2 KiB
TypeScript
Raw Blame 히스토리

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)}
</>
);
}