{
return "";
}
+ /**
+ * Reader 상태 초기화
+ * Job 재실행 시 이전 실행의 상태를 클리어하여 새로 데이터를 읽을 수 있도록 함
+ */
+ private void resetReaderState() {
+ // Chunk 모드 상태 초기화
+ this.currentBatch = null;
+ this.initialized = false;
+
+ // Legacy 모드 상태 초기화
+ this.legacyDataList = null;
+ this.legacyNextIndex = 0;
+
+ // 통계 초기화
+ this.totalApiCalls = 0;
+ this.completedApiCalls = 0;
+
+ // 하위 클래스 상태 초기화 훅 호출
+ resetCustomState();
+
+ log.debug("[{}] Reader 상태 초기화 완료", getReaderName());
+ }
+
+ /**
+ * 하위 클래스 커스텀 상태 초기화 훅
+ * Chunk 모드에서 사용하는 currentBatchIndex, allImoNumbers 등의 필드를 초기화할 때 오버라이드
+ *
+ * 예시:
+ *
+ * @Override
+ * protected void resetCustomState() {
+ * this.currentBatchIndex = 0;
+ * this.allImoNumbers = null;
+ * this.dbMasterHashes = null;
+ * }
+ *
+ */
+ protected void resetCustomState() {
+ // 기본 구현: 아무것도 하지 않음
+ // 하위 클래스에서 필요 시 오버라이드
+ }
+
/**
* API 호출 통계 업데이트
*/
@@ -209,21 +429,42 @@ public abstract class BaseApiReader implements ItemReader {
}
// currentBatch가 비어있으면 다음 배치 로드
- if (currentBatch == null || !currentBatch.hasNext()) {
+ /*if (currentBatch == null || !currentBatch.hasNext()) {
List nextBatch = fetchNextBatch();
// 더 이상 데이터가 없으면 종료
- if (nextBatch == null || nextBatch.isEmpty()) {
+// if (nextBatch == null || nextBatch.isEmpty()) {
+ if (nextBatch == null ) {
afterFetch(null);
log.info("[{}] 모든 배치 처리 완료", getReaderName());
return null;
}
+
// Iterator 갱신
+ currentBatch = nextBatch.iterator();
+ log.debug("[{}] 배치 로드 완료: {} 건", getReaderName(), nextBatch.size());
+ }*/
+ // currentBatch가 비어있으면 다음 배치 로드
+ while (currentBatch == null || !currentBatch.hasNext()) {
+ List nextBatch = fetchNextBatch();
+
+ if (nextBatch == null) { // 진짜 종료
+ afterFetch(null);
+ log.info("[{}] 모든 배치 처리 완료", getReaderName());
+ return null;
+ }
+
+ if (nextBatch.isEmpty()) { // emptyList면 다음 batch를 시도
+ log.warn("[{}] 빈 배치 수신 → 다음 배치 재요청", getReaderName());
+ continue; // while 반복문으로 다시 fetch
+ }
+
currentBatch = nextBatch.iterator();
log.debug("[{}] 배치 로드 완료: {} 건", getReaderName(), nextBatch.size());
}
+
// Iterator에서 1건씩 반환
return currentBatch.next();
}
diff --git a/src/main/java/com/snp/batch/common/util/JsonChangeDetector.java b/src/main/java/com/snp/batch/common/util/JsonChangeDetector.java
index 96231f1..41eaf65 100644
--- a/src/main/java/com/snp/batch/common/util/JsonChangeDetector.java
+++ b/src/main/java/com/snp/batch/common/util/JsonChangeDetector.java
@@ -13,14 +13,44 @@ public class JsonChangeDetector {
// 해시 비교에서 제외할 필드 목록 (DataSetVersion 등)
// 이 목록은 모든 JSON 계층에 걸쳐 적용됩니다.
private static final java.util.Set EXCLUDE_KEYS =
- java.util.Set.of("DataSetVersion", "APSStatus", "LastUpdateDate", "LastUpdateDateTime");
+ java.util.Set.of("DataSetVersion", "APSStatus", "LastUpdateDateTime");
- private static final Map LIST_SORT_KEYS = Map.of(
- // List 필드명 // 정렬 기준 키
- "OwnerHistory" ,"Sequence", // OwnerHistory는 Sequence를 기준으로 정렬
- "SurveyDatesHistoryUnique" , "SurveyDate" // SurveyDatesHistoryUnique는 SurveyDate를 기준으로 정렬
- // 추가적인 List/Array 필드가 있다면 여기에 추가
- );
+ // =========================================================================
+ // ✅ LIST_SORT_KEYS: 정적 초기화 블록을 사용한 Map 정의
+ // =========================================================================
+ private static final Map LIST_SORT_KEYS;
+ static {
+ // TreeMap을 사용하여 키를 알파벳 순으로 정렬할 수도 있지만, 여기서는 HashMap을 사용하고 final로 만듭니다.
+ Map map = new HashMap<>();
+ // List 필드명 // 정렬 기준 복합 키 (JSON 필드명, 쉼표로 구분)
+ map.put("OwnerHistory", "OwnerCode,EffectiveDate,Sequence");
+ map.put("CrewList", "LRNO,Shipname,Nationality");
+ map.put("StowageCommodity", "Sequence,CommodityCode,StowageCode");
+ map.put("GroupBeneficialOwnerHistory", "EffectiveDate,GroupBeneficialOwnerCode,Sequence");
+ map.put("ShipManagerHistory", "EffectiveDate,ShipManagerCode,Sequence");
+ map.put("OperatorHistory", "EffectiveDate,OperatorCode,Sequence");
+ map.put("TechnicalManagerHistory", "EffectiveDate,Sequence,TechnicalManagerCode");
+ map.put("BareBoatCharterHistory", "Sequence,EffectiveDate,BBChartererCode");
+ map.put("NameHistory", "Sequence,EffectiveDate");
+ map.put("FlagHistory", "FlagCode,EffectiveDate,Sequence");
+ map.put("PandIHistory", "PandIClubCode,EffectiveDate");
+ map.put("CallSignAndMmsiHistory", "EffectiveDate,SeqNo");
+ map.put("IceClass", "IceClassCode");
+ map.put("SafetyManagementCertificateHistory", "Sequence");
+ map.put("ClassHistory", "ClassCode,EffectiveDate,Sequence");
+ map.put("SurveyDatesHistory", "ClassSocietyCode");
+ map.put("SurveyDatesHistoryUnique", "ClassSocietyCode,SurveyDate,SurveyType");
+ map.put("SisterShipLinks", "LinkedLRNO");
+ map.put("StatusHistory", "Sequence,StatusCode,StatusDate");
+ map.put("SpecialFeature", "Sequence,SpecialFeatureCode");
+ map.put("Thrusters", "Sequence");
+ map.put("DarkActivityConfirmed", "Lrno,Mmsi,Dark_Time,Dark_Status");
+ map.put("CompanyComplianceDetails", "OwCode");
+ map.put("CompanyVesselRelationships", "LRNO");
+ map.put("CompanyDetailsComplexWithCodesAndParent", "OWCODE,LastChangeDate");
+
+ LIST_SORT_KEYS = Collections.unmodifiableMap(map);
+ }
// =========================================================================
// 1. JSON 문자열을 정렬 및 필터링된 Map으로 변환하는 핵심 로직
@@ -90,14 +120,16 @@ public class JsonChangeDetector {
}
}
- // 2. 🔑 List 필드명에 따른 순서 정렬 로직 (추가된 핵심 로직)
+ // 2. 🔑 List 필드명에 따른 복합 순서 정렬 로직 (수정된 핵심 로직)
String listFieldName = entry.getKey();
- String sortKey = LIST_SORT_KEYS.get(listFieldName);
+ String sortKeysString = LIST_SORT_KEYS.get(listFieldName); // 쉼표로 구분된 복합 키 문자열
+
+ if (sortKeysString != null && !filteredList.isEmpty() && filteredList.get(0) instanceof Map) {
+ // 복합 키 문자열을 개별 키 배열로 분리
+ final String[] sortKeys = sortKeysString.split(",");
- if (sortKey != null && !filteredList.isEmpty() && filteredList.get(0) instanceof Map) {
// Map 요소를 가진 리스트인 경우에만 정렬 실행
try {
- // 정렬 기준 키를 사용하여 Comparator를 생성
Collections.sort(filteredList, new Comparator