fix: 헤더 1행 배치 + 비허가 어구 2개 이상만 탐지 + 중국 어구그룹 감시 배지
- 헤더 mode-toggle: flex-wrap:nowrap + overflow-x:auto → 1행 가로 스크롤 - 비허가 어구 그룹: 어구 2개 이상일 때만 그룹 탐지/폴리곤 생성 (1개는 제외) → 조업구역내 어구 + 선단 현황은 현행 유지 - cnFishing 배지: '중국 어구그룹 감시 N개' (어구그룹 수=고유 모선명 수) → 어구 패턴 매칭 선박만 집계 (중국 어선 단독은 제외) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
0da477c53c
커밋
c515975185
@ -1996,12 +1996,16 @@
|
|||||||
/* ======================== */
|
/* ======================== */
|
||||||
.mode-toggle {
|
.mode-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
background: var(--kcg-subtle);
|
background: var(--kcg-subtle);
|
||||||
border: 1px solid var(--kcg-border);
|
border: 1px solid var(--kcg-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
.mode-toggle::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
.mode-btn {
|
.mode-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -304,12 +304,17 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster
|
|||||||
const outZone: { name: string; parent: Ship | null; gears: Ship[] }[] = [];
|
const outZone: { name: string; parent: Ship | null; gears: Ship[] }[] = [];
|
||||||
for (const [name, { parent, gears }] of gearGroupMap) {
|
for (const [name, { parent, gears }] of gearGroupMap) {
|
||||||
const anchor = parent ?? gears[0];
|
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);
|
const zoneInfo = classifyFishingZone(anchor.lat, anchor.lng);
|
||||||
if (zoneInfo.zone !== 'OUTSIDE') {
|
if (zoneInfo.zone !== 'OUTSIDE') {
|
||||||
inZone.push({ name, parent, gears, zone: zoneInfo.name });
|
inZone.push({ name, parent, gears, zone: zoneInfo.name });
|
||||||
} else {
|
} 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);
|
inZone.sort((a, b) => b.gears.length - a.gears.length);
|
||||||
@ -318,10 +323,15 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster
|
|||||||
}, [gearGroupMap]);
|
}, [gearGroupMap]);
|
||||||
|
|
||||||
// 어구 클러스터 GeoJSON (수역 내: 붉은색, 수역 외: 오렌지)
|
// 어구 클러스터 GeoJSON (수역 내: 붉은색, 수역 외: 오렌지)
|
||||||
|
// 비허가 어구(outZone)는 2개 이상만 폴리곤 생성
|
||||||
const gearClusterGeoJson = useMemo((): GeoJSON => {
|
const gearClusterGeoJson = useMemo((): GeoJSON => {
|
||||||
const inZoneNames = new Set(inZoneGearGroups.map(g => g.name));
|
const inZoneNames = new Set(inZoneGearGroups.map(g => g.name));
|
||||||
|
const outZoneNames = new Set(outZoneGearGroups.map(g => g.name));
|
||||||
const features: GeoJSON.Feature[] = [];
|
const features: GeoJSON.Feature[] = [];
|
||||||
for (const [parentName, { parent, gears }] of gearGroupMap) {
|
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]);
|
const points: [number, number][] = gears.map(g => [g.lng, g.lat]);
|
||||||
if (parent) points.push([parent.lng, parent.lat]);
|
if (parent) points.push([parent.lng, parent.lat]);
|
||||||
if (points.length < 3) continue;
|
if (points.length < 3) continue;
|
||||||
@ -330,12 +340,12 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, cluster
|
|||||||
padded.push(padded[0]);
|
padded.push(padded[0]);
|
||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
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] },
|
geometry: { type: 'Polygon', coordinates: [padded] },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { type: 'FeatureCollection', features };
|
return { type: 'FeatureCollection', features };
|
||||||
}, [gearGroupMap, inZoneGearGroups]);
|
}, [gearGroupMap, inZoneGearGroups, outZoneGearGroups]);
|
||||||
|
|
||||||
const handleGearGroupZoom = useCallback((parentName: string) => {
|
const handleGearGroupZoom = useCallback((parentName: string) => {
|
||||||
setSelectedGearGroup(prev => prev === parentName ? null : parentName);
|
setSelectedGearGroup(prev => prev === parentName ? null : parentName);
|
||||||
|
|||||||
@ -698,7 +698,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
case 'cableWatch': return all.filter(s => cableWatchSuspects.has(s.mmsi));
|
case 'cableWatch': return all.filter(s => cableWatchSuspects.has(s.mmsi));
|
||||||
case 'dokdoWatch': return all.filter(s => dokdoWatchSuspects.has(s.mmsi));
|
case 'dokdoWatch': return all.filter(s => dokdoWatchSuspects.has(s.mmsi));
|
||||||
case 'ferryWatch': return all.filter(s => s.mtCategory === 'passenger');
|
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 [];
|
default: return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -721,12 +721,21 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
<div className="absolute top-2.5 left-1/2 -translate-x-1/2 z-20 flex gap-1.5 backdrop-blur-lg">
|
<div className="absolute top-2.5 left-1/2 -translate-x-1/2 z-20 flex gap-1.5 backdrop-blur-lg">
|
||||||
{active.map(k => {
|
{active.map(k => {
|
||||||
const color = FILTER_COLOR[k];
|
const color = FILTER_COLOR[k];
|
||||||
const count = getShipsForFilter(k).length;
|
const filterShips = getShipsForFilter(k);
|
||||||
const isOpen = activeBadgeFilter === k;
|
const isOpen = activeBadgeFilter === k;
|
||||||
|
// cnFishing: 어구그룹 수(고유 모선명)로 표시, 나머지: 선박 수
|
||||||
|
let badgeLabel: string;
|
||||||
|
if (k === 'cnFishing') {
|
||||||
|
const groupNames = new Set(filterShips.map(s => (s.name || '').match(/^(.+?)_\d+/)?.[1]).filter(Boolean));
|
||||||
|
badgeLabel = `${groupNames.size}개`;
|
||||||
|
} else {
|
||||||
|
badgeLabel = `${filterShips.length}척`;
|
||||||
|
}
|
||||||
|
const badgeName = k === 'cnFishing' ? '중국 어구그룹 감시' : (FILTER_I18N_KEY[k] ? t(FILTER_I18N_KEY[k]) : k);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={k}
|
key={k}
|
||||||
className={`rounded-lg px-3 py-1.5 font-mono text-[11px] font-bold flex items-center gap-1.5 cursor-pointer select-none ${isOpen ? '' : 'animate-pulse'}`}
|
className={`rounded-lg px-3 py-1.5 font-mono text-[11px] font-bold flex items-center gap-1.5 cursor-pointer select-none whitespace-nowrap ${isOpen ? '' : 'animate-pulse'}`}
|
||||||
style={{
|
style={{
|
||||||
background: isOpen ? `${color}44` : `${color}22`,
|
background: isOpen ? `${color}44` : `${color}22`,
|
||||||
border: `1px solid ${isOpen ? color : color + '88'}`,
|
border: `1px solid ${isOpen ? color : color + '88'}`,
|
||||||
@ -735,8 +744,8 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
onClick={() => setActiveBadgeFilter(prev => prev === k ? null : k)}
|
onClick={() => setActiveBadgeFilter(prev => prev === k ? null : k)}
|
||||||
>
|
>
|
||||||
<span className="text-[13px]">{FILTER_ICON[k]}</span>
|
<span className="text-[13px]">{FILTER_ICON[k]}</span>
|
||||||
{FILTER_I18N_KEY[k] ? t(FILTER_I18N_KEY[k]) : k}
|
{badgeName}
|
||||||
<span className="ml-0.5 text-white/80">{count}척</span>
|
<span className="ml-0.5 text-white/80">{badgeLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user