fix: cn-fishing/localStorage 변경 통합 — develop 머지 후 누락 복원
- KoreaDashboard: useLocalStorage 적용 (koreaLayers, nationalities) - KoreaDashboard: useKoreaFilters에 cnFishingOn 파라미터 전달 - KoreaDashboard: useCallback 의존성 React Compiler 호환 - FleetClusterLayer: geometry import 복원 + 로컬 함수 제거
This commit is contained in:
부모
4ee977101b
커밋
5fd1e2b3cf
@ -6,6 +6,7 @@ import type { Ship, VesselAnalysisDto } from '../../types';
|
|||||||
import { fetchFleetCompanies } from '../../services/vesselAnalysis';
|
import { fetchFleetCompanies } from '../../services/vesselAnalysis';
|
||||||
import type { FleetCompany } from '../../services/vesselAnalysis';
|
import type { FleetCompany } from '../../services/vesselAnalysis';
|
||||||
import { classifyFishingZone } from '../../utils/fishingAnalysis';
|
import { classifyFishingZone } from '../../utils/fishingAnalysis';
|
||||||
|
import { convexHull, padPolygon, clusterColor } from '../../utils/geometry';
|
||||||
|
|
||||||
export interface SelectedGearGroupData {
|
export interface SelectedGearGroupData {
|
||||||
parent: Ship | null;
|
parent: Ship | null;
|
||||||
@ -29,59 +30,6 @@ interface Props {
|
|||||||
onSelectedFleetChange?: (data: SelectedFleetData | null) => void;
|
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 속성으로 주입
|
// GeoJSON feature에 color 속성으로 주입
|
||||||
interface ClusterPolygonFeature {
|
interface ClusterPolygonFeature {
|
||||||
type: 'Feature';
|
type: 'Feature';
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useLocalStorage, useLocalStorageSet } from '../../hooks/useLocalStorage';
|
||||||
import { KoreaMap } from './KoreaMap';
|
import { KoreaMap } from './KoreaMap';
|
||||||
import { FieldAnalysisModal } from './FieldAnalysisModal';
|
import { FieldAnalysisModal } from './FieldAnalysisModal';
|
||||||
import { LayerPanel } from '../common/LayerPanel';
|
import { LayerPanel } from '../common/LayerPanel';
|
||||||
@ -82,7 +83,7 @@ export const KoreaDashboard = ({
|
|||||||
const { hiddenAcCategories, hiddenShipCategories, toggleAcCategory, toggleShipCategory } =
|
const { hiddenAcCategories, hiddenShipCategories, toggleAcCategory, toggleShipCategory } =
|
||||||
useSharedFilters();
|
useSharedFilters();
|
||||||
|
|
||||||
const [koreaLayers, setKoreaLayers] = useState<Record<string, boolean>>({
|
const [koreaLayers, setKoreaLayers] = useLocalStorage<Record<string, boolean>>('koreaLayers', {
|
||||||
ships: true,
|
ships: true,
|
||||||
aircraft: true,
|
aircraft: true,
|
||||||
satellites: true,
|
satellites: true,
|
||||||
@ -122,25 +123,25 @@ export const KoreaDashboard = ({
|
|||||||
|
|
||||||
const toggleKoreaLayer = useCallback((key: string) => {
|
const toggleKoreaLayer = useCallback((key: string) => {
|
||||||
setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
}, []);
|
}, [setKoreaLayers]);
|
||||||
|
|
||||||
const [hiddenNationalities, setHiddenNationalities] = useState<Set<string>>(new Set());
|
const [hiddenNationalities, setHiddenNationalities] = useLocalStorageSet('hiddenNationalities', new Set());
|
||||||
const toggleNationality = useCallback((nat: string) => {
|
const toggleNationality = useCallback((nat: string) => {
|
||||||
setHiddenNationalities(prev => {
|
setHiddenNationalities(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(nat)) { next.delete(nat); } else { next.add(nat); }
|
if (next.has(nat)) { next.delete(nat); } else { next.add(nat); }
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, [setHiddenNationalities]);
|
||||||
|
|
||||||
const [hiddenFishingNats, setHiddenFishingNats] = useState<Set<string>>(new Set());
|
const [hiddenFishingNats, setHiddenFishingNats] = useLocalStorageSet('hiddenFishingNats', new Set());
|
||||||
const toggleFishingNat = useCallback((nat: string) => {
|
const toggleFishingNat = useCallback((nat: string) => {
|
||||||
setHiddenFishingNats(prev => {
|
setHiddenFishingNats(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(nat)) { next.delete(nat); } else { next.add(nat); }
|
if (next.has(nat)) { next.delete(nat); } else { next.add(nat); }
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, [setHiddenFishingNats]);
|
||||||
|
|
||||||
const koreaData = useKoreaData({
|
const koreaData = useKoreaData({
|
||||||
currentTime,
|
currentTime,
|
||||||
@ -158,6 +159,7 @@ export const KoreaDashboard = ({
|
|||||||
koreaData.visibleShips,
|
koreaData.visibleShips,
|
||||||
currentTime,
|
currentTime,
|
||||||
vesselAnalysis.analysisMap,
|
vesselAnalysis.analysisMap,
|
||||||
|
koreaLayers.cnFishing,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTabChange = useCallback((_tab: DashboardTab) => {
|
const handleTabChange = useCallback((_tab: DashboardTab) => {
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user