gc-wing/apps/web/src/pages/dashboard/useDashboardState.ts
htlee d0f67ae803 feat(encMap): gcnautical 타일 서버 기반 ENC 전자해도 + UI 개선
## ENC 베이스맵 (features/encMap/)
- gcnautical 타일 서버 연동 (nautical.json 49개 레이어, 73개 S-52 스프라이트)
- 설정 패널: 12개 레이어 토글, 영역 색상 3종, 수심 색상 5단계
- 배경색 밝기 기반 선박 라벨 색상 자동 전환 (labelColor.ts)
- useMapStyleSettings에 ENC 가드 추가 (스타일 간섭 방지)
- useBaseMapToggle 초기 로드 스킵 (useMapInit과 중복 setStyle 방지)

## 선박 표시 개선
- Globe 원형 halo/outline 제거 — 아이콘 본체만 표시
- Globe 아이콘 스케일 1.3배, 줌아웃 최소 크기 보장 (minzoom 2)
- SDF icon-halo로 테두리 적용 (성능 영향 없음)
- 기타 AIS 투명도 상향 (0.28→0.6 ~ 1.0)
- 선박명 영문 우선 표시 (shipNameRoman > shipNameCn)

## 오버레이 제어 수정
- 연결선/범위/선단 토글 off 시 인터랙티브 오버레이도 비활성
- Globe pair/fc/fleet 레이어: || active 제거 → 토글 우선
- 강조 링/알람 링: shipData→shipLayerData (클러스터링 연동)

## 기본값 변경
- 경고 필터 5개: 초기 false
- 연결선/범위/선단: 초기 false
- 사진 파란 원 아이콘: Globe+Mercator 모두 제거

## 폰트 정리
- Open Sans 폴백 전면 제거 → Noto Sans 단독
- ENC 스타일 fetch 시 text-font 패치

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 14:19:28 +09:00

165 lines
8.6 KiB
TypeScript

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 { DEFAULT_OCEAN_MAP_SETTINGS } from '../../features/oceanMap/model/types';
import type { OceanMapSettings } from '../../features/oceanMap/model/types';
import { DEFAULT_ENC_MAP_SETTINGS } from '../../features/encMap/model/types';
import type { EncMapSettings } from '../../features/encMap/model/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', true);
// ── Map settings (persisted) ──
const [baseMap, setBaseMap] = usePersistedState<BaseMapId>(uid, 'baseMap', 'ocean');
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: false, pairRange: false, fcLines: false, zones: true,
fleetCircles: false, predictVectors: false, shipLabels: true, subcables: false,
});
const [settings, setSettings] = usePersistedState<Map3DSettings>(uid, 'map3dSettings', {
showShips: true, showDensity: false, showSeamark: false,
});
const [mapView, setMapView] = usePersistedState<MapViewState | null>(uid, 'mapView', null);
const [oceanMapSettings, setOceanMapSettings] = usePersistedState<OceanMapSettings>(uid, 'oceanMapSettings', DEFAULT_OCEAN_MAP_SETTINGS);
const [encMapSettingsRaw, setEncMapSettings] = usePersistedState<EncMapSettings>(uid, 'encMapSettings', DEFAULT_ENC_MAP_SETTINGS);
// Merge with defaults to fill missing fields from older localStorage entries
const encMapSettings: EncMapSettings = { ...DEFAULT_ENC_MAP_SETTINGS, ...encMapSettingsRaw };
// ── 자유 시점 (모드별 독립) ──
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]);
// ── 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, false])) 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; isPermitted: boolean } | null>(null);
const handleOpenTrackMenu = useCallback((info: { x: number; y: number; mmsi: number; vesselName: string; isPermitted: boolean }) => 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, setBaseMap, projection, setProjection,
mapStyleSettings, setMapStyleSettings,
overlays, setOverlays, settings, setSettings,
mapView, setMapView, freeCamera, toggleFreeCamera,
oceanMapSettings, setOceanMapSettings,
encMapSettings, setEncMapSettings,
fleetRelationSortMode, setFleetRelationSortMode,
alarmKindEnabled, setAlarmKindEnabled,
fleetFocus, setFleetFocus,
hoveredCableId, setHoveredCableId, selectedCableId, setSelectedCableId,
trackContextMenu, handleOpenTrackMenu, handleCloseTrackMenu,
handleProjectionLoadingChange,
isGlobeShipsReady, setIsGlobeShipsReady,
showMapLoader, isProjectionToggleDisabled,
clock, adminMode, onLogoClick,
setUniqueSorted, setSortedIfChanged, toggleHighlightedMmsi,
};
}