Merge pull request 'release: 어구그룹 하이라이트' (#141) from develop into main
All checks were successful
Deploy KCG / deploy (push) Successful in 2m0s

This commit is contained in:
htlee 2026-03-20 19:08:15 +09:00
커밋 f98eca0aec

파일 보기

@ -1,5 +1,5 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Source, Layer } from 'react-map-gl/maplibre';
import { Source, Layer, Marker } from 'react-map-gl/maplibre';
import type { GeoJSON } from 'geojson';
import type { Ship, VesselAnalysisDto } from '../../types';
import { fetchFleetCompanies } from '../../services/vesselAnalysis';
@ -89,6 +89,7 @@ export function FleetClusterLayer({ ships, analysisMap, clusters, onShipSelect,
const [expandedFleet, setExpandedFleet] = useState<number | null>(null);
const [hoveredFleetId, setHoveredFleetId] = useState<number | null>(null);
const [expandedGearGroup, setExpandedGearGroup] = useState<string | null>(null);
const [selectedGearGroup, setSelectedGearGroup] = useState<string | null>(null);
useEffect(() => {
fetchFleetCompanies().then(setCompanies).catch(() => {});
@ -199,6 +200,8 @@ export function FleetClusterLayer({ ships, analysisMap, clusters, onShipSelect,
}, [gearGroupMap]);
const handleGearGroupZoom = useCallback((parentName: string) => {
setSelectedGearGroup(prev => prev === parentName ? null : parentName);
setExpandedGearGroup(parentName);
const entry = gearGroupMap.get(parentName);
if (!entry) return;
const all: Ship[] = [...entry.gears];
@ -383,6 +386,60 @@ export function FleetClusterLayer({ ships, analysisMap, clusters, onShipSelect,
/>
</Source>
{/* 선택된 어구 그룹 하이라이트 + 모선 마커 */}
{selectedGearGroup && (() => {
const entry = gearGroupMap.get(selectedGearGroup);
if (!entry) return null;
const points: [number, number][] = entry.gears.map(g => [g.lng, g.lat]);
if (entry.parent) points.push([entry.parent.lng, entry.parent.lat]);
const hlFeatures: GeoJSON.Feature[] = [];
if (points.length >= 3) {
const hull = convexHull(points);
const padded = padPolygon(hull, 0.01);
padded.push(padded[0]);
hlFeatures.push({
type: 'Feature',
properties: {},
geometry: { type: 'Polygon', coordinates: [padded] },
});
}
const hlGeoJson: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: hlFeatures };
return (
<>
{hlFeatures.length > 0 && (
<Source id="gear-cluster-selected" type="geojson" data={hlGeoJson}>
<Layer id="gear-selected-fill" type="fill" paint={{ 'fill-color': '#f97316', 'fill-opacity': 0.25 }} />
<Layer id="gear-selected-line" type="line" paint={{ 'line-color': '#f97316', 'line-width': 3, 'line-opacity': 0.9 }} />
</Source>
)}
{entry.parent && (
<Marker longitude={entry.parent.lng} latitude={entry.parent.lat} anchor="center">
<div style={{
width: 28, height: 28, borderRadius: '50%',
border: '3px solid #f97316',
backgroundColor: 'rgba(249, 115, 22, 0.3)',
boxShadow: '0 0 12px rgba(249, 115, 22, 0.6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
pointerEvents: 'none',
}}>
<span style={{ fontSize: 9, fontWeight: 900, color: '#fff' }}>M</span>
</div>
<div style={{
fontSize: 8, fontWeight: 700, color: '#f97316',
textShadow: '0 0 3px #000, 0 0 3px #000',
textAlign: 'center', marginTop: 2, whiteSpace: 'nowrap',
pointerEvents: 'none',
}}>
{entry.parent.name || entry.parent.mmsi}
</div>
</Marker>
)}
</>
);
})()}
{/* 비허가 어구 클러스터 폴리곤 */}
<Source id="gear-clusters" type="geojson" data={gearClusterGeoJson}>
<Layer