From 77efab8652800ab1eda65a8ae0f4d9ddbf3482de Mon Sep 17 00:00:00 2001 From: htlee Date: Wed, 1 Apr 2026 12:29:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=95=AD=EA=B3=B5=EA=B8=B0=20=EC=A4=8C?= =?UTF-8?q?=20=EC=8A=A4=EC=BC=80=EC=9D=BC=20+=20=EC=84=A0=EB=B0=95/?= =?UTF-8?q?=ED=95=AD=EA=B3=B5=EA=B8=B0=20=EC=8B=AC=EB=B3=BC=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=EC=A0=95=20=ED=8C=A8=EB=84=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 항공기 아이콘에 정수레벨 줌 기반 스케일 적용 (getZoomScale export) - 심볼 크기 조정: SymbolScaleContext + SymbolScalePanel (0.5~2.0x) - LayerPanel에 '심볼 크기' 섹션 추가 (선박/항공기 개별 조정) - localStorage 영속화 (mapSymbolScale) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 3 ++ frontend/src/components/common/LayerPanel.tsx | 2 + .../components/common/SymbolScalePanel.tsx | 43 +++++++++++++++++++ .../src/components/layers/AircraftLayer.tsx | 8 +++- frontend/src/contexts/SymbolScaleContext.tsx | 10 +++++ frontend/src/contexts/symbolScaleState.ts | 12 ++++++ frontend/src/hooks/useShipDeckLayers.ts | 9 ++-- frontend/src/hooks/useSymbolScale.ts | 6 +++ 8 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/common/SymbolScalePanel.tsx create mode 100644 frontend/src/contexts/SymbolScaleContext.tsx create mode 100644 frontend/src/contexts/symbolScaleState.ts create mode 100644 frontend/src/hooks/useSymbolScale.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bddd0be..ec2334b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import LoginPage from './components/auth/LoginPage'; import CollectorMonitor from './components/common/CollectorMonitor'; import { SharedFilterProvider } from './contexts/SharedFilterContext'; import { FontScaleProvider } from './contexts/FontScaleContext'; +import { SymbolScaleProvider } from './contexts/SymbolScaleContext'; import { IranDashboard } from './components/iran/IranDashboard'; import { KoreaDashboard } from './components/korea/KoreaDashboard'; import './App.css'; @@ -67,6 +68,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { return ( +
@@ -160,6 +162,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { )}
+
); } diff --git a/frontend/src/components/common/LayerPanel.tsx b/frontend/src/components/common/LayerPanel.tsx index d9e9f2c..2fb3345 100644 --- a/frontend/src/components/common/LayerPanel.tsx +++ b/frontend/src/components/common/LayerPanel.tsx @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocalStorageSet } from '../../hooks/useLocalStorage'; import { FontScalePanel } from './FontScalePanel'; +import { SymbolScalePanel } from './SymbolScalePanel'; // Aircraft category colors (matches AircraftLayer military fixed colors) const AC_CAT_COLORS: Record = { @@ -897,6 +898,7 @@ export function LayerPanel({ )} + ); } diff --git a/frontend/src/components/common/SymbolScalePanel.tsx b/frontend/src/components/common/SymbolScalePanel.tsx new file mode 100644 index 0000000..4ec6021 --- /dev/null +++ b/frontend/src/components/common/SymbolScalePanel.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { useSymbolScale } from '../../hooks/useSymbolScale'; +import type { SymbolScaleConfig } from '../../contexts/symbolScaleState'; + +const LABELS: Record = { + ship: '선박 심볼', + aircraft: '항공기 심볼', +}; + +export function SymbolScalePanel() { + const { symbolScale, setSymbolScale } = useSymbolScale(); + const [open, setOpen] = useState(false); + + const update = (key: keyof SymbolScaleConfig, val: number) => { + setSymbolScale({ ...symbolScale, [key]: Math.round(val * 10) / 10 }); + }; + + return ( +
+ + {open && ( +
+ {(Object.keys(LABELS) as (keyof SymbolScaleConfig)[]).map(key => ( +
+ + update(key, parseFloat(e.target.value))} /> + {symbolScale[key].toFixed(1)} +
+ ))} + +
+ )} +
+ ); +} diff --git a/frontend/src/components/layers/AircraftLayer.tsx b/frontend/src/components/layers/AircraftLayer.tsx index 874852d..13230d2 100644 --- a/frontend/src/components/layers/AircraftLayer.tsx +++ b/frontend/src/components/layers/AircraftLayer.tsx @@ -2,6 +2,9 @@ import { memo, useMemo, useState, useEffect } from 'react'; import { Marker, Popup, Source, Layer } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; import type { Aircraft, AircraftCategory } from '../../types'; +import { useShipDeckStore } from '../../stores/shipDeckStore'; +import { getZoomScale } from '../../hooks/useShipDeckLayers'; +import { useSymbolScale } from '../../hooks/useSymbolScale'; interface Props { aircraft: Aircraft[]; @@ -187,9 +190,12 @@ export function AircraftLayer({ aircraft, militaryOnly }: Props) { const AircraftMarker = memo(function AircraftMarker({ ac }: { ac: Aircraft }) { const { t } = useTranslation('ships'); const [showPopup, setShowPopup] = useState(false); + const zoomLevel = useShipDeckStore(s => s.zoomLevel); + const { symbolScale } = useSymbolScale(); const color = getAircraftColor(ac); const shape = getShape(ac); - const size = shape.w; + const zs = getZoomScale(zoomLevel); + const size = Math.round(shape.w * zs * symbolScale.aircraft / 0.8); const showLabel = ac.category === 'fighter' || ac.category === 'surveillance'; const strokeWidth = ac.category === 'fighter' || ac.category === 'military' ? 1.5 : 0.8; diff --git a/frontend/src/contexts/SymbolScaleContext.tsx b/frontend/src/contexts/SymbolScaleContext.tsx new file mode 100644 index 0000000..809e38a --- /dev/null +++ b/frontend/src/contexts/SymbolScaleContext.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from 'react'; +import { useLocalStorage } from '../hooks/useLocalStorage'; +import { SymbolScaleCtx, DEFAULT_SYMBOL_SCALE } from './symbolScaleState'; + +export type { SymbolScaleConfig } from './symbolScaleState'; + +export function SymbolScaleProvider({ children }: { children: ReactNode }) { + const [symbolScale, setSymbolScale] = useLocalStorage('mapSymbolScale', DEFAULT_SYMBOL_SCALE); + return {children}; +} diff --git a/frontend/src/contexts/symbolScaleState.ts b/frontend/src/contexts/symbolScaleState.ts new file mode 100644 index 0000000..d5aa5bf --- /dev/null +++ b/frontend/src/contexts/symbolScaleState.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react'; + +export interface SymbolScaleConfig { + ship: number; + aircraft: number; +} + +export const DEFAULT_SYMBOL_SCALE: SymbolScaleConfig = { ship: 1.0, aircraft: 1.0 }; + +export const SymbolScaleCtx = createContext<{ symbolScale: SymbolScaleConfig; setSymbolScale: (c: SymbolScaleConfig) => void }>({ + symbolScale: DEFAULT_SYMBOL_SCALE, setSymbolScale: () => {}, +}); diff --git a/frontend/src/hooks/useShipDeckLayers.ts b/frontend/src/hooks/useShipDeckLayers.ts index c96ec5a..bdf1e88 100644 --- a/frontend/src/hooks/useShipDeckLayers.ts +++ b/frontend/src/hooks/useShipDeckLayers.ts @@ -9,6 +9,7 @@ import { getMarineTrafficCategory } from '../utils/marineTraffic'; import { getNationalityGroup } from './useKoreaData'; import { FONT_MONO } from '../styles/fonts'; import type { Ship, VesselAnalysisDto } from '../types'; +import { useSymbolScale } from './useSymbolScale'; // ── Constants ───────────────────────────────────────────────────────────────── @@ -19,7 +20,7 @@ const ZOOM_SCALE: Record = { }; const ZOOM_SCALE_DEFAULT = 4.2; // z14+ -function getZoomScale(zoom: number): number { +export function getZoomScale(zoom: number): number { if (zoom >= 14) return ZOOM_SCALE_DEFAULT; return ZOOM_SCALE[zoom] ?? ZOOM_SCALE_DEFAULT; } @@ -156,6 +157,8 @@ export function useShipDeckLayers( shipLayerRef: React.MutableRefObject, requestRender: () => void, ): void { + const { symbolScale } = useSymbolScale(); + const shipSymbolScale = symbolScale.ship; const renderFrame = useCallback(() => { const state = useShipDeckStore.getState(); @@ -170,7 +173,7 @@ export function useShipDeckLayers( return; } - const zoomScale = getZoomScale(zoomLevel); + const zoomScale = getZoomScale(zoomLevel) * shipSymbolScale; const layers: Layer[] = []; // 1. Build filtered ship render data (~3K ships, <1ms) @@ -316,7 +319,7 @@ export function useShipDeckLayers( shipLayerRef.current = layers; requestRender(); - }, [shipLayerRef, requestRender]); + }, [shipLayerRef, requestRender, shipSymbolScale]); // Subscribe to all relevant state changes useEffect(() => { diff --git a/frontend/src/hooks/useSymbolScale.ts b/frontend/src/hooks/useSymbolScale.ts new file mode 100644 index 0000000..d018b13 --- /dev/null +++ b/frontend/src/hooks/useSymbolScale.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { SymbolScaleCtx } from '../contexts/symbolScaleState'; + +export function useSymbolScale() { + return useContext(SymbolScaleCtx); +}