feat(vessel): 선박 검색 시 지도에 하이라이트 링 표시
- MapView, IncidentsView에 searchedVesselMmsi 상태 추가 - 검색된 선박 위치에 pulsing 링 애니메이션 Marker 렌더링 - 선박 클릭 시 하이라이트 초기화 - vsb-highlight-ring CSS 애니메이션 추가 (components.css)
This commit is contained in:
부모
559ebd666a
커밋
e8b9b92389
@ -1675,4 +1675,38 @@
|
|||||||
[data-theme='light'] .vsb-item {
|
[data-theme='light'] .vsb-item {
|
||||||
border-bottom: 1px solid var(--stroke-light);
|
border-bottom: 1px solid var(--stroke-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 선박 검색 하이라이트 링 */
|
||||||
|
.vsb-highlight-ring {
|
||||||
|
position: relative;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsb-highlight-ring::before,
|
||||||
|
.vsb-highlight-ring::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2.5px solid #38bdf8;
|
||||||
|
animation: vsb-ring-pulse 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsb-highlight-ring::after {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes vsb-ring-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2.8);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -399,6 +399,7 @@ export function MapView({
|
|||||||
const [vesselHover, setVesselHover] = useState<VesselHoverInfo | null>(null);
|
const [vesselHover, setVesselHover] = useState<VesselHoverInfo | null>(null);
|
||||||
const [selectedVessel, setSelectedVessel] = useState<VesselPosition | null>(null);
|
const [selectedVessel, setSelectedVessel] = useState<VesselPosition | null>(null);
|
||||||
const [detailVessel, setDetailVessel] = useState<VesselPosition | null>(null);
|
const [detailVessel, setDetailVessel] = useState<VesselPosition | null>(null);
|
||||||
|
const [searchedVesselMmsi, setSearchedVesselMmsi] = useState<string | null>(null);
|
||||||
const [vesselSearchFlyTarget, setVesselSearchFlyTarget] = useState<{
|
const [vesselSearchFlyTarget, setVesselSearchFlyTarget] = useState<{
|
||||||
lng: number;
|
lng: number;
|
||||||
lat: number;
|
lat: number;
|
||||||
@ -406,6 +407,14 @@ export function MapView({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const currentTime = isControlled ? externalCurrentTime : internalCurrentTime;
|
const currentTime = isControlled ? externalCurrentTime : internalCurrentTime;
|
||||||
|
|
||||||
|
const searchHighlightVessel = useMemo(
|
||||||
|
() =>
|
||||||
|
searchedVesselMmsi
|
||||||
|
? ((allVessels ?? vessels).find((v) => v.mmsi === searchedVesselMmsi) ?? null)
|
||||||
|
: null,
|
||||||
|
[searchedVesselMmsi, allVessels, vessels],
|
||||||
|
);
|
||||||
|
|
||||||
const handleMapCenterChange = useCallback((lat: number, lng: number, zoom: number) => {
|
const handleMapCenterChange = useCallback((lat: number, lng: number, zoom: number) => {
|
||||||
setMapCenter([lat, lng]);
|
setMapCenter([lat, lng]);
|
||||||
setMapZoom(zoom);
|
setMapZoom(zoom);
|
||||||
@ -1290,6 +1299,7 @@ export function MapView({
|
|||||||
onClick: (vessel) => {
|
onClick: (vessel) => {
|
||||||
setSelectedVessel(vessel);
|
setSelectedVessel(vessel);
|
||||||
setDetailVessel(null);
|
setDetailVessel(null);
|
||||||
|
setSearchedVesselMmsi(null);
|
||||||
},
|
},
|
||||||
onHover: (vessel, x, y) => {
|
onHover: (vessel, x, y) => {
|
||||||
setVesselHover(vessel ? { x, y, vessel } : null);
|
setVesselHover(vessel ? { x, y, vessel } : null);
|
||||||
@ -1406,6 +1416,19 @@ export function MapView({
|
|||||||
<HydrParticleOverlay hydrStep={hydrData[currentTime] ?? null} />
|
<HydrParticleOverlay hydrStep={hydrData[currentTime] ?? null} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 선박 검색 하이라이트 링 */}
|
||||||
|
{searchHighlightVessel && !isDrawingBoom && measureMode === null && drawAnalysisMode === null && (
|
||||||
|
<Marker
|
||||||
|
key={searchHighlightVessel.mmsi}
|
||||||
|
longitude={searchHighlightVessel.lon}
|
||||||
|
latitude={searchHighlightVessel.lat}
|
||||||
|
anchor="center"
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
<div className="vsb-highlight-ring" />
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 사고 위치 마커 (MapLibre Marker) */}
|
{/* 사고 위치 마커 (MapLibre Marker) */}
|
||||||
{incidentCoord &&
|
{incidentCoord &&
|
||||||
!isNaN(incidentCoord.lat) &&
|
!isNaN(incidentCoord.lat) &&
|
||||||
@ -1450,7 +1473,10 @@ export function MapView({
|
|||||||
{(allVessels ?? vessels).length > 0 && !isDrawingBoom && measureMode === null && drawAnalysisMode === null && (
|
{(allVessels ?? vessels).length > 0 && !isDrawingBoom && measureMode === null && drawAnalysisMode === null && (
|
||||||
<VesselSearchBar
|
<VesselSearchBar
|
||||||
vessels={allVessels ?? vessels}
|
vessels={allVessels ?? vessels}
|
||||||
onFlyTo={(v) => setVesselSearchFlyTarget({ lng: v.lon, lat: v.lat, zoom: 13 })}
|
onFlyTo={(v) => {
|
||||||
|
setVesselSearchFlyTarget({ lng: v.lon, lat: v.lat, zoom: 16 });
|
||||||
|
setSearchedVesselMmsi(v.mmsi);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Map as MapLibreMap, Popup } from '@vis.gl/react-maplibre';
|
import { Map as MapLibreMap, Marker, Popup } from '@vis.gl/react-maplibre';
|
||||||
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle';
|
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle';
|
||||||
import { ScatterplotLayer, PathLayer, TextLayer, GeoJsonLayer } from '@deck.gl/layers';
|
import { ScatterplotLayer, PathLayer, TextLayer, GeoJsonLayer } from '@deck.gl/layers';
|
||||||
import { PathStyleExtension } from '@deck.gl/extensions';
|
import { PathStyleExtension } from '@deck.gl/extensions';
|
||||||
@ -133,6 +133,17 @@ export function IncidentsView() {
|
|||||||
lat: number;
|
lat: number;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [searchedVesselMmsi, setSearchedVesselMmsi] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const searchHighlightVessel = useMemo(
|
||||||
|
() =>
|
||||||
|
searchedVesselMmsi
|
||||||
|
? ((allRealVessels.length > 0 ? allRealVessels : realVessels).find(
|
||||||
|
(v) => v.mmsi === searchedVesselMmsi,
|
||||||
|
) ?? null)
|
||||||
|
: null,
|
||||||
|
[searchedVesselMmsi, allRealVessels, realVessels],
|
||||||
|
);
|
||||||
|
|
||||||
const [vesselStatus, setVesselStatus] = useState<VesselCacheStatus | null>(null);
|
const [vesselStatus, setVesselStatus] = useState<VesselCacheStatus | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -624,6 +635,7 @@ export function IncidentsView() {
|
|||||||
});
|
});
|
||||||
setIncidentPopup(null);
|
setIncidentPopup(null);
|
||||||
setDetailVessel(null);
|
setDetailVessel(null);
|
||||||
|
setSearchedVesselMmsi(null);
|
||||||
},
|
},
|
||||||
onHover: (vessel, x, y) => {
|
onHover: (vessel, x, y) => {
|
||||||
if (vessel) {
|
if (vessel) {
|
||||||
@ -799,6 +811,19 @@ export function IncidentsView() {
|
|||||||
<FlyToController incident={selectedIncident} />
|
<FlyToController incident={selectedIncident} />
|
||||||
<VesselFlyToController target={vesselSearchFlyTarget} duration={1200} />
|
<VesselFlyToController target={vesselSearchFlyTarget} duration={1200} />
|
||||||
|
|
||||||
|
{/* 선박 검색 하이라이트 링 */}
|
||||||
|
{searchHighlightVessel && !dischargeMode && measureMode === null && (
|
||||||
|
<Marker
|
||||||
|
key={searchHighlightVessel.mmsi}
|
||||||
|
longitude={searchHighlightVessel.lon}
|
||||||
|
latitude={searchHighlightVessel.lat}
|
||||||
|
anchor="center"
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
<div className="vsb-highlight-ring" />
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 사고 팝업 */}
|
{/* 사고 팝업 */}
|
||||||
{incidentPopup && (
|
{incidentPopup && (
|
||||||
<Popup
|
<Popup
|
||||||
@ -823,7 +848,10 @@ export function IncidentsView() {
|
|||||||
{(allRealVessels.length > 0 || realVessels.length > 0) && !dischargeMode && measureMode === null && (
|
{(allRealVessels.length > 0 || realVessels.length > 0) && !dischargeMode && measureMode === null && (
|
||||||
<VesselSearchBar
|
<VesselSearchBar
|
||||||
vessels={allRealVessels.length > 0 ? allRealVessels : realVessels}
|
vessels={allRealVessels.length > 0 ? allRealVessels : realVessels}
|
||||||
onFlyTo={(v) => setVesselSearchFlyTarget({ lng: v.lon, lat: v.lat, zoom: 13 })}
|
onFlyTo={(v) => {
|
||||||
|
setVesselSearchFlyTarget({ lng: v.lon, lat: v.lat, zoom: 13 });
|
||||||
|
setSearchedVesselMmsi(v.mmsi);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user