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 = {