diff --git a/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java b/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java index f9eb073..e58b2b9 100644 --- a/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java +++ b/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java @@ -118,6 +118,8 @@ public class OsintCollector { if (articleUrl == null || title == null || title.isBlank()) continue; if (osintFeedRepository.existsBySourceAndSourceUrl("gdelt", articleUrl)) continue; + if (osintFeedRepository.existsByRegionAndTitleAndCollectedAtAfter( + region, title, Instant.now().minus(24, ChronoUnit.HOURS))) continue; String seendate = article.path("seendate").asText(null); Instant publishedAt = parseGdeltDate(seendate); @@ -182,6 +184,8 @@ public class OsintCollector { if (link == null || title == null || title.isBlank()) continue; if (osintFeedRepository.existsBySourceAndSourceUrl(sourceName, link)) continue; + if (osintFeedRepository.existsByRegionAndTitleAndCollectedAtAfter( + region, title, Instant.now().minus(24, ChronoUnit.HOURS))) continue; Instant publishedAt = parseRssDate(pubDate); diff --git a/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java b/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java index 95cf034..b87a231 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java +++ b/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java @@ -9,5 +9,7 @@ public interface OsintFeedRepository extends JpaRepository { boolean existsBySourceAndSourceUrl(String source, String sourceUrl); + boolean existsByRegionAndTitleAndCollectedAtAfter(String region, String title, Instant since); + List findByRegionAndCollectedAtAfterOrderByPublishedAtDesc(String region, Instant since); } diff --git a/frontend/src/components/layers/ShipLayer.tsx b/frontend/src/components/layers/ShipLayer.tsx index f82a686..7049302 100644 --- a/frontend/src/components/layers/ShipLayer.tsx +++ b/frontend/src/components/layers/ShipLayer.tsx @@ -359,6 +359,7 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM const [selectedMmsi, setSelectedMmsi] = useState(null); const [imageReady, setImageReady] = useState(false); const highlightKorean = !!koreanOnly; + const prevHoveredRef = useRef(null); // focusMmsi로 외부에서 모달 열기 useEffect(() => { @@ -399,7 +400,6 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM isMil: isMilitary(ship.category) ? 1 : 0, isKorean: ship.flag === 'KR' ? 1 : 0, isCheonghae: ship.mmsi === '440001981' ? 1 : 0, - isHovered: ship.mmsi === hoveredMmsi ? 1 : 0, heading: ship.heading, }, geometry: { @@ -408,7 +408,26 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM }, })); return { type: 'FeatureCollection' as const, features }; - }, [filtered, hoveredMmsi]); + }, [filtered]); + + // hoveredMmsi 변경 시 feature-state로 hover 표시 (GeoJSON 재생성 없이) + useEffect(() => { + if (!map) return; + const m = map.getMap(); + if (!m.getSource('ships-source')) return; + + if (prevHoveredRef.current != null) { + try { + m.removeFeatureState({ source: 'ships-source', id: prevHoveredRef.current }); + } catch { /* source not ready */ } + } + if (hoveredMmsi) { + try { + m.setFeatureState({ source: 'ships-source', id: hoveredMmsi }, { hovered: true }); + } catch { /* source not ready */ } + } + prevHoveredRef.current = hoveredMmsi ?? null; + }, [map, hoveredMmsi]); // Register click and cursor handlers useEffect(() => { @@ -447,12 +466,12 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM return ( <> - + {/* Hovered ship highlight ring */} - {/* Korean ship label (only when highlighted) */} - {highlightKorean && ( - - )} + {/* Korean ship label — always mounted, visibility으로 제어 */} + {/* Main ship triangles */}