From 5fd1e2b3cf0976d2a8b9b2b15cd19c5c5cb1f668 Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 23 Mar 2026 11:05:05 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20cn-fishing/localStorage=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=ED=86=B5=ED=95=A9=20=E2=80=94=20develop=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=ED=9B=84=20=EB=88=84=EB=9D=BD=20=EB=B3=B5?= =?UTF-8?q?=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KoreaDashboard: useLocalStorage 적용 (koreaLayers, nationalities) - KoreaDashboard: useKoreaFilters에 cnFishingOn 파라미터 전달 - KoreaDashboard: useCallback 의존성 React Compiler 호환 - FleetClusterLayer: geometry import 복원 + 로컬 함수 제거 --- .../components/korea/FleetClusterLayer.tsx | 54 +------------------ .../src/components/korea/KoreaDashboard.tsx | 14 ++--- 2 files changed, 9 insertions(+), 59 deletions(-) diff --git a/frontend/src/components/korea/FleetClusterLayer.tsx b/frontend/src/components/korea/FleetClusterLayer.tsx index 76f3b10..f73cf35 100644 --- a/frontend/src/components/korea/FleetClusterLayer.tsx +++ b/frontend/src/components/korea/FleetClusterLayer.tsx @@ -6,6 +6,7 @@ import type { Ship, VesselAnalysisDto } from '../../types'; import { fetchFleetCompanies } from '../../services/vesselAnalysis'; import type { FleetCompany } from '../../services/vesselAnalysis'; import { classifyFishingZone } from '../../utils/fishingAnalysis'; +import { convexHull, padPolygon, clusterColor } from '../../utils/geometry'; export interface SelectedGearGroupData { parent: Ship | null; @@ -29,59 +30,6 @@ interface Props { onSelectedFleetChange?: (data: SelectedFleetData | null) => void; } -// 두 벡터의 외적 (2D) — Graham scan에서 왼쪽 회전 여부 판별 -function cross(o: [number, number], a: [number, number], b: [number, number]): number { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); -} - -// Graham scan 기반 볼록 껍질 (반시계 방향) -function convexHull(points: [number, number][]): [number, number][] { - const n = points.length; - if (n < 2) return points.slice(); - const sorted = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); - const lower: [number, number][] = []; - for (const p of sorted) { - while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) { - lower.pop(); - } - lower.push(p); - } - const upper: [number, number][] = []; - for (let i = sorted.length - 1; i >= 0; i--) { - const p = sorted[i]; - while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) { - upper.pop(); - } - upper.push(p); - } - // lower + upper (첫/끝 중복 제거) - lower.pop(); - upper.pop(); - return lower.concat(upper); -} - -// 중심에서 각 꼭짓점 방향으로 padding 확장 -function padPolygon(hull: [number, number][], padding: number): [number, number][] { - if (hull.length === 0) return hull; - const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length; - const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length; - return hull.map(([x, y]) => { - const dx = x - cx; - const dy = y - cy; - const len = Math.sqrt(dx * dx + dy * dy); - if (len === 0) return [x + padding, y + padding] as [number, number]; - const scale = (len + padding) / len; - return [cx + dx * scale, cy + dy * scale] as [number, number]; - }); -} - -// cluster_id 해시 → HSL 색상 -function clusterColor(id: number): string { - const h = (id * 137) % 360; - return `hsl(${h}, 80%, 55%)`; -} - -// HSL 문자열 → hex 근사 (MapLibre는 hsl() 지원하므로 직접 사용 가능) // GeoJSON feature에 color 속성으로 주입 interface ClusterPolygonFeature { type: 'Feature'; diff --git a/frontend/src/components/korea/KoreaDashboard.tsx b/frontend/src/components/korea/KoreaDashboard.tsx index 7d4c14a..d0a8e28 100644 --- a/frontend/src/components/korea/KoreaDashboard.tsx +++ b/frontend/src/components/korea/KoreaDashboard.tsx @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; +import { useLocalStorage, useLocalStorageSet } from '../../hooks/useLocalStorage'; import { KoreaMap } from './KoreaMap'; import { FieldAnalysisModal } from './FieldAnalysisModal'; import { LayerPanel } from '../common/LayerPanel'; @@ -82,7 +83,7 @@ export const KoreaDashboard = ({ const { hiddenAcCategories, hiddenShipCategories, toggleAcCategory, toggleShipCategory } = useSharedFilters(); - const [koreaLayers, setKoreaLayers] = useState>({ + const [koreaLayers, setKoreaLayers] = useLocalStorage>('koreaLayers', { ships: true, aircraft: true, satellites: true, @@ -122,25 +123,25 @@ export const KoreaDashboard = ({ const toggleKoreaLayer = useCallback((key: string) => { setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] })); - }, []); + }, [setKoreaLayers]); - const [hiddenNationalities, setHiddenNationalities] = useState>(new Set()); + const [hiddenNationalities, setHiddenNationalities] = useLocalStorageSet('hiddenNationalities', new Set()); const toggleNationality = useCallback((nat: string) => { setHiddenNationalities(prev => { const next = new Set(prev); if (next.has(nat)) { next.delete(nat); } else { next.add(nat); } return next; }); - }, []); + }, [setHiddenNationalities]); - const [hiddenFishingNats, setHiddenFishingNats] = useState>(new Set()); + const [hiddenFishingNats, setHiddenFishingNats] = useLocalStorageSet('hiddenFishingNats', new Set()); const toggleFishingNat = useCallback((nat: string) => { setHiddenFishingNats(prev => { const next = new Set(prev); if (next.has(nat)) { next.delete(nat); } else { next.add(nat); } return next; }); - }, []); + }, [setHiddenFishingNats]); const koreaData = useKoreaData({ currentTime, @@ -158,6 +159,7 @@ export const KoreaDashboard = ({ koreaData.visibleShips, currentTime, vesselAnalysis.analysisMap, + koreaLayers.cnFishing, ); const handleTabChange = useCallback((_tab: DashboardTab) => {