Merge pull request 'release: 2026-03-19 (5건 커밋)' (#72) from develop into main
All checks were successful
Deploy KCG / deploy (push) Successful in 1m20s

This commit is contained in:
htlee 2026-03-19 10:24:59 +09:00
커밋 a96103e639
4개의 변경된 파일95개의 추가작업 그리고 92개의 파일을 삭제

파일 보기

@ -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,77 +4,14 @@
## [Unreleased]
## [2026-03-18.5]
### 추가
- 지진 포인트 클릭 → 지도 flyTo + SeismicMarker 진도별 펄스 원형 영향범위 표시
- SatelliteMap flyTo 지원
## [2026-03-19]
### 변경
- 히스토리 프리셋: 10M/30M/1H/3H/6H → 1H/2H/3H/6H (최소 1시간)
- 기압 그래프: 해수면 기압 보정(SLP), 원본 포인트 기반 렌더링
- 그래프 데이터 범위: 표시 범위보다 1칸 확장 (y축 시작점 연결)
- Tooltip: KST 시간 포맷, 상단 고정, 전체 스타일 통일
- OilFacilityLayer: planned ring SVG 내부 이동 (아이콘 중심 정렬)
- 밝은 테마: 지도 라벨 text-shadow CSS 변수 분리
- 인라인 CSS 정리 — 공통 클래스 추출 + Tailwind 전환
### 수정
- deploy.yml: SSH SCP+실행 각 3회 재시도 (kex_exchange 거부 대응)
## [2026-03-18.4]
### 추가
- 한국 선박 현황 헤더 ON/OFF 토글 → 지도 강조 링+라벨 표시 (기본 ON)
- 우측 패널 한국 선박 목록: hover 시 지도 강조 링, 클릭 시 선박 모달 호출
### 변경
- 지진파 그래프: LineChart → ScatterChart (진도별 색상/크기, 이벤트 점 표시)
- 기압 그래프: 버킷 평균 → 관측소별 개별 라인 (데이터 없는 구간 0 제거)
## [2026-03-18.3]
### 추가
- 센서 API 서비스(sensorApi.ts): 백엔드 지진/기압 실데이터 연동
- 선박 모달 S&P Global 다중 사진 슬라이드 (좌우 화살표 + 인디케이터)
- 선박 모달 드래그 이동 (헤더 영역 grab)
- LiveControls KST/UTC 라디오 버튼 그룹
### 변경
- SensorChart: 더미 → 실데이터(지진/기압), x축 동적 시간 표시
- 히스토리 프리셋: 30M/1H/3H/6H/12H/24H → 10M/30M/1H/3H/6H (8칸 구조)
- 센서 API 파라미터: hours → min (기본 2880=48h)
- 센서 데이터 polling: 초기 48h 전체 → 10분마다 incremental merge
- 선박 데이터 polling: 초기 60분 → 5분마다 6분 윈도우 merge + 60분 stale 제거
- 선박 모달 고정 크기(300px) + 사진 영역 고정(160px, object-contain)
- 선박 모달 데이터 레이아웃: 2컬럼 그리드 + 연관 정보 쌍 배치 + 긴 값 단독행
- 선박 모달 CSS 통일 (태그 패딩/배경, 컬럼 간격 12px)
### 수정
- 센서 API(/api/sensor/*) 인증 예외 처리 (공개 데이터)
- 선박 모달 열 때마다 S&P Global 우선 탭 리셋 (MarineTraffic 포커스 유지 버그)
- S&P Global 사진 URL: IMO 기반 이미지 목록 API 연동 (잘못된 번호 패턴 제거)
### 기타
- 로그인 화면 KCG 로고에 DEMO 문구 오버레이
## [2026-03-18.2]
### 추가
- 지진파 수집기: USGS FDSN API, 이란 bbox(M2+), 5분 주기
- 기압 수집기: Open-Meteo API, 이란 5개 관측점, 10분 주기
- DB: seismic_events, pressure_readings 테이블 (마이그레이션 004)
- REST: GET /api/sensor/seismic, GET /api/sensor/pressure
### 변경
- 프론트엔드 패키지 구조 리팩토링: components/ → common/layers/iran/korea/ 분리
- App.tsx 분해: 1,179줄 → 588줄 (useIranData, useKoreaData, useKoreaFilters 훅 추출)
- SensorChart 그래프 순서: 지진파 → 기압 → 소음(DEMO) → 방사선(DEMO)
- 선박 모달 사진 탭: S&P Global 명칭, 고화질(_2) 기본 표시
- Overpass API 외부 호출 제거 → 정적 인프라 데이터
### 수정
- LiveControls KST 시간 이중 오프셋(+9h×2) 수정 + KST/UTC 토글
- nginx /shipimg/ 프록시: ^~ 추가 (정적파일 regex 우선매칭 방지)
- OSINT 기사 중복 수집 방지: title 기반 24h 중복 체크 추가 (GDELT/Google News)
- MapLibre symbol layer race condition 해소: hover를 feature-state로 분리, ships-korean-label visibility 제어로 변경
## [2026-03-18]
@ -85,9 +22,51 @@
- Caffeine 캐시 TTL 2일 (Aircraft 포함 전체 통일)
- DB 마이그레이션: `ship_positions`, `osint_feeds`, `satellite_tle` 테이블 + 샘플 데이터
- 프론트엔드 OSINT/위성 데이터 백엔드 API 우선 호출 + 직접 호출 fallback
- 지진파 수집기: USGS FDSN API, 이란 bbox(M2+), 5분 주기
- 기압 수집기: Open-Meteo API, 이란 5개 관측점, 10분 주기
- DB: seismic_events, pressure_readings 테이블 (마이그레이션 004)
- REST: GET /api/sensor/seismic, GET /api/sensor/pressure
- 센서 API 서비스(sensorApi.ts): 백엔드 지진/기압 실데이터 연동
- 선박 모달 S&P Global 다중 사진 슬라이드 (좌우 화살표 + 인디케이터)
- 선박 모달 드래그 이동 (헤더 영역 grab)
- LiveControls KST/UTC 라디오 버튼 그룹
- 한국 선박 현황 헤더 ON/OFF 토글 → 지도 강조 링+라벨 표시 (기본 ON)
- 우측 패널 한국 선박 목록: hover 시 지도 강조 링, 클릭 시 선박 모달 호출
- 지진 포인트 클릭 → 지도 flyTo + SeismicMarker 진도별 펄스 원형 영향범위 표시
- SatelliteMap flyTo 지원
### 변경
- 프론트엔드 패키지 구조 리팩토링: components/ → common/layers/iran/korea/ 분리
- App.tsx 분해: 1,179줄 → 588줄 (useIranData, useKoreaData, useKoreaFilters 훅 추출)
- SensorChart: 더미 → 실데이터(지진/기압), x축 동적 시간 표시
- SensorChart 그래프 순서: 지진파 → 기압 → 소음(DEMO) → 방사선(DEMO)
- 히스토리 프리셋: 1H/2H/3H/6H (최소 1시간, 8칸 구조)
- 센서 API 파라미터: hours → min (기본 2880=48h)
- 센서 데이터 polling: 초기 48h 전체 → 10분마다 incremental merge
- 선박 데이터 polling: 초기 60분 → 5분마다 6분 윈도우 merge + 60분 stale 제거
- 선박 모달 고정 크기(300px) + 사진 영역 고정(160px, object-contain)
- 선박 모달 데이터 레이아웃: 2컬럼 그리드 + 연관 정보 쌍 배치 + 긴 값 단독행
- 선박 모달 CSS 통일 (태그 패딩/배경, 컬럼 간격 12px)
- 선박 모달 사진 탭: S&P Global 명칭, 고화질(_2) 기본 표시
- Overpass API 외부 호출 제거 → 정적 인프라 데이터
- 지진파 그래프: LineChart → ScatterChart (진도별 색상/크기, 이벤트 점 표시)
- 기압 그래프: 해수면 기압 보정(SLP), 관측소별 개별 라인, 원본 포인트 기반 렌더링
- 그래프 데이터 범위: 표시 범위보다 1칸 확장 (y축 시작점 연결)
- Tooltip: KST 시간 포맷, 상단 고정, 전체 스타일 통일
- OilFacilityLayer: planned ring SVG 내부 이동 (아이콘 중심 정렬)
- 밝은 테마: 지도 라벨 text-shadow CSS 변수 분리
### 수정
- 002 마이그레이션 search_path에 public 추가 (PostGIS 타입 참조)
- LiveControls KST 시간 이중 오프셋(+9h×2) 수정 + KST/UTC 토글
- nginx /shipimg/ 프록시: ^~ 추가 (정적파일 regex 우선매칭 방지)
- 센서 API(/api/sensor/*) 인증 예외 처리 (공개 데이터)
- 선박 모달 열 때마다 S&P Global 우선 탭 리셋 (MarineTraffic 포커스 유지 버그)
- S&P Global 사진 URL: IMO 기반 이미지 목록 API 연동 (잘못된 번호 패턴 제거)
- deploy.yml: SSH SCP+실행 각 3회 재시도 (kex_exchange 거부 대응)
### 기타
- 로그인 화면 KCG 로고에 DEMO 문구 오버레이
## [2026-03-17]

파일 보기

@ -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"