release: 2026-04-20 (346�� Ŀ��) #197
@ -1675,4 +1675,38 @@
|
||||
[data-theme='light'] .vsb-item {
|
||||
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 [selectedVessel, setSelectedVessel] = useState<VesselPosition | null>(null);
|
||||
const [detailVessel, setDetailVessel] = useState<VesselPosition | null>(null);
|
||||
const [searchedVesselMmsi, setSearchedVesselMmsi] = useState<string | null>(null);
|
||||
const [vesselSearchFlyTarget, setVesselSearchFlyTarget] = useState<{
|
||||
lng: number;
|
||||
lat: number;
|
||||
@ -406,6 +407,14 @@ export function MapView({
|
||||
} | null>(null);
|
||||
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) => {
|
||||
setMapCenter([lat, lng]);
|
||||
setMapZoom(zoom);
|
||||
@ -1290,6 +1299,7 @@ export function MapView({
|
||||
onClick: (vessel) => {
|
||||
setSelectedVessel(vessel);
|
||||
setDetailVessel(null);
|
||||
setSearchedVesselMmsi(null);
|
||||
},
|
||||
onHover: (vessel, x, y) => {
|
||||
setVesselHover(vessel ? { x, y, vessel } : null);
|
||||
@ -1406,6 +1416,19 @@ export function MapView({
|
||||
<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) */}
|
||||
{incidentCoord &&
|
||||
!isNaN(incidentCoord.lat) &&
|
||||
@ -1450,7 +1473,10 @@ export function MapView({
|
||||
{(allVessels ?? vessels).length > 0 && !isDrawingBoom && measureMode === null && drawAnalysisMode === null && (
|
||||
<VesselSearchBar
|
||||
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 { 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 { ScatterplotLayer, PathLayer, TextLayer, GeoJsonLayer } from '@deck.gl/layers';
|
||||
import { PathStyleExtension } from '@deck.gl/extensions';
|
||||
@ -133,6 +133,17 @@ export function IncidentsView() {
|
||||
lat: number;
|
||||
zoom: number;
|
||||
} | 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);
|
||||
useEffect(() => {
|
||||
@ -624,6 +635,7 @@ export function IncidentsView() {
|
||||
});
|
||||
setIncidentPopup(null);
|
||||
setDetailVessel(null);
|
||||
setSearchedVesselMmsi(null);
|
||||
},
|
||||
onHover: (vessel, x, y) => {
|
||||
if (vessel) {
|
||||
@ -799,6 +811,19 @@ export function IncidentsView() {
|
||||
<FlyToController incident={selectedIncident} />
|
||||
<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 && (
|
||||
<Popup
|
||||
@ -823,7 +848,10 @@ export function IncidentsView() {
|
||||
{(allRealVessels.length > 0 || realVessels.length > 0) && !dischargeMode && measureMode === null && (
|
||||
<VesselSearchBar
|
||||
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