From 8c5ba0000c7d995894bbcf3d165c3005440a11ab Mon Sep 17 00:00:00 2001 From: htlee Date: Fri, 20 Mar 2026 12:30:25 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A4=91=EA=B5=AD=EC=96=B4=EC=84=A0?= =?UTF-8?q?=EA=B0=90=EC=8B=9C=20=EC=97=B0=EA=B2=B0=EC=84=A0=20=ED=8F=AD?= =?UTF-8?q?=EB=B0=9C=20=E2=80=94=20=EB=B6=80=EB=B6=84=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20+=20=EA=B1=B0=EB=A6=AC=EC=A0=9C=ED=95=9C?= =?UTF-8?q?=20+=20=EB=A7=88=EC=BB=A4=20=EC=83=81=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gearLinks: 부분 매칭(startsWith) 제거 → 정확 이름 매칭만 - gearLinks: 거리 제한 0.15도(~10NM) 추가 — 원거리 연결선 차단 - gearLinks: 최대 200개 제한 - operating 마커: 최대 100척 - 역할 라벨: 일반 어선(FV) 제외, 본선/부속/운반만 최대 100개 - parentName 최소 3글자 이상만 매칭 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../korea/ChineseFishingOverlay.tsx | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/korea/ChineseFishingOverlay.tsx b/frontend/src/components/korea/ChineseFishingOverlay.tsx index 6131f3b..64eb937 100644 --- a/frontend/src/components/korea/ChineseFishingOverlay.tsx +++ b/frontend/src/components/korea/ChineseFishingOverlay.tsx @@ -100,47 +100,44 @@ export function ChineseFishingOverlay({ ships }: Props) { })); }, [chineseFishing]); - // 조업 중인 선박만 (어구 아이콘 표시용) - const operating = useMemo(() => analyzed.filter(a => a.analysis.isOperating), [analyzed]); + // 조업 중인 선박만 (어구 아이콘 표시용, 최대 100척) + const operating = useMemo(() => analyzed.filter(a => a.analysis.isOperating).slice(0, 100), [analyzed]); - // 어구/어망 → 모선 연결 탐지 + // 어구/어망 → 모선 연결 탐지 (거리 제한 + 정확 매칭 우선) const gearLinks: GearToParentLink[] = useMemo(() => { - // 어구/어망 선박 (이름_숫자_ 또는 이름% 패턴) const gearPattern = /^.+_\d+_\d*$|%$/; const gearShips = ships.filter(s => gearPattern.test(s.name)); if (gearShips.length === 0) return []; - // 모선 후보 (모든 선박의 이름 → Ship 매핑) + // 모선 후보 const nameMap = new Map(); for (const s of ships) { if (!gearPattern.test(s.name) && s.name) { - // 정확한 이름 매핑 nameMap.set(s.name.trim(), s); } } + const MAX_DIST_DEG = 0.15; // ~10NM — 이 이상 떨어지면 연결 안 함 + const MAX_LINKS = 200; // 브라우저 성능 보호 + const links: GearToParentLink[] = []; for (const gear of gearShips) { + if (links.length >= MAX_LINKS) break; + const parentName = extractParentName(gear.name); - if (!parentName) continue; + if (!parentName || parentName.length < 3) continue; // 너무 짧은 이름 제외 - // 정확히 일치하는 모선 찾기 - let parent = nameMap.get(parentName); + // 정확 매칭만 (부분 매칭 제거 — 오탐 원인) + const parent = nameMap.get(parentName); + if (!parent) continue; - // 정확 매칭 없으면 부분 매칭 (앞부분이 같은 선박) - if (!parent) { - for (const [name, ship] of nameMap) { - if (name.startsWith(parentName) || parentName.startsWith(name)) { - parent = ship; - break; - } - } - } + // 거리 제한: ~10NM 이내만 연결 + const dlat = Math.abs(gear.lat - parent.lat); + const dlng = Math.abs(gear.lng - parent.lng); + if (dlat > MAX_DIST_DEG || dlng > MAX_DIST_DEG) continue; - if (parent) { - links.push({ gear, parent, parentName }); - } + links.push({ gear, parent, parentName }); } return links; }, [ships]); @@ -227,8 +224,8 @@ export function ChineseFishingOverlay({ ships }: Props) { ); })} - {/* 본선/부속선/어선 역할 라벨 */} - {analyzed.filter(a => a.role.role).map(({ ship, role }) => ( + {/* 본선/부속선/어선 역할 라벨 (본선/부속/운반만, 최대 100개) */} + {analyzed.filter(a => a.role.role && a.role.role !== 'FV').slice(0, 100).map(({ ship, role }) => (