release: 2026-03-27.3 (7건 커밋) #121
@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2026-03-27.3]
|
||||||
|
|
||||||
|
### 추가
|
||||||
|
- 비정상 궤적 포함 저장 플래그 (`include-abnormal-in-tracks`) — 강화학습 데이터 수집용
|
||||||
|
|
||||||
|
### 수정
|
||||||
|
- REST API 경로 client_id 수집 누락 수정 — JWT 쿠키 파싱 공용 메서드 추출
|
||||||
|
|
||||||
## [2026-03-27.2]
|
## [2026-03-27.2]
|
||||||
|
|
||||||
### 수정
|
### 수정
|
||||||
|
|||||||
@ -62,6 +62,9 @@ public class DailyAggregationStepConfig {
|
|||||||
@Value("${vessel.batch.chunk-size:5000}")
|
@Value("${vessel.batch.chunk-size:5000}")
|
||||||
private int chunkSize;
|
private int chunkSize;
|
||||||
|
|
||||||
|
@Value("${vessel.batch.track.include-abnormal-in-tracks:false}")
|
||||||
|
private boolean includeAbnormalInTracks;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Step mergeDailyTracksStep() {
|
public Step mergeDailyTracksStep() {
|
||||||
log.info("Building mergeDailyTracksStep with cache-based in-memory merge");
|
log.info("Building mergeDailyTracksStep with cache-based in-memory merge");
|
||||||
@ -110,7 +113,9 @@ public class DailyAggregationStepConfig {
|
|||||||
return new CompositeTrackWriter(
|
return new CompositeTrackWriter(
|
||||||
vesselTrackBulkWriter,
|
vesselTrackBulkWriter,
|
||||||
abnormalTrackWriter,
|
abnormalTrackWriter,
|
||||||
"daily"
|
"daily",
|
||||||
|
null,
|
||||||
|
includeAbnormalInTracks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,6 +69,9 @@ public class HourlyAggregationStepConfig {
|
|||||||
@Value("${vessel.batch.chunk-size:5000}")
|
@Value("${vessel.batch.chunk-size:5000}")
|
||||||
private int chunkSize;
|
private int chunkSize;
|
||||||
|
|
||||||
|
@Value("${vessel.batch.track.include-abnormal-in-tracks:false}")
|
||||||
|
private boolean includeAbnormalInTracks;
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// Step 1: 5분 → 시간 병합 (인메모리 캐시 기반)
|
// Step 1: 5분 → 시간 병합 (인메모리 캐시 기반)
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
@ -122,7 +125,8 @@ public class HourlyAggregationStepConfig {
|
|||||||
vesselTrackBulkWriter,
|
vesselTrackBulkWriter,
|
||||||
abnormalTrackWriter,
|
abnormalTrackWriter,
|
||||||
"hourly",
|
"hourly",
|
||||||
hourlyTrackCache
|
hourlyTrackCache,
|
||||||
|
includeAbnormalInTracks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -96,6 +96,9 @@ public class VesselTrackStepConfig {
|
|||||||
@Value("${vessel.batch.chunk-size:1000}")
|
@Value("${vessel.batch.chunk-size:1000}")
|
||||||
private int chunkSize;
|
private int chunkSize;
|
||||||
|
|
||||||
|
@Value("${vessel.batch.track.include-abnormal-in-tracks:false}")
|
||||||
|
private boolean includeAbnormalInTracks;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
// 5분 Job의 이름을 명시적으로 설정
|
// 5분 Job의 이름을 명시적으로 설정
|
||||||
@ -203,11 +206,15 @@ public class VesselTrackStepConfig {
|
|||||||
log.warn("비정상 궤적 감지 [{}]: vessel={}, avg_speed={}, distance={}",
|
log.warn("비정상 궤적 감지 [{}]: vessel={}, avg_speed={}, distance={}",
|
||||||
abnormalReason, track.getVesselKey(), track.getAvgSpeed(), track.getDistanceNm());
|
abnormalReason, track.getVesselKey(), track.getAvgSpeed(), track.getDistanceNm());
|
||||||
saveAbnormalTrack(track, abnormalReason);
|
saveAbnormalTrack(track, abnormalReason);
|
||||||
|
if (includeAbnormalInTracks) {
|
||||||
|
filteredTracks.add(track); // 플래그 true → 정상 테이블+캐시에도 포함
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
filteredTracks.add(track);
|
filteredTracks.add(track);
|
||||||
|
}
|
||||||
|
|
||||||
// 정상 궤적의 종료 위치 저장 (캐시 업데이트용)
|
// 궤적의 종료 위치 저장 (캐시 업데이트용) — 비정상 포함 시에도 위치 추적
|
||||||
if (track.getEndPosition() != null) {
|
if (filteredTracks.contains(track) && track.getEndPosition() != null) {
|
||||||
currentBucketEndPositions.put(track.getMmsi(), VesselBucketPositionDto.builder()
|
currentBucketEndPositions.put(track.getMmsi(), VesselBucketPositionDto.builder()
|
||||||
.mmsi(track.getMmsi())
|
.mmsi(track.getMmsi())
|
||||||
.endLon(track.getEndPosition().getLon())
|
.endLon(track.getEndPosition().getLon())
|
||||||
@ -216,7 +223,6 @@ public class VesselTrackStepConfig {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return filteredTracks.isEmpty() ? null : filteredTracks;
|
return filteredTracks.isEmpty() ? null : filteredTracks;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,21 +25,24 @@ public class CompositeTrackWriter implements ItemWriter<AbnormalDetectionResult>
|
|||||||
private final AbnormalTrackWriter abnormalTrackWriter;
|
private final AbnormalTrackWriter abnormalTrackWriter;
|
||||||
private final String targetTable;
|
private final String targetTable;
|
||||||
private final HourlyTrackCache hourlyTrackCache; // nullable (daily writer는 미사용)
|
private final HourlyTrackCache hourlyTrackCache; // nullable (daily writer는 미사용)
|
||||||
|
private final boolean includeAbnormalInTracks;
|
||||||
|
|
||||||
public CompositeTrackWriter(VesselTrackBulkWriter vesselTrackBulkWriter,
|
public CompositeTrackWriter(VesselTrackBulkWriter vesselTrackBulkWriter,
|
||||||
AbnormalTrackWriter abnormalTrackWriter,
|
AbnormalTrackWriter abnormalTrackWriter,
|
||||||
String targetTable,
|
String targetTable,
|
||||||
HourlyTrackCache hourlyTrackCache) {
|
HourlyTrackCache hourlyTrackCache,
|
||||||
|
boolean includeAbnormalInTracks) {
|
||||||
this.vesselTrackBulkWriter = vesselTrackBulkWriter;
|
this.vesselTrackBulkWriter = vesselTrackBulkWriter;
|
||||||
this.abnormalTrackWriter = abnormalTrackWriter;
|
this.abnormalTrackWriter = abnormalTrackWriter;
|
||||||
this.targetTable = targetTable;
|
this.targetTable = targetTable;
|
||||||
this.hourlyTrackCache = hourlyTrackCache;
|
this.hourlyTrackCache = hourlyTrackCache;
|
||||||
|
this.includeAbnormalInTracks = includeAbnormalInTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompositeTrackWriter(VesselTrackBulkWriter vesselTrackBulkWriter,
|
public CompositeTrackWriter(VesselTrackBulkWriter vesselTrackBulkWriter,
|
||||||
AbnormalTrackWriter abnormalTrackWriter,
|
AbnormalTrackWriter abnormalTrackWriter,
|
||||||
String targetTable) {
|
String targetTable) {
|
||||||
this(vesselTrackBulkWriter, abnormalTrackWriter, targetTable, null);
|
this(vesselTrackBulkWriter, abnormalTrackWriter, targetTable, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeStep
|
@BeforeStep
|
||||||
@ -66,9 +69,11 @@ public class CompositeTrackWriter implements ItemWriter<AbnormalDetectionResult>
|
|||||||
abnormalResults.add(result);
|
abnormalResults.add(result);
|
||||||
|
|
||||||
// 정정된 궤적이 있으면 정상 궤적으로 저장
|
// 정정된 궤적이 있으면 정상 궤적으로 저장
|
||||||
// null이면 전체 궤적이 비정상이므로 제외
|
// null이면 전체 궤적이 비정상이므로 제외 (플래그 true면 원본 포함)
|
||||||
if (result.getCorrectedTrack() != null) {
|
if (result.getCorrectedTrack() != null) {
|
||||||
normalTracks.add(result.getCorrectedTrack());
|
normalTracks.add(result.getCorrectedTrack());
|
||||||
|
} else if (includeAbnormalInTracks) {
|
||||||
|
normalTracks.add(result.getOriginalTrack());
|
||||||
} else {
|
} else {
|
||||||
log.debug("비정상 궤적 전체 제외: vessel={}",
|
log.debug("비정상 궤적 전체 제외: vessel={}",
|
||||||
result.getOriginalTrack().getVesselKey());
|
result.getOriginalTrack().getVesselKey());
|
||||||
|
|||||||
@ -191,7 +191,11 @@ public class GisControllerV2 {
|
|||||||
)
|
)
|
||||||
@RequestBody VesselTracksRequest request,
|
@RequestBody VesselTracksRequest request,
|
||||||
HttpServletRequest httpRequest) {
|
HttpServletRequest httpRequest) {
|
||||||
return gisServiceV2.getVesselTracksV2(request, getClientIp(httpRequest));
|
return gisServiceV2.getVesselTracksV2(request, getClientIp(httpRequest), getClientId(httpRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClientId(HttpServletRequest request) {
|
||||||
|
return gc.mda.signal_batch.global.config.WebSocketStompConfig.extractClientIdFromRequest(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getClientIp(HttpServletRequest request) {
|
private String getClientIp(HttpServletRequest request) {
|
||||||
|
|||||||
@ -285,7 +285,7 @@ public class GisServiceV2 {
|
|||||||
/**
|
/**
|
||||||
* 선박별 항적 조회 V2 (캐시 + Semaphore + 간소화 + ChnPrmShip enrichment)
|
* 선박별 항적 조회 V2 (캐시 + Semaphore + 간소화 + ChnPrmShip enrichment)
|
||||||
*/
|
*/
|
||||||
public List<CompactVesselTrack> getVesselTracksV2(VesselTracksRequest request, String clientIp) {
|
public List<CompactVesselTrack> getVesselTracksV2(VesselTracksRequest request, String clientIp, String clientId) {
|
||||||
String queryId = "rest-vessels-" + UUID.randomUUID().toString().substring(0, 8);
|
String queryId = "rest-vessels-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
long startMs = System.currentTimeMillis();
|
long startMs = System.currentTimeMillis();
|
||||||
boolean slotAcquired = false;
|
boolean slotAcquired = false;
|
||||||
@ -329,7 +329,7 @@ public class GisServiceV2 {
|
|||||||
result.size(), request.getVessels().size(),
|
result.size(), request.getVessels().size(),
|
||||||
dailyTrackCacheManager.isEnabled(), request.isIncludeChnPrmShip());
|
dailyTrackCacheManager.isEnabled(), request.isIncludeChnPrmShip());
|
||||||
|
|
||||||
enqueueRestMetric(queryId, request, result, startMs, clientIp);
|
enqueueRestMetric(queryId, request, result, startMs, clientIp, clientId);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
@ -347,7 +347,7 @@ public class GisServiceV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void enqueueRestMetric(String queryId, VesselTracksRequest request,
|
private void enqueueRestMetric(String queryId, VesselTracksRequest request,
|
||||||
List<CompactVesselTrack> result, long startMs, String clientIp) {
|
List<CompactVesselTrack> result, long startMs, String clientIp, String clientId) {
|
||||||
try {
|
try {
|
||||||
int totalPoints = result.stream().mapToInt(CompactVesselTrack::getPointCount).sum();
|
int totalPoints = result.stream().mapToInt(CompactVesselTrack::getPointCount).sum();
|
||||||
long responseBytes = (long) result.size() * 200 + (long) totalPoints * 40;
|
long responseBytes = (long) result.size() * 200 + (long) totalPoints * 40;
|
||||||
@ -367,6 +367,7 @@ public class GisServiceV2 {
|
|||||||
.elapsedMs(System.currentTimeMillis() - startMs)
|
.elapsedMs(System.currentTimeMillis() - startMs)
|
||||||
.status("COMPLETED")
|
.status("COMPLETED")
|
||||||
.clientIp(clientIp)
|
.clientIp(clientIp)
|
||||||
|
.clientId(clientId)
|
||||||
.build());
|
.build());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Failed to enqueue REST metric: {}", e.getMessage());
|
log.debug("Failed to enqueue REST metric: {}", e.getMessage());
|
||||||
|
|||||||
@ -235,11 +235,16 @@ public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractEmailFromJwtCookie(HttpServletRequest request) {
|
||||||
|
return extractClientIdFromRequest(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GC_SESSION 쿠키에서 JWT payload의 email 클레임 추출.
|
* GC_SESSION 쿠키에서 JWT payload의 email 클레임 추출 (REST/WebSocket 공용).
|
||||||
* JWT 검증은 nginx auth_request에서 이미 완료 — 여기서는 payload 디코딩만 수행.
|
* JWT 검증은 nginx auth_request에서 이미 완료 — 여기서는 payload 디코딩만 수행.
|
||||||
*/
|
*/
|
||||||
private String extractEmailFromJwtCookie(HttpServletRequest request) {
|
public static String extractClientIdFromRequest(HttpServletRequest request) {
|
||||||
Cookie[] cookies = request.getCookies();
|
Cookie[] cookies = request.getCookies();
|
||||||
if (cookies == null) return null;
|
if (cookies == null) return null;
|
||||||
|
|
||||||
@ -253,12 +258,10 @@ public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
if (token == null || token.isEmpty()) return null;
|
if (token == null || token.isEmpty()) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// JWT: header.payload.signature — payload만 Base64URL 디코딩
|
|
||||||
String[] parts = token.split("\\.");
|
String[] parts = token.split("\\.");
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
|
|
||||||
String payload = new String(Base64.getUrlDecoder().decode(parts[1]));
|
String payload = new String(Base64.getUrlDecoder().decode(parts[1]));
|
||||||
// 간단한 JSON 파싱 (Jackson 의존 없이): "email":"value" 추출
|
|
||||||
int emailIdx = payload.indexOf("\"email\"");
|
int emailIdx = payload.indexOf("\"email\"");
|
||||||
if (emailIdx < 0) return null;
|
if (emailIdx < 0) return null;
|
||||||
|
|
||||||
@ -272,5 +275,4 @@ public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -173,6 +173,7 @@ vessel: # spring 하위가 아닌 최상위 레벨
|
|||||||
|
|
||||||
# 궤적 비정상 검출 설정
|
# 궤적 비정상 검출 설정
|
||||||
track:
|
track:
|
||||||
|
include-abnormal-in-tracks: true # 비정상 궤적도 정상 테이블+캐시에 포함 (강화학습 데이터 수집용)
|
||||||
abnormal-detection:
|
abnormal-detection:
|
||||||
large-gap-threshold-hours: 4 # 이 시간 이상 gap은 연결 안함
|
large-gap-threshold-hours: 4 # 이 시간 이상 gap은 연결 안함
|
||||||
extreme-speed-threshold: 1000 # 이 속도 이상은 무조건 비정상 (knots)
|
extreme-speed-threshold: 1000 # 이 속도 이상은 무조건 비정상 (knots)
|
||||||
|
|||||||
@ -159,6 +159,8 @@ vessel:
|
|||||||
page-size: ${BATCH_PAGE_SIZE:10000}
|
page-size: ${BATCH_PAGE_SIZE:10000}
|
||||||
partition-size: ${BATCH_PARTITION_SIZE:24}
|
partition-size: ${BATCH_PARTITION_SIZE:24}
|
||||||
skip-limit: 100
|
skip-limit: 100
|
||||||
|
track:
|
||||||
|
include-abnormal-in-tracks: false # true: 비정상 궤적도 정상 테이블+캐시에 포함 (강화학습 데이터 수집용)
|
||||||
retry-limit: 3
|
retry-limit: 3
|
||||||
# Reader 설정
|
# Reader 설정
|
||||||
use-cursor-reader: true # Cursor Reader 사용 여부
|
use-cursor-reader: true # Cursor Reader 사용 여부
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user