2026-02-16 23:55:58 +09:00
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
|
import { usePersistedState } from '../../shared/hooks';
|
|
|
|
|
import type { VesselTypeCode } from '../../entities/vessel/model/types';
|
|
|
|
|
import type { MapToggleState } from '../../features/mapToggles/MapToggles';
|
|
|
|
|
import type { LegacyAlarmKind } from '../../features/legacyDashboard/model/types';
|
|
|
|
|
import { LEGACY_ALARM_KINDS } from '../../features/legacyDashboard/model/types';
|
|
|
|
|
import type { BaseMapId, Map3DSettings, MapProjectionId } from '../../widgets/map3d/Map3D';
|
|
|
|
|
import type { MapViewState } from '../../widgets/map3d/types';
|
|
|
|
|
import { DEFAULT_MAP_STYLE_SETTINGS } from '../../features/mapSettings/types';
|
|
|
|
|
import type { MapStyleSettings } from '../../features/mapSettings/types';
|
|
|
|
|
import { fmtDateTimeFull } from '../../shared/lib/datetime';
|
|
|
|
|
|
|
|
|
|
export type Bbox = [number, number, number, number];
|
|
|
|
|
export type FleetRelationSortMode = 'count' | 'range';
|
|
|
|
|
|
|
|
|
|
export function useDashboardState(uid: number | null) {
|
|
|
|
|
// ── Map instance ──
|
|
|
|
|
const [mapInstance, setMapInstance] = useState<import('maplibre-gl').Map | null>(null);
|
|
|
|
|
const handleMapReady = useCallback((map: import('maplibre-gl').Map) => setMapInstance(map), []);
|
|
|
|
|
|
|
|
|
|
// ── Viewport / API BBox ──
|
|
|
|
|
const [viewBbox, setViewBbox] = useState<Bbox | null>(null);
|
|
|
|
|
const [useViewportFilter, setUseViewportFilter] = useState(false);
|
|
|
|
|
const [useApiBbox, setUseApiBbox] = useState(false);
|
|
|
|
|
const [apiBbox, setApiBbox] = useState<string | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
// ── Selection & hover ──
|
|
|
|
|
const [selectedMmsi, setSelectedMmsi] = useState<number | null>(null);
|
|
|
|
|
const [highlightedMmsiSet, setHighlightedMmsiSet] = useState<number[]>([]);
|
|
|
|
|
const [hoveredMmsiSet, setHoveredMmsiSet] = useState<number[]>([]);
|
|
|
|
|
const [hoveredFleetMmsiSet, setHoveredFleetMmsiSet] = useState<number[]>([]);
|
|
|
|
|
const [hoveredPairMmsiSet, setHoveredPairMmsiSet] = useState<number[]>([]);
|
|
|
|
|
const [hoveredFleetOwnerKey, setHoveredFleetOwnerKey] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// ── Filters (persisted) ──
|
|
|
|
|
const [typeEnabled, setTypeEnabled] = usePersistedState<Record<VesselTypeCode, boolean>>(
|
|
|
|
|
uid, 'typeEnabled', { PT: true, 'PT-S': true, GN: true, OT: true, PS: true, FC: true },
|
|
|
|
|
);
|
|
|
|
|
const [showTargets, setShowTargets] = usePersistedState(uid, 'showTargets', true);
|
2026-02-20 04:05:01 +09:00
|
|
|
const [showOthers, setShowOthers] = usePersistedState(uid, 'showOthers', true);
|
2026-02-16 23:55:58 +09:00
|
|
|
|
|
|
|
|
// ── Map settings (persisted) ──
|
|
|
|
|
// 레거시 베이스맵 비활성 — 향후 위성/라이트 등 추가 시 재활용
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
|
const [baseMap, _setBaseMap] = useState<BaseMapId>('enhanced');
|
|
|
|
|
const [projection, setProjection] = useState<MapProjectionId>('mercator');
|
|
|
|
|
const [mapStyleSettings, setMapStyleSettings] = usePersistedState<MapStyleSettings>(uid, 'mapStyleSettings', DEFAULT_MAP_STYLE_SETTINGS);
|
|
|
|
|
const [overlays, setOverlays] = usePersistedState<MapToggleState>(uid, 'overlays', {
|
|
|
|
|
pairLines: true, pairRange: true, fcLines: true, zones: true,
|
2026-02-20 10:27:55 +09:00
|
|
|
fleetCircles: true, predictVectors: true, shipLabels: true, subcables: false,
|
2026-02-16 23:55:58 +09:00
|
|
|
});
|
|
|
|
|
const [settings, setSettings] = usePersistedState<Map3DSettings>(uid, 'map3dSettings', {
|
|
|
|
|
showShips: true, showDensity: false, showSeamark: false,
|
|
|
|
|
});
|
|
|
|
|
const [mapView, setMapView] = usePersistedState<MapViewState | null>(uid, 'mapView', null);
|
|
|
|
|
|
2026-02-20 11:45:28 +09:00
|
|
|
// ── 자유 시점 (모드별 독립) ──
|
|
|
|
|
const [freeCameraMercator, setFreeCameraMercator] = usePersistedState(uid, 'freeCameraMercator', true);
|
|
|
|
|
const [freeCameraGlobe, setFreeCameraGlobe] = usePersistedState(uid, 'freeCameraGlobe', true);
|
|
|
|
|
const freeCamera = projection === 'globe' ? freeCameraGlobe : freeCameraMercator;
|
|
|
|
|
const toggleFreeCamera = useCallback(() => {
|
|
|
|
|
if (projection === 'globe') setFreeCameraGlobe((v) => !v);
|
|
|
|
|
else setFreeCameraMercator((v) => !v);
|
|
|
|
|
}, [projection, setFreeCameraGlobe, setFreeCameraMercator]);
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
// ── Sort & alarm filters (persisted) ──
|
|
|
|
|
const [fleetRelationSortMode, setFleetRelationSortMode] = usePersistedState<FleetRelationSortMode>(uid, 'sortMode', 'count');
|
|
|
|
|
const [alarmKindEnabled, setAlarmKindEnabled] = usePersistedState<Record<LegacyAlarmKind, boolean>>(
|
|
|
|
|
uid, 'alarmKindEnabled',
|
|
|
|
|
() => Object.fromEntries(LEGACY_ALARM_KINDS.map((k) => [k, true])) as Record<LegacyAlarmKind, boolean>,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ── Fleet focus ──
|
|
|
|
|
const [fleetFocus, setFleetFocus] = useState<{ id: string | number; center: [number, number]; zoom?: number } | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
// ── Cable ──
|
|
|
|
|
const [hoveredCableId, setHoveredCableId] = useState<string | null>(null);
|
|
|
|
|
const [selectedCableId, setSelectedCableId] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// ── Track context menu ──
|
|
|
|
|
const [trackContextMenu, setTrackContextMenu] = useState<{ x: number; y: number; mmsi: number; vesselName: string } | null>(null);
|
|
|
|
|
const handleOpenTrackMenu = useCallback((info: { x: number; y: number; mmsi: number; vesselName: string }) => setTrackContextMenu(info), []);
|
|
|
|
|
const handleCloseTrackMenu = useCallback(() => setTrackContextMenu(null), []);
|
|
|
|
|
|
|
|
|
|
// ── Projection loading ──
|
|
|
|
|
const [isProjectionLoading, setIsProjectionLoading] = useState(false);
|
|
|
|
|
const [isGlobeShipsReady, setIsGlobeShipsReady] = useState(false);
|
|
|
|
|
const handleProjectionLoadingChange = useCallback((loading: boolean) => setIsProjectionLoading(loading), []);
|
|
|
|
|
const showMapLoader = isProjectionLoading;
|
|
|
|
|
const isProjectionToggleDisabled = !isGlobeShipsReady || isProjectionLoading;
|
|
|
|
|
|
|
|
|
|
// ── Clock ──
|
|
|
|
|
const [clock, setClock] = useState(() => fmtDateTimeFull(new Date()));
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const id = window.setInterval(() => setClock(fmtDateTimeFull(new Date())), 1000);
|
|
|
|
|
return () => window.clearInterval(id);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// ── Admin mode (7 clicks within 900ms) ──
|
|
|
|
|
const [adminMode, setAdminMode] = useState(false);
|
|
|
|
|
const clicksRef = useRef<number[]>([]);
|
|
|
|
|
const onLogoClick = () => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
clicksRef.current = clicksRef.current.filter((t) => now - t < 900);
|
|
|
|
|
clicksRef.current.push(now);
|
|
|
|
|
if (clicksRef.current.length >= 7) {
|
|
|
|
|
clicksRef.current = [];
|
|
|
|
|
setAdminMode((v) => !v);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Helpers ──
|
|
|
|
|
const setUniqueSorted = (items: number[]) =>
|
|
|
|
|
Array.from(new Set(items.filter((item) => Number.isFinite(item)))).sort((a, b) => a - b);
|
|
|
|
|
|
|
|
|
|
const setSortedIfChanged = (next: number[]) => {
|
|
|
|
|
const sorted = setUniqueSorted(next);
|
|
|
|
|
return (prev: number[]) => (prev.length === sorted.length && prev.every((v, i) => v === sorted[i]) ? prev : sorted);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleHighlightedMmsi = (mmsi: number) => {
|
|
|
|
|
setHighlightedMmsiSet((prev) => {
|
|
|
|
|
const s = new Set(prev);
|
|
|
|
|
if (s.has(mmsi)) s.delete(mmsi);
|
|
|
|
|
else s.add(mmsi);
|
|
|
|
|
return Array.from(s).sort((a, b) => a - b);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
mapInstance, handleMapReady,
|
|
|
|
|
viewBbox, setViewBbox, useViewportFilter, setUseViewportFilter,
|
|
|
|
|
useApiBbox, setUseApiBbox, apiBbox, setApiBbox,
|
|
|
|
|
selectedMmsi, setSelectedMmsi,
|
|
|
|
|
highlightedMmsiSet,
|
|
|
|
|
hoveredMmsiSet, setHoveredMmsiSet,
|
|
|
|
|
hoveredFleetMmsiSet, setHoveredFleetMmsiSet,
|
|
|
|
|
hoveredPairMmsiSet, setHoveredPairMmsiSet,
|
|
|
|
|
hoveredFleetOwnerKey, setHoveredFleetOwnerKey,
|
|
|
|
|
typeEnabled, setTypeEnabled, showTargets, setShowTargets, showOthers, setShowOthers,
|
|
|
|
|
baseMap, projection, setProjection,
|
|
|
|
|
mapStyleSettings, setMapStyleSettings,
|
|
|
|
|
overlays, setOverlays, settings, setSettings,
|
2026-02-20 11:45:28 +09:00
|
|
|
mapView, setMapView, freeCamera, toggleFreeCamera,
|
2026-02-16 23:55:58 +09:00
|
|
|
fleetRelationSortMode, setFleetRelationSortMode,
|
|
|
|
|
alarmKindEnabled, setAlarmKindEnabled,
|
|
|
|
|
fleetFocus, setFleetFocus,
|
|
|
|
|
hoveredCableId, setHoveredCableId, selectedCableId, setSelectedCableId,
|
|
|
|
|
trackContextMenu, handleOpenTrackMenu, handleCloseTrackMenu,
|
|
|
|
|
handleProjectionLoadingChange,
|
|
|
|
|
isGlobeShipsReady, setIsGlobeShipsReady,
|
|
|
|
|
showMapLoader, isProjectionToggleDisabled,
|
|
|
|
|
clock, adminMode, onLogoClick,
|
|
|
|
|
setUniqueSorted, setSortedIfChanged, toggleHighlightedMmsi,
|
|
|
|
|
};
|
|
|
|
|
}
|