From 0da477c53c13041aa853c62ab3a5ad4847f9c299 Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 23 Mar 2026 12:55:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A4=91=EA=B5=AD=EC=96=B4=EC=84=A0?= =?UTF-8?q?=EA=B0=90=EC=8B=9C=20KoreaFilters=20=ED=86=B5=ED=95=A9=20+=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=B0=B0=EC=A7=80=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=84=A0=EB=B0=95=EB=AA=A9=EB=A1=9D/CSV=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cnFishing을 KoreaFilters 인터페이스에 통합 (koreaLayers → koreaFilters) → 다른 필터 탭과 동일한 선박 비활성화/상단 배지/카운트 동작 - 상단 필터 배지 클릭 → 대상 선박 목록 패널 (MMSI/이름/국적/유형/속도) → 선박 클릭 시 flyTo, 200척까지 표시 - CSV 다운로드: BOM 포함 UTF-8, 필터별 파일명 (e.g. cnFishing_2026-03-23.csv) - cnFishingSuspects Set 추가 (useKoreaFilters 반환값) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/korea/KoreaDashboard.tsx | 6 +- frontend/src/components/korea/KoreaMap.tsx | 146 +++++++++++++----- frontend/src/hooks/useKoreaFilters.ts | 27 +++- 3 files changed, 130 insertions(+), 49 deletions(-) diff --git a/frontend/src/components/korea/KoreaDashboard.tsx b/frontend/src/components/korea/KoreaDashboard.tsx index 84ec81b..a733c68 100644 --- a/frontend/src/components/korea/KoreaDashboard.tsx +++ b/frontend/src/components/korea/KoreaDashboard.tsx @@ -159,7 +159,6 @@ export const KoreaDashboard = ({ koreaData.visibleShips, currentTime, vesselAnalysis.analysisMap, - koreaLayers.cnFishing, ); const handleTabChange = useCallback((_tab: DashboardTab) => { @@ -198,8 +197,8 @@ export const KoreaDashboard = ({ onClick={() => koreaFiltersResult.setFilter('ferryWatch', !koreaFiltersResult.filters.ferryWatch)} title={t('filters.ferryWatch')}> 🚢{t('filters.ferryWatch')} - + + - ); - })} - +
+ + + + + + + + + + + + {badgeShips.slice(0, 200).map(s => ( + setFlyToTarget({ lng: s.lng, lat: s.lat, zoom: 12 })} + > + + + + + + + ))} + +
MMSINameFlagTypeSpeed
{s.mmsi}{s.name || '-'}{s.flag || '??'}{s.mtCategory || '-'}{s.speed?.toFixed(1)}kn
+ {badgeShips.length > 200 &&
...외 {badgeShips.length - 200}척
} +
+ + )} + ); })()} diff --git a/frontend/src/hooks/useKoreaFilters.ts b/frontend/src/hooks/useKoreaFilters.ts index 5a7dcde..e5a679d 100644 --- a/frontend/src/hooks/useKoreaFilters.ts +++ b/frontend/src/hooks/useKoreaFilters.ts @@ -12,6 +12,7 @@ interface KoreaFilters { cableWatch: boolean; dokdoWatch: boolean; ferryWatch: boolean; + cnFishing: boolean; } interface DokdoAlert { @@ -28,6 +29,7 @@ interface UseKoreaFiltersResult { transshipSuspects: Set; cableWatchSuspects: Set; dokdoWatchSuspects: Set; + cnFishingSuspects: Set; dokdoAlerts: DokdoAlert[]; anyFilterOn: boolean; } @@ -43,7 +45,6 @@ export function useKoreaFilters( visibleShips: Ship[], currentTime: number, analysisMap?: Map, - cnFishingOn = false, ): UseKoreaFiltersResult { const [filters, setFilters] = useLocalStorage('koreaFilters', { illegalFishing: false, @@ -52,6 +53,7 @@ export function useKoreaFilters( cableWatch: false, dokdoWatch: false, ferryWatch: false, + cnFishing: false, }); const [dokdoAlerts, setDokdoAlerts] = useState([]); @@ -70,7 +72,7 @@ export function useKoreaFilters( filters.cableWatch || filters.dokdoWatch || filters.ferryWatch || - cnFishingOn; + filters.cnFishing; // 불법환적 의심 선박 탐지 (Python 분석 결과 소비) const transshipSuspects = useMemo(() => { @@ -250,6 +252,18 @@ export function useKoreaFilters( return result; }, [koreaShips, filters.dokdoWatch, currentTime]); + // 중국어선 의심 선박 Set + const cnFishingSuspects = useMemo(() => { + if (!filters.cnFishing) return new Set(); + const result = new Set(); + for (const s of koreaShips) { + const isCnFishing = s.flag === 'CN' && s.mtCategory === 'fishing'; + const isGearPattern = /^.+?_\d+_\d+_?$/.test(s.name || ''); + if (isCnFishing || isGearPattern) result.add(s.mmsi); + } + return result; + }, [filters.cnFishing, koreaShips]); + // 필터링된 선박 목록 const filteredShips = useMemo(() => { if (!anyFilterOn) return visibleShips; @@ -272,14 +286,10 @@ export function useKoreaFilters( if (filters.cableWatch && cableWatchSet.has(s.mmsi)) return true; if (filters.dokdoWatch && dokdoWatchSet.has(s.mmsi)) return true; if (filters.ferryWatch && s.mtCategory === 'passenger') return true; - if (cnFishingOn) { - const isCnFishing = s.flag === 'CN' && s.mtCategory === 'fishing'; - const isGearPattern = /^.+?_\d+_\d+_?$/.test(s.name || ''); - if (isCnFishing || isGearPattern) return true; - } + if (filters.cnFishing && cnFishingSuspects.has(s.mmsi)) return true; return false; }); - }, [visibleShips, filters, anyFilterOn, transshipSuspects, darkVesselSet, cableWatchSet, dokdoWatchSet, analysisMap, cnFishingOn]); + }, [visibleShips, filters, anyFilterOn, transshipSuspects, darkVesselSet, cableWatchSet, dokdoWatchSet, analysisMap, cnFishingSuspects]); return { filters, @@ -288,6 +298,7 @@ export function useKoreaFilters( transshipSuspects, cableWatchSuspects: cableWatchSet, dokdoWatchSuspects: dokdoWatchSet, + cnFishingSuspects, dokdoAlerts, anyFilterOn, };