gc-wing/apps/web/src/pages/dashboard/useDashboardState.ts

148 lines
7.3 KiB
TypeScript
Raw Normal View 히스토리

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);
const [showOthers, setShowOthers] = usePersistedState(uid, 'showOthers', false);
// ── 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,
fleetCircles: true, predictVectors: true, shipLabels: true, subcables: false, shipPhotos: true,
});
const [settings, setSettings] = usePersistedState<Map3DSettings>(uid, 'map3dSettings', {
showShips: true, showDensity: false, showSeamark: false,
});
const [mapView, setMapView] = usePersistedState<MapViewState | null>(uid, 'mapView', null);
// ── 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,
mapView, setMapView,
fleetRelationSortMode, setFleetRelationSortMode,
alarmKindEnabled, setAlarmKindEnabled,
fleetFocus, setFleetFocus,
hoveredCableId, setHoveredCableId, selectedCableId, setSelectedCableId,
trackContextMenu, handleOpenTrackMenu, handleCloseTrackMenu,
handleProjectionLoadingChange,
isGlobeShipsReady, setIsGlobeShipsReady,
showMapLoader, isProjectionToggleDisabled,
clock, adminMode, onLogoClick,
setUniqueSorted, setSortedIfChanged, toggleHighlightedMmsi,
};
}