From 7ae867fe35fd6a115d98f26914d7d64caec19d45 Mon Sep 17 00:00:00 2001 From: htlee Date: Fri, 20 Feb 2026 11:45:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(map):=20=EC=9E=90=EC=9C=A0=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=20=ED=86=A0=EA=B8=80=20=EC=B6=94=EA=B0=80=20(?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=EB=B3=84=20=EB=8F=85=EB=A6=BD=20=EC=83=81?= =?UTF-8?q?=ED=83=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../web/src/pages/dashboard/DashboardPage.tsx | 1 + .../src/pages/dashboard/DashboardSidebar.tsx | 27 +++++++++++++------ .../src/pages/dashboard/useDashboardState.ts | 11 +++++++- apps/web/src/widgets/map3d/Map3D.tsx | 20 ++++++++++++++ .../web/src/widgets/map3d/hooks/useMapInit.ts | 8 +++--- .../map3d/hooks/useProjectionToggle.ts | 1 + apps/web/src/widgets/map3d/types.ts | 2 ++ 7 files changed, 57 insertions(+), 13 deletions(-) diff --git a/apps/web/src/pages/dashboard/DashboardPage.tsx b/apps/web/src/pages/dashboard/DashboardPage.tsx index 9cfd2b5..d6f31f4 100644 --- a/apps/web/src/pages/dashboard/DashboardPage.tsx +++ b/apps/web/src/pages/dashboard/DashboardPage.tsx @@ -411,6 +411,7 @@ export function DashboardPage() { onMapReady={handleMapReady} alarmMmsiMap={alarmMmsiMap} onClickShipPhoto={handleOpenImageModal} + freeCamera={state.freeCamera} /> 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 - +
+ + 자유 시점 + + 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] }))} /> diff --git a/apps/web/src/pages/dashboard/useDashboardState.ts b/apps/web/src/pages/dashboard/useDashboardState.ts index a8ce54d..efa56d1 100644 --- a/apps/web/src/pages/dashboard/useDashboardState.ts +++ b/apps/web/src/pages/dashboard/useDashboardState.ts @@ -54,6 +54,15 @@ export function useDashboardState(uid: number | null) { }); const [mapView, setMapView] = usePersistedState(uid, 'mapView', null); + // ── 자유 시점 (모드별 독립) ── + 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(uid, 'sortMode', 'count'); const [alarmKindEnabled, setAlarmKindEnabled] = usePersistedState>( @@ -132,7 +141,7 @@ export function useDashboardState(uid: number | null) { baseMap, projection, setProjection, mapStyleSettings, setMapStyleSettings, overlays, setOverlays, settings, setSettings, - mapView, setMapView, + mapView, setMapView, freeCamera, toggleFreeCamera, fleetRelationSortMode, setFleetRelationSortMode, alarmKindEnabled, setAlarmKindEnabled, fleetFocus, setFleetFocus, diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index be52cc6..3c30bb8 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -83,6 +83,7 @@ export function Map3D({ onMapReady, alarmMmsiMap, onClickShipPhoto, + freeCamera = true, }: Props) { // ── Shared refs ────────────────────────────────────────────────────── const containerRef = useRef(null); @@ -549,6 +550,25 @@ export function Map3D({ { projection, ensureMercatorOverlay, onProjectionLoadingChange, pulseMapSync, setMapSyncEpoch }, ); + // freeCamera 토글에 따라 회전/틸트 제어 (모드별 독립 상태) + // mapSyncEpoch: 맵 비동기 생성 후 최초 적용을 위해 의존 + useEffect(() => { + const map = mapRef.current; + if (!map) return; + try { + if (freeCamera) { + map.dragRotate.enable(); + map.touchPitch.enable(); + } else { + map.dragRotate.disable(); + map.touchPitch.disable(); + if (!projectionBusyRef.current && (map.getPitch() !== 0 || map.getBearing() !== 0)) { + map.easeTo({ pitch: 0, bearing: 0, duration: 300 }); + } + } + } catch { /* ignore */ } + }, [freeCamera, projection, mapSyncEpoch]); + useBaseMapToggle( mapRef, { baseMap, projection, showSeamark: settings.showSeamark, mapSyncEpoch, pulseMapSync }, diff --git a/apps/web/src/widgets/map3d/hooks/useMapInit.ts b/apps/web/src/widgets/map3d/hooks/useMapInit.ts index 0a276fd..c0a6a6a 100644 --- a/apps/web/src/widgets/map3d/hooks/useMapInit.ts +++ b/apps/web/src/widgets/map3d/hooks/useMapInit.ts @@ -81,12 +81,12 @@ export function useMapInit( style, center: iv?.center ?? [126.5, 34.2], zoom: iv?.zoom ?? 7, - pitch: iv?.pitch ?? 45, - bearing: iv?.bearing ?? 0, + pitch: 0, + bearing: 0, maxPitch: 85, - dragRotate: true, + dragRotate: false, pitchWithRotate: true, - touchPitch: true, + touchPitch: false, scrollZoom: { around: 'center' }, }); diff --git a/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts b/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts index 7040fcf..f75c38e 100644 --- a/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts +++ b/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts @@ -238,6 +238,7 @@ export function useProjectionToggle( map.setRenderWorldCopies(next !== 'globe'); // Globe에서는 easeTo around 미지원 → scrollZoom 동작 전환 + // dragRotate/touchPitch는 Map3D의 freeCamera effect에서 제어 try { map.scrollZoom.disable(); if (next === 'globe') { diff --git a/apps/web/src/widgets/map3d/types.ts b/apps/web/src/widgets/map3d/types.ts index f374532..ba66214 100644 --- a/apps/web/src/widgets/map3d/types.ts +++ b/apps/web/src/widgets/map3d/types.ts @@ -73,6 +73,8 @@ export interface Map3DProps { alarmMmsiMap?: Map; /** 사진 있는 선박 클릭 시 콜백 (사진 표시자 or 선박 아이콘) */ onClickShipPhoto?: (mmsi: number) => void; + /** 자유 시점 모드 (회전/틸트 허용) */ + freeCamera?: boolean; } export type DashSeg = {