import { useEffect, useState } from 'react'; import { ToggleButton, Section } from '@wing/ui'; import type { AisTarget } from '../../entities/aisTarget/model/types'; import type { LegacyVesselIndex } from '../../entities/legacyVessel/lib'; import type { LegacyVesselDataset, LegacyVesselInfo } from '../../entities/legacyVessel/model/types'; import type { VesselTypeCode } from '../../entities/vessel/model/types'; import { VESSEL_TYPE_ORDER } from '../../entities/vessel/model/meta'; import type { ZonesGeoJson } from '../../entities/zone/api/useZones'; import type { AisPollingSnapshot } from '../../features/aisPolling/useAisTargetPolling'; import { Map3DSettingsToggles } from '../../features/map3dSettings/Map3DSettingsToggles'; import type { DerivedLegacyVessel, LegacyAlarm, LegacyAlarmKind } from '../../features/legacyDashboard/model/types'; import { LEGACY_ALARM_KIND_LABEL, LEGACY_ALARM_KINDS } from '../../features/legacyDashboard/model/types'; import { MapToggles } from '../../features/mapToggles/MapToggles'; import { TypeFilterGrid } from '../../features/typeFilter/TypeFilterGrid'; import { AisTargetList } from '../../widgets/aisTargetList/AisTargetList'; import { AlarmsPanel } from '../../widgets/alarms/AlarmsPanel'; import { RelationsPanel } from '../../widgets/relations/RelationsPanel'; import { SpeedProfilePanel } from '../../widgets/speed/SpeedProfilePanel'; import { VesselList } from '../../widgets/vesselList/VesselList'; import { fmtIsoFull } from '../../shared/lib/datetime'; import type { useDashboardState } from './useDashboardState'; import type { Bbox } from './useDashboardState'; const AIS_API_BASE = (import.meta.env.VITE_API_URL || '/snp-api').replace(/\/$/, ''); function fmtBbox(b: Bbox | null) { if (!b) return '-'; return `${b[0].toFixed(4)},${b[1].toFixed(4)},${b[2].toFixed(4)},${b[3].toFixed(4)}`; } interface DashboardSidebarProps { isOpen: boolean; onClose: () => void; state: ReturnType; legacyVesselsAll: DerivedLegacyVessel[]; legacyVesselsFiltered: DerivedLegacyVessel[]; legacyCounts: Record; selectedLegacyVessel: DerivedLegacyVessel | null; activeHighlightedMmsiSet: number[]; legacyHits: Map; filteredAlarms: LegacyAlarm[]; alarms: LegacyAlarm[]; alarmKindCounts: Record; speedPanelType: VesselTypeCode; onFleetContextMenu: (ownerKey: string, mmsis: number[]) => void; snapshot: AisPollingSnapshot; legacyError: string | null; legacyData: LegacyVesselDataset | null; targetsInScope: AisTarget[]; zonesError: string | null; zones: ZonesGeoJson | null; legacyIndex: LegacyVesselIndex | null; } export function DashboardSidebar({ isOpen, onClose, state, legacyVesselsAll, legacyVesselsFiltered, legacyCounts, selectedLegacyVessel, activeHighlightedMmsiSet, legacyHits, filteredAlarms, alarms, alarmKindCounts, speedPanelType, onFleetContextMenu, snapshot, legacyError, legacyData, targetsInScope, zonesError, zones, legacyIndex, }: DashboardSidebarProps) { const { showTargets, setShowTargets, showOthers, setShowOthers, typeEnabled, setTypeEnabled, overlays, setOverlays, projection, setProjection, isProjectionToggleDisabled, freeCamera, toggleFreeCamera, baseMap, setBaseMap, selectedMmsi, setSelectedMmsi, fleetRelationSortMode, setFleetRelationSortMode, hoveredFleetOwnerKey, hoveredFleetMmsiSet, setHoveredMmsiSet, setHoveredPairMmsiSet, setHoveredFleetOwnerKey, setHoveredFleetMmsiSet, alarmKindEnabled, setAlarmKindEnabled, adminMode, viewBbox, useViewportFilter, setUseViewportFilter, useApiBbox, setUseApiBbox, setApiBbox, settings, setSettings, setUniqueSorted, setSortedIfChanged, toggleHighlightedMmsi, } = state; const [isAlarmFilterOpen, setIsAlarmFilterOpen] = useState(false); useEffect(() => { if (!isOpen) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [isOpen, onClose]); return ( <> {isOpen && (
)}
{ setShowTargets((v) => { const next = !v; if (!next) setSelectedMmsi((m) => (legacyHits.has(m ?? -1) ? null : m)); return next; }); }} title="레거시(CN permit) 대상 선박 표시" > 대상 선박 setShowOthers((v) => !v)} title="대상 외 AIS 선박 표시"> 기타 AIS
{ if (selectedLegacyVessel && selectedLegacyVessel.shipCode === code && typeEnabled[code]) setSelectedMmsi(null); setTypeEnabled((prev) => ({ ...prev, [code]: !prev[code] })); }} onToggleAll={() => { const allOn = VESSEL_TYPE_ORDER.every((c) => typeEnabled[c]); const nextVal = !allOn; if (!nextVal && selectedLegacyVessel) setSelectedMmsi(null); setTypeEnabled({ PT: nextVal, 'PT-S': nextVal, GN: nextVal, OT: nextVal, PS: nextVal, FC: nextVal }); }} />
자유 시점 setBaseMap(baseMap === 'ocean' ? 'enhanced' : 'ocean')} title="Ocean 전용 지도 (해양 정보 극대화)" className="px-2 py-0.5 text-[9px]" > Ocean setProjection((p) => (p === 'globe' ? 'mercator' : 'globe'))} title={isProjectionToggleDisabled ? '3D 모드 준비 중...' : '3D 지구본 투영'} className={`px-2 py-0.5 text-[9px]${isProjectionToggleDisabled ? " opacity-40 cursor-not-allowed" : ""}`} > 3D
} > setOverlays((prev) => ({ ...prev, [k]: !prev[k] }))} />
선단 연관관계{' '} {selectedLegacyVessel ? `${selectedLegacyVessel.permitNo} 연관` : '(선박 클릭 시 표시)'} } actions={
} className="md:shrink-0 max-h-[260px] flex flex-col overflow-hidden" contentClassName="flex-1 min-h-0 overflow-y-auto" > setHoveredMmsiSet(setUniqueSorted(mmsis))} onClearHover={() => setHoveredMmsiSet([])} onHoverPair={(pairMmsis) => setHoveredPairMmsiSet(setUniqueSorted(pairMmsis))} onClearPairHover={() => setHoveredPairMmsiSet([])} onHoverFleet={(ownerKey, fleetMmsis) => { setHoveredFleetOwnerKey(ownerKey); setHoveredFleetMmsiSet(setSortedIfChanged(fleetMmsis)); }} onClearFleetHover={() => { setHoveredFleetOwnerKey(null); setHoveredFleetMmsiSet((prev) => (prev.length === 0 ? prev : [])); }} fleetSortMode={fleetRelationSortMode} hoveredFleetOwnerKey={hoveredFleetOwnerKey} hoveredFleetMmsiSet={hoveredFleetMmsiSet} onContextMenuFleet={onFleetContextMenu} />
선박 목록{' '} ({legacyVesselsFiltered.length}척) } className="md:flex-1 md:min-h-0 flex flex-col overflow-hidden" contentClassName="md:flex-1 md:min-h-0 flex flex-col overflow-hidden" > setHoveredMmsiSet([mmsi])} onClearHover={() => setHoveredMmsiSet([])} />
실시간 경고{' '} ({filteredAlarms.length}/{alarms.length}) } actions={ setIsAlarmFilterOpen((v) => !v)}> 필터 } className="md:shrink-0 max-h-[180px] flex flex-col overflow-hidden" contentClassName="flex-1 min-h-0 overflow-y-auto" > {isAlarmFilterOpen && (
{LEGACY_ALARM_KINDS.map((k) => ( ))}
)}
{adminMode ? ( <>
엔드포인트
{AIS_API_BASE}/api/ais-target/search
상태
{snapshot.status.toUpperCase()} {snapshot.error ? {snapshot.error} : null}
최근 fetch
{fmtIsoFull(snapshot.lastFetchAt)}{' '} ({snapshot.lastFetchMinutes ?? '-'}m, inserted {snapshot.lastInserted}, upserted {snapshot.lastUpserted})
메시지
{snapshot.lastMessage ?? '-'}
{legacyError ? (
legacy load error: {legacyError}
) : (
데이터셋
/data/legacy/chinese-permitted.v1.json
매칭(현재 scope)
{legacyVesselsAll.length}{' '} / {targetsInScope.length}
생성시각
{legacyData?.generatedAt ? fmtIsoFull(legacyData.generatedAt) : 'loading...'}
)}
현재 View BBox
{fmtBbox(viewBbox)}
표시 선박: {targetsInScope.length} / 스토어:{' '} {snapshot.total}
setSettings((prev) => ({ ...prev, [k]: !prev[k] }))} />
단일 WebGL 컨텍스트: MapboxOverlay(interleaved)
{zonesError ? (
zones load error: {zonesError}
) : (
{zones ? `loaded (${zones.features.length} features)` : 'loading...'}
)}
) : null}
); }