package com.snp.batch.service; import com.snp.batch.global.model.BatchCollectionPeriod; import com.snp.batch.global.repository.BatchCollectionPeriodRepository; import com.snp.batch.global.repository.BatchLastExecutionRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.scope.context.StepContext; import org.springframework.batch.core.scope.context.StepSynchronizationManager; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; import java.util.Optional; @Slf4j @Service @RequiredArgsConstructor public class BatchDateService { private final BatchLastExecutionRepository repository; private final BatchCollectionPeriodRepository collectionPeriodRepository; /** * 현재 Step의 Job 파라미터에서 executionMode를 확인 */ private String getExecutionMode() { try { StepContext context = StepSynchronizationManager.getContext(); if (context != null && context.getStepExecution() != null) { return context.getStepExecution().getJobExecution() .getJobParameters().getString("executionMode", "NORMAL"); } } catch (Exception e) { log.debug("StepSynchronizationManager 컨텍스트 접근 실패, NORMAL 모드로 처리", e); } return "NORMAL"; } /** * 현재 Step의 Job 파라미터에서 apiKey 파라미터를 확인 (재수집용) */ private String getRecollectApiKey() { try { StepContext context = StepSynchronizationManager.getContext(); if (context != null && context.getStepExecution() != null) { return context.getStepExecution().getJobExecution() .getJobParameters().getString("apiKey"); } } catch (Exception e) { // ignore } return null; } /** * 현재 Step의 Job 파라미터에서 executor를 확인. * AUTO_RETRY/MANUAL_RETRY이면 실패건 재수집이므로 기간 테이블을 사용하지 않는다. */ private boolean isFailedRecordRetry() { try { StepContext context = StepSynchronizationManager.getContext(); if (context != null && context.getStepExecution() != null) { String executor = context.getStepExecution().getJobExecution() .getJobParameters().getString("executor"); return "AUTO_RETRY".equals(executor) || "MANUAL_RETRY".equals(executor); } } catch (Exception e) { log.debug("executor 파라미터 확인 실패", e); } return false; } public Map getDateRangeWithoutTimeParams(String apiKey) { // 기간 재수집 모드: batch_collection_period에서 날짜 조회 // 실패건 재수집(AUTO_RETRY/MANUAL_RETRY)은 정상 모드와 동일하게 last_success_date 기반 사용 if ("RECOLLECT".equals(getExecutionMode()) && !isFailedRecordRetry()) { return getCollectionPeriodDateParams(apiKey); } // 정상 모드: last_success_date ~ now() return repository.findDateRangeByApiKey(apiKey) .map(projection -> { LocalDateTime toDate = LocalDateTime.now(); saveToDateToJobContext(toDate); Map params = new HashMap<>(); putDateParams(params, "from", projection.getLastSuccessDate()); putDateParams(params, "to", toDate); params.put("shipsCategory", "0"); return params; }) .orElseGet(() -> { log.warn("해당 apiKey에 대한 데이터를 찾을 수 없습니다: {}", apiKey); return new HashMap<>(); }); } public Map getDateRangeWithTimezoneParams(String apiKey) { return getDateRangeWithTimezoneParams(apiKey, "fromDate", "toDate"); } public Map getDateRangeWithTimezoneParams(String apiKey, String dateParam1, String dateParam2) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSX"); // 기간 재수집 모드: batch_collection_period에서 날짜 조회 // 실패건 재수집(AUTO_RETRY/MANUAL_RETRY)은 정상 모드와 동일하게 last_success_date 기반 사용 if ("RECOLLECT".equals(getExecutionMode()) && !isFailedRecordRetry()) { return getCollectionPeriodTimezoneParams(apiKey, dateParam1, dateParam2, formatter); } // 정상 모드: last_success_date ~ now() return repository.findDateRangeByApiKey(apiKey) .map(projection -> { LocalDateTime toDate = LocalDateTime.now(); saveToDateToJobContext(toDate); Map params = new HashMap<>(); params.put(dateParam1, formatToUtc(projection.getLastSuccessDate(), formatter)); params.put(dateParam2, formatToUtc(toDate, formatter)); return params; }) .orElseGet(() -> { log.warn("해당 apiKey에 대한 데이터를 찾을 수 없습니다: {}", apiKey); return new HashMap<>(); }); } /** * 재수집 모드: batch_collection_period에서 년/월/일 파라미터 생성 */ private Map getCollectionPeriodDateParams(String apiKey) { String recollectApiKey = getRecollectApiKey(); String lookupKey = recollectApiKey != null ? recollectApiKey : apiKey; Optional opt = collectionPeriodRepository.findById(lookupKey); if (opt.isEmpty()) { log.warn("[RECOLLECT] 수집기간 설정을 찾을 수 없습니다: {}", lookupKey); return new HashMap<>(); } BatchCollectionPeriod cp = opt.get(); Map params = new HashMap<>(); putDateParams(params, "from", cp.getRangeFromDate()); putDateParams(params, "to", cp.getRangeToDate()); params.put("shipsCategory", "0"); log.info("[RECOLLECT] batch_collection_period 날짜 사용: apiKey={}, range={}~{}", lookupKey, cp.getRangeFromDate(), cp.getRangeToDate()); return params; } /** * 재수집 모드: batch_collection_period에서 UTC 타임존 파라미터 생성 */ private Map getCollectionPeriodTimezoneParams( String apiKey, String dateParam1, String dateParam2, DateTimeFormatter formatter) { String recollectApiKey = getRecollectApiKey(); String lookupKey = recollectApiKey != null ? recollectApiKey : apiKey; Optional opt = collectionPeriodRepository.findById(lookupKey); if (opt.isEmpty()) { log.warn("[RECOLLECT] 수집기간 설정을 찾을 수 없습니다: {}", lookupKey); return new HashMap<>(); } BatchCollectionPeriod cp = opt.get(); Map params = new HashMap<>(); params.put(dateParam1, formatToUtc(cp.getRangeFromDate(), formatter)); params.put(dateParam2, formatToUtc(cp.getRangeToDate(), formatter)); log.info("[RECOLLECT] batch_collection_period 날짜 사용 (UTC): apiKey={}, range={}~{}", lookupKey, cp.getRangeFromDate(), cp.getRangeToDate()); return params; } /** * 배치 시작 시 캡처한 toDate를 JobExecutionContext에 저장 * LastExecutionUpdateTasklet에서 이 값을 꺼내 LAST_SUCCESS_DATE로 사용 */ private void saveToDateToJobContext(LocalDateTime toDate) { try { StepContext context = StepSynchronizationManager.getContext(); if (context != null && context.getStepExecution() != null) { context.getStepExecution().getJobExecution() .getExecutionContext().put("batchToDate", toDate.toString()); log.debug("batchToDate JobContext 저장 완료: {}", toDate); } } catch (Exception e) { log.warn("batchToDate JobContext 저장 실패", e); } } /** * LocalDateTime에서 연, 월, 일을 추출하여 Map에 담는 헬퍼 메소드 */ private void putDateParams(Map params, String prefix, LocalDateTime dateTime) { if (dateTime != null) { params.put(prefix + "Year", String.valueOf(dateTime.getYear())); params.put(prefix + "Month", String.valueOf(dateTime.getMonthValue())); params.put(prefix + "Day", String.valueOf(dateTime.getDayOfMonth())); } } /** * 한국 시간(LocalDateTime)을 UTC 문자열로 변환 */ private String formatToUtc(LocalDateTime localDateTime, DateTimeFormatter formatter) { if (localDateTime == null) return null; return localDateTime.atZone(ZoneId.of("Asia/Seoul")) .withZoneSameInstant(ZoneOffset.UTC) .format(formatter); } }