fix: ChnPrmShip 캐시 갱신 조건 완화 및 스케줄 이전 실행 시간 표시 #3
@ -84,6 +84,14 @@ public class AisTargetEntity extends BaseEntity {
|
|||||||
private OffsetDateTime receivedDate;
|
private OffsetDateTime receivedDate;
|
||||||
private OffsetDateTime collectedAt; // 배치 수집 시점
|
private OffsetDateTime collectedAt; // 배치 수집 시점
|
||||||
|
|
||||||
|
// ========== 선종 분류 정보 ==========
|
||||||
|
/**
|
||||||
|
* MDA 범례코드 (signalKindCode)
|
||||||
|
* - vesselType + extraInfo 기반으로 치환
|
||||||
|
* - 예: "000020"(어선), "000023"(카고), "000027"(일반/기타)
|
||||||
|
*/
|
||||||
|
private String signalKindCode;
|
||||||
|
|
||||||
// ========== ClassType 분류 정보 ==========
|
// ========== ClassType 분류 정보 ==========
|
||||||
/**
|
/**
|
||||||
* 선박 클래스 타입
|
* 선박 클래스 타입
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.snp.batch.common.batch.writer.BaseWriter;
|
|||||||
import com.snp.batch.jobs.aistarget.batch.entity.AisTargetEntity;
|
import com.snp.batch.jobs.aistarget.batch.entity.AisTargetEntity;
|
||||||
import com.snp.batch.jobs.aistarget.cache.AisTargetCacheManager;
|
import com.snp.batch.jobs.aistarget.cache.AisTargetCacheManager;
|
||||||
import com.snp.batch.jobs.aistarget.classifier.AisClassTypeClassifier;
|
import com.snp.batch.jobs.aistarget.classifier.AisClassTypeClassifier;
|
||||||
|
import com.snp.batch.jobs.aistarget.classifier.SignalKindCode;
|
||||||
import com.snp.batch.jobs.aistarget.kafka.AisTargetKafkaProducer;
|
import com.snp.batch.jobs.aistarget.kafka.AisTargetKafkaProducer;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -15,8 +16,9 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* 동작:
|
* 동작:
|
||||||
* 1. ClassType 분류 (Core20 캐시 기반 A/B 분류)
|
* 1. ClassType 분류 (Core20 캐시 기반 A/B 분류)
|
||||||
* 2. 캐시에 최신 위치 정보 업데이트 (classType, core20Mmsi 포함)
|
* 2. SignalKindCode 치환 (vesselType + extraInfo → MDA 범례코드)
|
||||||
* 3. Kafka 토픽으로 AIS Target 정보 전송 (서브청크 분할)
|
* 3. 캐시에 최신 위치 정보 업데이트 (classType, core20Mmsi, signalKindCode 포함)
|
||||||
|
* 4. Kafka 토픽으로 AIS Target 정보 전송 (서브청크 분할)
|
||||||
*
|
*
|
||||||
* 참고:
|
* 참고:
|
||||||
* - DB 저장은 별도 Job(aisTargetDbSyncJob)에서 15분 주기로 수행
|
* - DB 저장은 별도 Job(aisTargetDbSyncJob)에서 15분 주기로 수행
|
||||||
@ -48,13 +50,19 @@ public class AisTargetDataWriter extends BaseWriter<AisTargetEntity> {
|
|||||||
// - Core20 캐시의 IMO와 매칭하여 classType(A/B), core20Mmsi 설정
|
// - Core20 캐시의 IMO와 매칭하여 classType(A/B), core20Mmsi 설정
|
||||||
classTypeClassifier.classifyAll(items);
|
classTypeClassifier.classifyAll(items);
|
||||||
|
|
||||||
// 2. 캐시 업데이트 (classType, core20Mmsi 포함)
|
// 2. SignalKindCode 치환 (vesselType + extraInfo → MDA 범례코드)
|
||||||
|
items.forEach(item -> {
|
||||||
|
SignalKindCode kindCode = SignalKindCode.resolve(item.getVesselType(), item.getExtraInfo());
|
||||||
|
item.setSignalKindCode(kindCode.getCode());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 캐시 업데이트 (classType, core20Mmsi, signalKindCode 포함)
|
||||||
cacheManager.putAll(items);
|
cacheManager.putAll(items);
|
||||||
|
|
||||||
log.debug("AIS Target 캐시 업데이트 완료: {} 건 (캐시 크기: {})",
|
log.debug("AIS Target 캐시 업데이트 완료: {} 건 (캐시 크기: {})",
|
||||||
items.size(), cacheManager.size());
|
items.size(), cacheManager.size());
|
||||||
|
|
||||||
// 3. Kafka 전송 (설정 enabled=true 인 경우)
|
// 4. Kafka 전송 (설정 enabled=true 인 경우)
|
||||||
if (!kafkaProducer.isEnabled()) {
|
if (!kafkaProducer.isEnabled()) {
|
||||||
log.debug("AIS Kafka 전송 비활성화 - topic 전송 스킵");
|
log.debug("AIS Kafka 전송 비활성화 - topic 전송 스킵");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -0,0 +1,118 @@
|
|||||||
|
package com.snp.batch.jobs.aistarget.classifier;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MDA 선종 범례코드
|
||||||
|
*
|
||||||
|
* GlobalAIS 원본 데이터의 vesselType + extraInfo를 기반으로
|
||||||
|
* MDA 범례코드(signalKindCode)로 치환한다.
|
||||||
|
*
|
||||||
|
* @see <a href="GLOBALAIS - MDA 선종 범례 치환표.pdf">치환 규칙표</a>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum SignalKindCode {
|
||||||
|
|
||||||
|
FISHING("000020", "어선"),
|
||||||
|
KCGV("000021", "함정"),
|
||||||
|
FERRY("000022", "여객선"),
|
||||||
|
CARGO("000023", "카고"),
|
||||||
|
TANKER("000024", "탱커"),
|
||||||
|
GOV("000025", "관공선"),
|
||||||
|
DEFAULT("000027", "일반/기타선박"),
|
||||||
|
BUOY("000028", "부이/항로표지");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String koreanName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlobalAIS vesselType + extraInfo → MDA 범례코드 치환
|
||||||
|
*
|
||||||
|
* 치환 우선순위:
|
||||||
|
* 1. vesselType 단독 매칭 (Cargo, Tanker, Passenger, AtoN 등)
|
||||||
|
* 2. vesselType + extraInfo 조합 매칭 (Vessel + Fishing 등)
|
||||||
|
* 3. fallback → DEFAULT (000027)
|
||||||
|
*/
|
||||||
|
public static SignalKindCode resolve(String vesselType, String extraInfo) {
|
||||||
|
String vt = normalizeOrEmpty(vesselType);
|
||||||
|
String ei = normalizeOrEmpty(extraInfo);
|
||||||
|
|
||||||
|
// 1. vesselType 단독 매칭 (extraInfo 무관)
|
||||||
|
switch (vt) {
|
||||||
|
case "cargo":
|
||||||
|
return CARGO;
|
||||||
|
case "tanker":
|
||||||
|
return TANKER;
|
||||||
|
case "passenger":
|
||||||
|
return FERRY;
|
||||||
|
case "aton":
|
||||||
|
return BUOY;
|
||||||
|
case "law enforcement":
|
||||||
|
return GOV;
|
||||||
|
case "search and rescue":
|
||||||
|
return KCGV;
|
||||||
|
case "local vessel":
|
||||||
|
return FISHING;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// vesselType 그룹 매칭 (복합 선종명)
|
||||||
|
if (matchesAny(vt, "tug", "pilot boat", "tender", "anti pollution", "medical transport")) {
|
||||||
|
return GOV;
|
||||||
|
}
|
||||||
|
if (matchesAny(vt, "high speed craft", "wing in ground-effect")) {
|
||||||
|
return FERRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. "Vessel" + extraInfo 조합
|
||||||
|
if ("vessel".equals(vt)) {
|
||||||
|
return resolveVesselExtraInfo(ei);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. "N/A" + extraInfo 조합
|
||||||
|
if ("n/a".equals(vt)) {
|
||||||
|
if (ei.startsWith("hazardous cat")) {
|
||||||
|
return CARGO;
|
||||||
|
}
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. fallback
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SignalKindCode resolveVesselExtraInfo(String extraInfo) {
|
||||||
|
if ("fishing".equals(extraInfo)) {
|
||||||
|
return FISHING;
|
||||||
|
}
|
||||||
|
if ("military operations".equals(extraInfo)) {
|
||||||
|
return GOV;
|
||||||
|
}
|
||||||
|
if (matchesAny(extraInfo, "towing", "towing (large)", "dredging/underwater ops", "diving operations")) {
|
||||||
|
return GOV;
|
||||||
|
}
|
||||||
|
if (matchesAny(extraInfo, "pleasure craft", "sailing", "n/a")) {
|
||||||
|
return FISHING;
|
||||||
|
}
|
||||||
|
if (extraInfo.startsWith("hazardous cat")) {
|
||||||
|
return CARGO;
|
||||||
|
}
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesAny(String value, String... candidates) {
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
if (candidate.equals(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeOrEmpty(String value) {
|
||||||
|
return (value == null || value.isBlank()) ? "" : value.strip().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,6 +56,21 @@ public class AisTargetResponseDto {
|
|||||||
@Schema(description = "데이터 소스", example = "cache", allowableValues = {"cache", "db"})
|
@Schema(description = "데이터 소스", example = "cache", allowableValues = {"cache", "db"})
|
||||||
private String source;
|
private String source;
|
||||||
|
|
||||||
|
// 선종 분류 정보
|
||||||
|
@Schema(description = """
|
||||||
|
MDA 범례코드 (선종 분류)
|
||||||
|
- 000020: 어선 (FISHING)
|
||||||
|
- 000021: 함정 (KCGV)
|
||||||
|
- 000022: 여객선 (FERRY)
|
||||||
|
- 000023: 카고 (CARGO)
|
||||||
|
- 000024: 탱커 (TANKER)
|
||||||
|
- 000025: 관공선 (GOV)
|
||||||
|
- 000027: 일반/기타선박 (DEFAULT)
|
||||||
|
- 000028: 부이/항로표지 (BUOY)
|
||||||
|
""",
|
||||||
|
example = "000023")
|
||||||
|
private String signalKindCode;
|
||||||
|
|
||||||
// ClassType 분류 정보
|
// ClassType 분류 정보
|
||||||
@Schema(description = """
|
@Schema(description = """
|
||||||
선박 클래스 타입
|
선박 클래스 타입
|
||||||
@ -102,6 +117,7 @@ public class AisTargetResponseDto {
|
|||||||
.messageTimestamp(entity.getMessageTimestamp())
|
.messageTimestamp(entity.getMessageTimestamp())
|
||||||
.receivedDate(entity.getReceivedDate())
|
.receivedDate(entity.getReceivedDate())
|
||||||
.source(source)
|
.source(source)
|
||||||
|
.signalKindCode(entity.getSignalKindCode())
|
||||||
.classType(entity.getClassType())
|
.classType(entity.getClassType())
|
||||||
.core20Mmsi(entity.getCore20Mmsi())
|
.core20Mmsi(entity.getCore20Mmsi())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user