fix: OSINT 기사 중복 수집 방지 + MapLibre symbol layer race condition 해소
- OsintCollector: title 기반 24h 중복 체크 추가 (GDELT/Google News) - ShipLayer: hover를 feature-state로 분리하여 setData 빈도 감소 - ShipLayer: ships-korean-label 조건부 마운트 → visibility 제어로 변경
This commit is contained in:
부모
4dd1597111
커밋
e304a841ed
@ -118,6 +118,8 @@ public class OsintCollector {
|
|||||||
if (articleUrl == null || title == null || title.isBlank()) continue;
|
if (articleUrl == null || title == null || title.isBlank()) continue;
|
||||||
|
|
||||||
if (osintFeedRepository.existsBySourceAndSourceUrl("gdelt", articleUrl)) 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);
|
String seendate = article.path("seendate").asText(null);
|
||||||
Instant publishedAt = parseGdeltDate(seendate);
|
Instant publishedAt = parseGdeltDate(seendate);
|
||||||
@ -182,6 +184,8 @@ public class OsintCollector {
|
|||||||
|
|
||||||
if (link == null || title == null || title.isBlank()) continue;
|
if (link == null || title == null || title.isBlank()) continue;
|
||||||
if (osintFeedRepository.existsBySourceAndSourceUrl(sourceName, link)) continue;
|
if (osintFeedRepository.existsBySourceAndSourceUrl(sourceName, link)) continue;
|
||||||
|
if (osintFeedRepository.existsByRegionAndTitleAndCollectedAtAfter(
|
||||||
|
region, title, Instant.now().minus(24, ChronoUnit.HOURS))) continue;
|
||||||
|
|
||||||
Instant publishedAt = parseRssDate(pubDate);
|
Instant publishedAt = parseRssDate(pubDate);
|
||||||
|
|
||||||
|
|||||||
@ -9,5 +9,7 @@ public interface OsintFeedRepository extends JpaRepository<OsintFeed, Long> {
|
|||||||
|
|
||||||
boolean existsBySourceAndSourceUrl(String source, String sourceUrl);
|
boolean existsBySourceAndSourceUrl(String source, String sourceUrl);
|
||||||
|
|
||||||
|
boolean existsByRegionAndTitleAndCollectedAtAfter(String region, String title, Instant since);
|
||||||
|
|
||||||
List<OsintFeed> findByRegionAndCollectedAtAfterOrderByPublishedAtDesc(String region, Instant since);
|
List<OsintFeed> findByRegionAndCollectedAtAfterOrderByPublishedAtDesc(String region, Instant since);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -359,6 +359,7 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
|
|||||||
const [selectedMmsi, setSelectedMmsi] = useState<string | null>(null);
|
const [selectedMmsi, setSelectedMmsi] = useState<string | null>(null);
|
||||||
const [imageReady, setImageReady] = useState(false);
|
const [imageReady, setImageReady] = useState(false);
|
||||||
const highlightKorean = !!koreanOnly;
|
const highlightKorean = !!koreanOnly;
|
||||||
|
const prevHoveredRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// focusMmsi로 외부에서 모달 열기
|
// focusMmsi로 외부에서 모달 열기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -399,7 +400,6 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
|
|||||||
isMil: isMilitary(ship.category) ? 1 : 0,
|
isMil: isMilitary(ship.category) ? 1 : 0,
|
||||||
isKorean: ship.flag === 'KR' ? 1 : 0,
|
isKorean: ship.flag === 'KR' ? 1 : 0,
|
||||||
isCheonghae: ship.mmsi === '440001981' ? 1 : 0,
|
isCheonghae: ship.mmsi === '440001981' ? 1 : 0,
|
||||||
isHovered: ship.mmsi === hoveredMmsi ? 1 : 0,
|
|
||||||
heading: ship.heading,
|
heading: ship.heading,
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
@ -408,7 +408,26 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
return { type: 'FeatureCollection' as const, features };
|
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
|
// Register click and cursor handlers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -447,12 +466,12 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Source id="ships-source" type="geojson" data={shipGeoJson}>
|
<Source id="ships-source" type="geojson" data={shipGeoJson} promoteId="mmsi">
|
||||||
{/* Hovered ship highlight ring */}
|
{/* Hovered ship highlight ring */}
|
||||||
<Layer
|
<Layer
|
||||||
id="ships-hover-ring"
|
id="ships-hover-ring"
|
||||||
type="circle"
|
type="circle"
|
||||||
filter={['==', ['get', 'isHovered'], 1]}
|
filter={['boolean', ['feature-state', 'hovered'], false]}
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 18,
|
'circle-radius': 18,
|
||||||
'circle-color': 'rgba(255, 255, 255, 0.1)',
|
'circle-color': 'rgba(255, 255, 255, 0.1)',
|
||||||
@ -474,27 +493,26 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
|
|||||||
'circle-stroke-opacity': highlightKorean ? 1 : 0.6,
|
'circle-stroke-opacity': highlightKorean ? 1 : 0.6,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Korean ship label (only when highlighted) */}
|
{/* Korean ship label — always mounted, visibility으로 제어 */}
|
||||||
{highlightKorean && (
|
<Layer
|
||||||
<Layer
|
id="ships-korean-label"
|
||||||
id="ships-korean-label"
|
type="symbol"
|
||||||
type="symbol"
|
filter={['==', ['get', 'isKorean'], 1]}
|
||||||
filter={['==', ['get', 'isKorean'], 1]}
|
layout={{
|
||||||
layout={{
|
'visibility': highlightKorean ? 'visible' : 'none',
|
||||||
'text-field': ['get', 'name'],
|
'text-field': ['get', 'name'],
|
||||||
'text-size': 9,
|
'text-size': 9,
|
||||||
'text-offset': [0, 2.2],
|
'text-offset': [0, 2.2],
|
||||||
'text-anchor': 'top',
|
'text-anchor': 'top',
|
||||||
'text-allow-overlap': false,
|
'text-allow-overlap': false,
|
||||||
'text-font': ['Open Sans Regular'],
|
'text-font': ['Open Sans Regular'],
|
||||||
}}
|
}}
|
||||||
paint={{
|
paint={{
|
||||||
'text-color': '#00e5ff',
|
'text-color': '#00e5ff',
|
||||||
'text-halo-color': 'rgba(0,0,0,0.8)',
|
'text-halo-color': 'rgba(0,0,0,0.8)',
|
||||||
'text-halo-width': 1,
|
'text-halo-width': 1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{/* Main ship triangles */}
|
{/* Main ship triangles */}
|
||||||
<Layer
|
<Layer
|
||||||
id="ships-triangles"
|
id="ships-triangles"
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user