diff --git a/frontend/src/App.css b/frontend/src/App.css index de8a637..bc9f6e4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1996,12 +1996,16 @@ /* ======================== */ .mode-toggle { display: flex; + flex-wrap: nowrap; gap: 4px; background: var(--kcg-subtle); border: 1px solid var(--kcg-border); border-radius: 6px; padding: 3px; + overflow-x: auto; + scrollbar-width: none; } +.mode-toggle::-webkit-scrollbar { display: none; } .mode-btn { display: flex; diff --git a/frontend/src/components/korea/FleetClusterLayer.tsx b/frontend/src/components/korea/FleetClusterLayer.tsx index f73cf35..30f56ac 100644 --- a/frontend/src/components/korea/FleetClusterLayer.tsx +++ b/frontend/src/components/korea/FleetClusterLayer.tsx @@ -304,12 +304,17 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster const outZone: { name: string; parent: Ship | null; gears: Ship[] }[] = []; for (const [name, { parent, gears }] of gearGroupMap) { const anchor = parent ?? gears[0]; - if (!anchor) { outZone.push({ name, parent, gears }); continue; } + if (!anchor) { + // 비허가 어구: 2개 이상일 때만 그룹으로 탐지 + if (gears.length >= 2) outZone.push({ name, parent, gears }); + continue; + } const zoneInfo = classifyFishingZone(anchor.lat, anchor.lng); if (zoneInfo.zone !== 'OUTSIDE') { inZone.push({ name, parent, gears, zone: zoneInfo.name }); } else { - outZone.push({ name, parent, gears }); + // 비허가 어구: 2개 이상일 때만 그룹으로 탐지 + if (gears.length >= 2) outZone.push({ name, parent, gears }); } } inZone.sort((a, b) => b.gears.length - a.gears.length); @@ -318,10 +323,15 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster }, [gearGroupMap]); // 어구 클러스터 GeoJSON (수역 내: 붉은색, 수역 외: 오렌지) + // 비허가 어구(outZone)는 2개 이상만 폴리곤 생성 const gearClusterGeoJson = useMemo((): GeoJSON => { const inZoneNames = new Set(inZoneGearGroups.map(g => g.name)); + const outZoneNames = new Set(outZoneGearGroups.map(g => g.name)); const features: GeoJSON.Feature[] = []; for (const [parentName, { parent, gears }] of gearGroupMap) { + // 비허가(outZone) 1개짜리는 폴리곤에서 제외 + const isInZone = inZoneNames.has(parentName); + if (!isInZone && !outZoneNames.has(parentName)) continue; const points: [number, number][] = gears.map(g => [g.lng, g.lat]); if (parent) points.push([parent.lng, parent.lat]); if (points.length < 3) continue; @@ -330,12 +340,12 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster padded.push(padded[0]); features.push({ type: 'Feature', - properties: { name: parentName, gearCount: gears.length, inZone: inZoneNames.has(parentName) ? 1 : 0 }, + properties: { name: parentName, gearCount: gears.length, inZone: isInZone ? 1 : 0 }, geometry: { type: 'Polygon', coordinates: [padded] }, }); } return { type: 'FeatureCollection', features }; - }, [gearGroupMap, inZoneGearGroups]); + }, [gearGroupMap, inZoneGearGroups, outZoneGearGroups]); const handleGearGroupZoom = useCallback((parentName: string) => { setSelectedGearGroup(prev => prev === parentName ? null : parentName); diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 740366d..1699aef 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -698,7 +698,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF case 'cableWatch': return all.filter(s => cableWatchSuspects.has(s.mmsi)); case 'dokdoWatch': return all.filter(s => dokdoWatchSuspects.has(s.mmsi)); case 'ferryWatch': return all.filter(s => s.mtCategory === 'passenger'); - case 'cnFishing': return all.filter(s => (s.flag === 'CN' && s.mtCategory === 'fishing') || gearPattern.test(s.name || '')); + case 'cnFishing': return all.filter(s => gearPattern.test(s.name || '')); default: return []; } }; @@ -721,12 +721,21 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF