import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'; import { useTrackPlaybackStore, TRACK_PLAYBACK_SPEED_OPTIONS } from '../../features/trackReplay/stores/trackPlaybackStore'; import { useTrackQueryStore } from '../../features/trackReplay/stores/trackQueryStore'; function formatDateTime(ms: number): string { if (!Number.isFinite(ms) || ms <= 0) return '--'; const date = new Date(ms); const pad = (value: number) => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad( date.getMinutes(), )}:${pad(date.getSeconds())}`; } export function GlobalTrackReplayPanel() { const PANEL_WIDTH = 420; const PANEL_MARGIN = 12; const PANEL_DEFAULT_TOP = 16; const PANEL_RIGHT_RESERVED = 520; const panelRef = useRef(null); const dragRef = useRef<{ pointerId: number; startX: number; startY: number; originX: number; originY: number } | null>( null, ); const [isDragging, setIsDragging] = useState(false); const clampPosition = useCallback( (x: number, y: number) => { if (typeof window === 'undefined') return { x, y }; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const panelHeight = panelRef.current?.offsetHeight ?? 360; return { x: Math.min(Math.max(PANEL_MARGIN, x), Math.max(PANEL_MARGIN, viewportWidth - PANEL_WIDTH - PANEL_MARGIN)), y: Math.min(Math.max(PANEL_MARGIN, y), Math.max(PANEL_MARGIN, viewportHeight - panelHeight - PANEL_MARGIN)), }; }, [PANEL_MARGIN, PANEL_WIDTH], ); const [position, setPosition] = useState(() => { if (typeof window === 'undefined') { return { x: PANEL_MARGIN, y: PANEL_DEFAULT_TOP }; } return { x: Math.max(PANEL_MARGIN, window.innerWidth - PANEL_WIDTH - PANEL_RIGHT_RESERVED), y: PANEL_DEFAULT_TOP, }; }); const tracks = useTrackQueryStore((state) => state.tracks); const isLoading = useTrackQueryStore((state) => state.isLoading); const error = useTrackQueryStore((state) => state.error); const showPoints = useTrackQueryStore((state) => state.showPoints); const showVirtualShip = useTrackQueryStore((state) => state.showVirtualShip); const showLabels = useTrackQueryStore((state) => state.showLabels); const showTrail = useTrackQueryStore((state) => state.showTrail); const hideLiveShips = useTrackQueryStore((state) => state.hideLiveShips); const setShowPoints = useTrackQueryStore((state) => state.setShowPoints); const setShowVirtualShip = useTrackQueryStore((state) => state.setShowVirtualShip); const setShowLabels = useTrackQueryStore((state) => state.setShowLabels); const setShowTrail = useTrackQueryStore((state) => state.setShowTrail); const setHideLiveShips = useTrackQueryStore((state) => state.setHideLiveShips); const closeTrackQuery = useTrackQueryStore((state) => state.closeQuery); const isPlaying = useTrackPlaybackStore((state) => state.isPlaying); const currentTime = useTrackPlaybackStore((state) => state.currentTime); const startTime = useTrackPlaybackStore((state) => state.startTime); const endTime = useTrackPlaybackStore((state) => state.endTime); const playbackSpeed = useTrackPlaybackStore((state) => state.playbackSpeed); const loop = useTrackPlaybackStore((state) => state.loop); const play = useTrackPlaybackStore((state) => state.play); const pause = useTrackPlaybackStore((state) => state.pause); const stop = useTrackPlaybackStore((state) => state.stop); const setCurrentTime = useTrackPlaybackStore((state) => state.setCurrentTime); const setPlaybackSpeed = useTrackPlaybackStore((state) => state.setPlaybackSpeed); const toggleLoop = useTrackPlaybackStore((state) => state.toggleLoop); const progress = useMemo(() => { if (endTime <= startTime) return 0; return ((currentTime - startTime) / (endTime - startTime)) * 100; }, [startTime, endTime, currentTime]); const isVisible = isLoading || tracks.length > 0 || !!error; useEffect(() => { if (!isVisible) return; if (typeof window === 'undefined') return; const onResize = () => { setPosition((prev) => clampPosition(prev.x, prev.y)); }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, [clampPosition, isVisible]); useEffect(() => { if (!isVisible) return; const onPointerMove = (event: PointerEvent) => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; setPosition(() => { const nextX = drag.originX + (event.clientX - drag.startX); const nextY = drag.originY + (event.clientY - drag.startY); return clampPosition(nextX, nextY); }); }; const stopDrag = (event: PointerEvent) => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; dragRef.current = null; setIsDragging(false); }; window.addEventListener('pointermove', onPointerMove); window.addEventListener('pointerup', stopDrag); window.addEventListener('pointercancel', stopDrag); return () => { window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', stopDrag); window.removeEventListener('pointercancel', stopDrag); }; }, [clampPosition, isVisible]); const handleHeaderPointerDown = useCallback( (event: ReactPointerEvent) => { if (event.button !== 0) return; dragRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, originX: position.x, originY: position.y, }; setIsDragging(true); try { event.currentTarget.setPointerCapture(event.pointerId); } catch { // ignore } }, [position.x, position.y], ); if (!isVisible) return null; return (
Track Replay
{error ? (
{error}
) : null} {isLoading ?
항적 조회 중...
: null}
선박 {tracks.length}척 · {formatDateTime(startTime)} ~ {formatDateTime(endTime)}
setCurrentTime(Number(event.target.value))} style={{ width: '100%' }} disabled={tracks.length === 0 || endTime <= startTime} />
{formatDateTime(currentTime)} {Math.max(0, Math.min(100, progress)).toFixed(1)}%
); }