refactor+fix: 인라인 CSS 정리 + OSINT 중복 수집 방지 + MapLibre race condition 해소 #70

병합
htlee refactor/inline-css-cleanup 에서 develop 로 2 commits 를 머지했습니다 2026-03-19 10:21:22 +09:00
4개의 변경된 파일56개의 추가작업 그리고 25개의 파일을 삭제

파일 보기

@ -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);

파일 보기

@ -9,5 +9,7 @@ public interface OsintFeedRepository extends JpaRepository<OsintFeed, Long> {
boolean existsBySourceAndSourceUrl(String source, String sourceUrl);
boolean existsByRegionAndTitleAndCollectedAtAfter(String region, String title, Instant since);
List<OsintFeed> findByRegionAndCollectedAtAfterOrderByPublishedAtDesc(String region, Instant since);
}

파일 보기

@ -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]
### 추가

파일 보기

@ -359,6 +359,7 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM
const [selectedMmsi, setSelectedMmsi] = useState<string | null>(null);
const [imageReady, setImageReady] = useState(false);
const highlightKorean = !!koreanOnly;
const prevHoveredRef = useRef<string | null>(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 (
<>
<Source id="ships-source" type="geojson" data={shipGeoJson}>
<Source id="ships-source" type="geojson" data={shipGeoJson} promoteId="mmsi">
{/* Hovered ship highlight ring */}
<Layer
id="ships-hover-ring"
type="circle"
filter={['==', ['get', 'isHovered'], 1]}
filter={['boolean', ['feature-state', 'hovered'], false]}
paint={{
'circle-radius': 18,
'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,
}}
/>
{/* Korean ship label (only when highlighted) */}
{highlightKorean && (
<Layer
id="ships-korean-label"
type="symbol"
filter={['==', ['get', 'isKorean'], 1]}
layout={{
'text-field': ['get', 'name'],
'text-size': 9,
'text-offset': [0, 2.2],
'text-anchor': 'top',
'text-allow-overlap': false,
'text-font': ['Open Sans Regular'],
}}
paint={{
'text-color': '#00e5ff',
'text-halo-color': 'rgba(0,0,0,0.8)',
'text-halo-width': 1,
}}
/>
)}
{/* Korean ship label — always mounted, visibility으로 제어 */}
<Layer
id="ships-korean-label"
type="symbol"
filter={['==', ['get', 'isKorean'], 1]}
layout={{
'visibility': highlightKorean ? 'visible' : 'none',
'text-field': ['get', 'name'],
'text-size': 9,
'text-offset': [0, 2.2],
'text-anchor': 'top',
'text-allow-overlap': false,
'text-font': ['Open Sans Regular'],
}}
paint={{
'text-color': '#00e5ff',
'text-halo-color': 'rgba(0,0,0,0.8)',
'text-halo-width': 1,
}}
/>
{/* Main ship triangles */}
<Layer
id="ships-triangles"