From e304a841ed40f59dd57bb57ded79e9ec3e7d40db Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 19 Mar 2026 10:19:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20OSINT=20=EA=B8=B0=EC=82=AC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=88=98=EC=A7=91=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?+=20MapLibre=20symbol=20layer=20race=20condition=20=ED=95=B4?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OsintCollector: title 기반 24h 중복 체크 추가 (GDELT/Google News) - ShipLayer: hover를 feature-state로 분리하여 setData 빈도 감소 - ShipLayer: ships-korean-label 조건부 마운트 → visibility 제어로 변경 --- .../kcg/collector/osint/OsintCollector.java | 4 ++ .../kcg/domain/osint/OsintFeedRepository.java | 2 + frontend/src/components/layers/ShipLayer.tsx | 68 ++++++++++++------- 3 files changed, 49 insertions(+), 25 deletions(-) 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 */} Date: Thu, 19 Mar 2026 10:20:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index e85047c..f20faeb 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,13 @@ ## [Unreleased] +### 변경 +- 인라인 CSS 정리 — 공통 클래스 추출 + Tailwind 전환 + +### 수정 +- OSINT 기사 중복 수집 방지: title 기반 24h 중복 체크 추가 (GDELT/Google News) +- MapLibre symbol layer race condition 해소: hover를 feature-state로 분리, ships-korean-label visibility 제어로 변경 + ## [2026-03-18.5] ### 추가 -- 2.45.2