feat: 어구 1h/6h 듀얼 폴리곤 + 리플레이 컨트롤러 개선 + 심볼 스케일 #212
@ -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 (
|
||||
<FontScaleProvider>
|
||||
<SymbolScaleProvider>
|
||||
<SharedFilterProvider>
|
||||
<div className={`app ${isLive ? 'app-live' : ''}`}>
|
||||
<header className="app-header">
|
||||
@ -160,6 +162,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) {
|
||||
)}
|
||||
</div>
|
||||
</SharedFilterProvider>
|
||||
</SymbolScaleProvider>
|
||||
</FontScaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<string, string> = {
|
||||
@ -897,6 +898,7 @@ export function LayerPanel({
|
||||
)}
|
||||
</div>
|
||||
<FontScalePanel />
|
||||
<SymbolScalePanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
43
frontend/src/components/common/SymbolScalePanel.tsx
Normal file
43
frontend/src/components/common/SymbolScalePanel.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
import { useSymbolScale } from '../../hooks/useSymbolScale';
|
||||
import type { SymbolScaleConfig } from '../../contexts/symbolScaleState';
|
||||
|
||||
const LABELS: Record<keyof SymbolScaleConfig, string> = {
|
||||
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 (
|
||||
<div className="font-scale-section">
|
||||
<button type="button" className="font-scale-toggle" onClick={() => setOpen(!open)}>
|
||||
<span>◆ 심볼 크기</span>
|
||||
<span>{open ? '▼' : '▶'}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="font-scale-sliders">
|
||||
{(Object.keys(LABELS) as (keyof SymbolScaleConfig)[]).map(key => (
|
||||
<div key={key} className="font-scale-row">
|
||||
<label>{LABELS[key]}</label>
|
||||
<input type="range" min={0.5} max={2.0} step={0.1}
|
||||
value={symbolScale[key]}
|
||||
onChange={e => update(key, parseFloat(e.target.value))} />
|
||||
<span>{symbolScale[key].toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="font-scale-reset"
|
||||
onClick={() => setSymbolScale({ ship: 1.0, aircraft: 1.0 })}>
|
||||
초기화
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
10
frontend/src/contexts/SymbolScaleContext.tsx
Normal file
10
frontend/src/contexts/SymbolScaleContext.tsx
Normal file
@ -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 <SymbolScaleCtx.Provider value={{ symbolScale, setSymbolScale }}>{children}</SymbolScaleCtx.Provider>;
|
||||
}
|
||||
12
frontend/src/contexts/symbolScaleState.ts
Normal file
12
frontend/src/contexts/symbolScaleState.ts
Normal file
@ -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: () => {},
|
||||
});
|
||||
@ -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<number, number> = {
|
||||
};
|
||||
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<Layer[]>,
|
||||
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(() => {
|
||||
|
||||
6
frontend/src/hooks/useSymbolScale.ts
Normal file
6
frontend/src/hooks/useSymbolScale.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { SymbolScaleCtx } from '../contexts/symbolScaleState';
|
||||
|
||||
export function useSymbolScale() {
|
||||
return useContext(SymbolScaleCtx);
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user