signal-batch/frontend/src/features/recent-positions/components/VesselPopup.tsx
htlee ed0f3056b1 feat: Ship-GIS 기능 이관 — 최근위치/선박항적/뷰포트 리플레이
dark(ship-gis) 프로젝트의 맵 기반 3대 기능을 API 탐색기에 이관.
Feature 폴더 모듈화 구조로 타 프로젝트 재활용 가능하게 구성.

Phase 1: vessel-map 공유 모듈 (Deck.gl 9 + Zustand 5 + STOMP)
Phase 2: 최근 위치 (30초 폴링 + IconLayer + 선종 필터 + 팝업)
Phase 3: 선박 항적 (MMSI 조회 + PathLayer + 타임라인 보간)
Phase 4: 뷰포트 리플레이 (STOMP WebSocket 청크 + TripsLayer 애니메이션)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:19:21 +09:00

88 lines
3.0 KiB
TypeScript

import { usePositionStore } from '../stores/positionStore'
import { getShipKindLabel, getShipKindColor } from '../../vessel-map/utils/shipKindColors'
interface VesselPopupProps {
onTrackQuery?: (mmsi: string) => void
}
export default function VesselPopup({ onTrackQuery }: VesselPopupProps) {
const selectedMmsi = usePositionStore((s) => s.selectedMmsi)
const positions = usePositionStore((s) => s.positions)
const selectVessel = usePositionStore((s) => s.selectVessel)
if (!selectedMmsi) return null
const vessel = positions.get(selectedMmsi)
if (!vessel) return null
const kindLabel = getShipKindLabel(vessel.shipKindCode)
const kindColor = getShipKindColor(vessel.shipKindCode)
return (
<div className="absolute bottom-4 left-4 z-10 w-72 rounded-lg border border-border bg-surface p-3 shadow-lg">
{/* 헤더 */}
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className="inline-block h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: kindColor }}
/>
<span className="text-sm font-semibold text-foreground">
{vessel.shipNm || vessel.mmsi}
</span>
</div>
<button
onClick={() => selectVessel(null)}
className="text-muted hover:text-foreground"
aria-label="Close"
>
&times;
</button>
</div>
{/* 선박 정보 그리드 */}
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
<InfoRow label="MMSI" value={vessel.mmsi} />
{vessel.imo ? <InfoRow label="IMO" value={String(vessel.imo)} /> : null}
<InfoRow label="선종" value={kindLabel} />
<InfoRow label="국적" value={vessel.nationalCode || '-'} />
<InfoRow label="SOG" value={`${vessel.sog.toFixed(1)} kn`} />
<InfoRow label="COG" value={`${vessel.cog.toFixed(1)}\u00B0`} />
<InfoRow label="위치" value={`${vessel.lat.toFixed(4)}, ${vessel.lon.toFixed(4)}`} />
<InfoRow label="업데이트" value={vessel.lastUpdate} />
</div>
{/* 선박 사진 */}
{vessel.shipImagePath && (
<div className="mt-2">
<img
src={vessel.shipImagePath}
alt={vessel.shipNm || vessel.mmsi}
className="h-20 w-full rounded object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
</div>
)}
{/* 항적 조회 버튼 */}
{onTrackQuery && (
<button
onClick={() => onTrackQuery(vessel.mmsi)}
className="mt-2 w-full rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-white hover:bg-primary/90 transition"
>
</button>
)}
</div>
)
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<>
<span className="text-muted">{label}</span>
<span className="text-foreground">{value}</span>
</>
)
}