perf: 렌더링 성능 최적화 + 환적 Python 이관 + 중국어선감시 통합 #158
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
<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 => {
|
||||
const color = FILTER_COLOR[k];
|
||||
const count = getShipsForFilter(k).length;
|
||||
const filterShips = getShipsForFilter(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 (
|
||||
<div
|
||||
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={{
|
||||
background: isOpen ? `${color}44` : `${color}22`,
|
||||
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)}
|
||||
>
|
||||
<span className="text-[13px]">{FILTER_ICON[k]}</span>
|
||||
{FILTER_I18N_KEY[k] ? t(FILTER_I18N_KEY[k]) : k}
|
||||
<span className="ml-0.5 text-white/80">{count}척</span>
|
||||
{badgeName}
|
||||
<span className="ml-0.5 text-white/80">{badgeLabel}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user