import { useState, useCallback, useRef, useMemo } from 'react' import { useI18n } from '../hooks/useI18n.ts' import MapContainer from '../components/map/MapContainer.tsx' import Sidebar from '../components/layout/Sidebar.tsx' import RawDataPanel from '../components/RawDataPanel.tsx' import type maplibregl from 'maplibre-gl' import type { Layer } from '@deck.gl/core' import { useRecentPositions, useRecentPositionsLayer, usePositionStore, VesselPopup, PositionFilterPanel, } from '../features/recent-positions' import { useVesselTracksLayer, TrackQueryPanel, TrackInfoPanel, } from '../features/vessel-tracks' import { useTrackStore } from '../features/vessel-tracks/stores/trackStore' import { useReplayLayer, useReplayStore, useMergedTrackStore, ReplaySetupPanel, ReplayControlPanel, } from '../features/viewport-replay' type ApiMode = 'positions' | 'vessel' | 'replay' /** 최근위치 — 선택된 선박의 원본 데이터 */ function usePositionRawData() { const selectedMmsi = usePositionStore((s) => s.selectedMmsi) const positions = usePositionStore((s) => s.positions) return useMemo(() => { if (!selectedMmsi) return null const vessel = positions.get(selectedMmsi) return vessel ?? null }, [selectedMmsi, positions]) } /** 항적 — 조회 결과 요약 데이터 */ function useTrackRawData() { const tracks = useTrackStore((s) => s.tracks) return useMemo(() => { if (tracks.length === 0) return null return tracks.map((t) => ({ vesselId: t.vesselId, shipName: t.shipName, shipKindCode: t.shipKindCode, nationalCode: t.nationalCode, pointCount: t.geometry.length, totalDistance: t.totalDistance, avgSpeed: t.avgSpeed, maxSpeed: t.maxSpeed, timeRange: t.timestampsMs.length >= 2 ? [new Date(t.timestampsMs[0]).toISOString(), new Date(t.timestampsMs[t.timestampsMs.length - 1]).toISOString()] : null, })) }, [tracks]) } /** 리플레이 — 청크 병합 결과 데이터 */ function useReplayRawData() { const vesselChunks = useMergedTrackStore((s) => s.vesselChunks) const queryCompleted = useReplayStore((s) => s.queryCompleted) const receivedChunks = useReplayStore((s) => s.receivedChunks) return useMemo(() => { if (vesselChunks.size === 0) return null const vessels: Record[] = [] for (const [mmsi, data] of vesselChunks) { vessels.push({ vesselId: mmsi, shipName: data.shipName, shipKindCode: data.shipKindCode, nationalCode: data.nationalCode, chunkCount: data.chunks.length, totalPoints: data.chunks.reduce((sum, c) => sum + (c.geometry?.length ?? 0), 0), }) } return { queryCompleted, receivedChunks, vesselCount: vessels.length, vessels, } }, [vesselChunks, queryCompleted, receivedChunks]) } export default function ApiExplorer() { const { t } = useI18n() const [mode, setMode] = useState('positions') const mapRef = useRef(null) const [zoom, setZoom] = useState(7) const [trackMmsi, setTrackMmsi] = useState() const [replayLayers, setReplayLayers] = useState([]) const selectVessel = usePositionStore((s) => s.selectVessel) // 최근 위치 30초 폴링 (positions 모드일 때만) useRecentPositions(10, mode === 'positions') // Deck.gl 레이어 — positions const positionLayers = useRecentPositionsLayer({ zoom, onVesselClick: (mmsi) => selectVessel(mmsi), }) // Deck.gl 레이어 — vessel tracks const trackLayers = useVesselTracksLayer({ zoom }) // Deck.gl 레이어 — replay (콜백으로 업데이트) const handleReplayLayersUpdate = useCallback((layers: Layer[]) => { setReplayLayers(layers) }, []) useReplayLayer({ zoom, onLayersUpdate: handleReplayLayersUpdate, }) const handleMapReady = useCallback((m: maplibregl.Map) => { mapRef.current = m setZoom(m.getZoom()) m.on('zoom', () => setZoom(m.getZoom())) }, []) // 최근위치 → 항적조회 전환 const handleTrackQuery = useCallback((mmsi: string) => { setTrackMmsi(mmsi) setMode('vessel') selectVessel(null) }, [selectVessel]) // Raw Data 훅 const positionData = usePositionRawData() const trackData = useTrackRawData() const replayData = useReplayRawData() // 모드별 Deck.gl 레이어 const deckLayers: Layer[] = mode === 'positions' ? positionLayers : mode === 'vessel' ? trackLayers : mode === 'replay' ? replayLayers : [] return (
{/* Sidebar */}

{t('explorer.title')}

{/* 모드 선택 */}
{([ { value: 'positions' as const, label: t('explorer.recentPositions') }, { value: 'vessel' as const, label: t('explorer.vesselTracks') }, { value: 'replay' as const, label: '뷰포트 리플레이' }, ] as const).map(opt => ( ))}
{/* 모드별 사이드바 */} {mode === 'positions' && (
선종 필터
)} {mode === 'vessel' && (
항적 조회
)} {mode === 'replay' && (
뷰포트 리플레이
)} {/* Response Data — Swagger Response 스타일 */} {mode === 'positions' && ( )} {mode === 'vessel' && ( )} {mode === 'replay' && ( )}
{/* Map area */}
{/* 모드 표시 */}
{mode === 'positions' && t('explorer.recentPositions')} {mode === 'vessel' && t('explorer.vesselTracks')} {mode === 'replay' && '뷰포트 리플레이'}
{/* 모드별 오버레이 */} {mode === 'positions' && } {mode === 'vessel' && } {mode === 'replay' && }
) }