Risk&Compliance Range Import Update

This commit is contained in:
hyojin kim 2025-12-24 14:15:13 +09:00
부모 5683000024
커밋 fcf1d74c38
39개의 변경된 파일205개의 추가작업 그리고 2344개의 파일을 삭제

파일 보기

@ -1,11 +1,11 @@
package com.snp.batch.jobs.sanction.batch.config; package com.snp.batch.jobs.compliance.batch.config;
import com.snp.batch.common.batch.config.BaseJobConfig; import com.snp.batch.common.batch.config.BaseJobConfig;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceDto; import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto;
import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity; import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity;
import com.snp.batch.jobs.sanction.batch.processor.ComplianceDataProcessor; import com.snp.batch.jobs.compliance.batch.processor.ComplianceDataProcessor;
import com.snp.batch.jobs.sanction.batch.reader.ComplianceDataReader; import com.snp.batch.jobs.compliance.batch.reader.ComplianceDataReader;
import com.snp.batch.jobs.sanction.batch.writer.ComplianceDataWriter; import com.snp.batch.jobs.compliance.batch.writer.ComplianceDataWriter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job; import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
@ -22,7 +22,7 @@ import org.springframework.web.reactive.function.client.WebClient;
@Slf4j @Slf4j
@Configuration @Configuration
public class SanctionUpdateJobConfig extends BaseJobConfig<ComplianceDto, ComplianceEntity> { public class ComplianceImportJobConfig extends BaseJobConfig<ComplianceDto, ComplianceEntity> {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final WebClient maritimeServiceApiWebClient; private final WebClient maritimeServiceApiWebClient;
@ -34,7 +34,7 @@ public class SanctionUpdateJobConfig extends BaseJobConfig<ComplianceDto, Compli
protected int getChunkSize() { protected int getChunkSize() {
return 5000; // API에서 5000개씩 가져오므로 chunk도 5000으로 설정 return 5000; // API에서 5000개씩 가져오므로 chunk도 5000으로 설정
} }
public SanctionUpdateJobConfig( public ComplianceImportJobConfig(
JobRepository jobRepository, JobRepository jobRepository,
PlatformTransactionManager transactionManager, PlatformTransactionManager transactionManager,
ComplianceDataProcessor complianceDataProcessor, ComplianceDataProcessor complianceDataProcessor,
@ -50,12 +50,12 @@ public class SanctionUpdateJobConfig extends BaseJobConfig<ComplianceDto, Compli
@Override @Override
protected String getJobName() { protected String getJobName() {
return "sanctionUpdateJob"; return "ComplianceImportJob";
} }
@Override @Override
protected String getStepName() { protected String getStepName() {
return "sanctionUpdateStep"; return "ComplianceImportStep";
} }
@Override @Override
@ -73,13 +73,13 @@ public class SanctionUpdateJobConfig extends BaseJobConfig<ComplianceDto, Compli
return complianceDataWriter; return complianceDataWriter;
} }
@Bean(name = "sanctionUpdateJob") @Bean(name = "ComplianceImportJob")
public Job sanctionUpdateJob() { public Job complianceImportJob() {
return job(); return job();
} }
@Bean(name = "sanctionUpdateStep") @Bean(name = "ComplianceImportStep")
public Step sanctionUpdateStep() { public Step complianceImportStep() {
return step(); return step();
} }

파일 보기

@ -1,13 +1,12 @@
package com.snp.batch.jobs.sanction.batch.config; package com.snp.batch.jobs.compliance.batch.config;
import com.snp.batch.common.batch.config.BaseJobConfig; import com.snp.batch.common.batch.config.BaseJobConfig;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceDto; import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto;
import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity; import com.snp.batch.jobs.compliance.batch.processor.ComplianceDataProcessor;
import com.snp.batch.jobs.sanction.batch.processor.ComplianceDataProcessor; import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity;
import com.snp.batch.jobs.sanction.batch.reader.ComplianceDataRangeReader; import com.snp.batch.jobs.compliance.batch.reader.ComplianceDataRangeReader;
import com.snp.batch.jobs.sanction.batch.reader.ComplianceDataReader; import com.snp.batch.jobs.compliance.batch.writer.ComplianceDataWriter;
import com.snp.batch.jobs.sanction.batch.writer.ComplianceDataWriter; import com.snp.batch.service.BatchDateService;
import com.snp.batch.jobs.shipMovementAnchorageCalls.batch.reader.AnchorageCallsRangeReader;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job; import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
@ -26,40 +25,44 @@ import org.springframework.web.reactive.function.client.WebClient;
@Slf4j @Slf4j
@Configuration @Configuration
public class SanctionUpdateRangeJobConfig extends BaseJobConfig<ComplianceDto, ComplianceEntity> { public class ComplianceImportRangeJobConfig extends BaseJobConfig<ComplianceDto, ComplianceEntity> {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final WebClient maritimeServiceApiWebClient; private final WebClient maritimeServiceApiWebClient;
private final ComplianceDataProcessor complianceDataProcessor; private final ComplianceDataProcessor complianceDataProcessor;
private final ComplianceDataWriter complianceDataWriter; private final ComplianceDataWriter complianceDataWriter;
private final ComplianceDataRangeReader complianceDataRangeReader; private final ComplianceDataRangeReader complianceDataRangeReader;
private final BatchDateService batchDateService;
@Override @Override
protected int getChunkSize() { protected int getChunkSize() {
return 5000; // API에서 5000개씩 가져오므로 chunk도 5000으로 설정 return 10000;
} }
public SanctionUpdateRangeJobConfig( public ComplianceImportRangeJobConfig(
JobRepository jobRepository, JobRepository jobRepository,
PlatformTransactionManager transactionManager, PlatformTransactionManager transactionManager,
ComplianceDataProcessor complianceDataProcessor, ComplianceDataProcessor complianceDataProcessor,
ComplianceDataWriter complianceDataWriter, ComplianceDataWriter complianceDataWriter,
JdbcTemplate jdbcTemplate, JdbcTemplate jdbcTemplate,
@Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient, ComplianceDataRangeReader complianceDataRangeReader) { @Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient,
ComplianceDataRangeReader complianceDataRangeReader,
BatchDateService batchDateService) {
super(jobRepository, transactionManager); super(jobRepository, transactionManager);
this.jdbcTemplate = jdbcTemplate; this.jdbcTemplate = jdbcTemplate;
this.maritimeServiceApiWebClient = maritimeServiceApiWebClient; this.maritimeServiceApiWebClient = maritimeServiceApiWebClient;
this.complianceDataProcessor = complianceDataProcessor; this.complianceDataProcessor = complianceDataProcessor;
this.complianceDataWriter = complianceDataWriter; this.complianceDataWriter = complianceDataWriter;
this.complianceDataRangeReader = complianceDataRangeReader; this.complianceDataRangeReader = complianceDataRangeReader;
this.batchDateService = batchDateService;
} }
@Override @Override
protected String getJobName() { protected String getJobName() {
return "SanctionRangeUpdateJob"; return "ComplianceImportRangeJob";
} }
@Override @Override
protected String getStepName() { protected String getStepName() {
return "SanctionRangeUpdateStep"; return "ComplianceImportRangeStep";
} }
@Override @Override
@ -69,11 +72,8 @@ public class SanctionUpdateRangeJobConfig extends BaseJobConfig<ComplianceDto, C
@Bean @Bean
@StepScope @StepScope
public ComplianceDataRangeReader complianceDataRangeReader( public ComplianceDataRangeReader complianceDataRangeReader() {
@Value("#{jobParameters['fromDate']}") String startDate, return new ComplianceDataRangeReader(maritimeServiceApiWebClient, jdbcTemplate, batchDateService);
@Value("#{jobParameters['toDate']}") String stopDate
) {
return new ComplianceDataRangeReader(maritimeServiceApiWebClient, startDate, stopDate);
} }
@Override @Override
protected ItemProcessor<ComplianceDto, ComplianceEntity> createProcessor() { protected ItemProcessor<ComplianceDto, ComplianceEntity> createProcessor() {
@ -85,13 +85,13 @@ public class SanctionUpdateRangeJobConfig extends BaseJobConfig<ComplianceDto, C
return complianceDataWriter; return complianceDataWriter;
} }
@Bean(name = "SanctionRangeUpdateJob") @Bean(name = "ComplianceImportRangeJob")
public Job sanctionRangeUpdateJob() { public Job complianceImportRangeJob() {
return job(); return job();
} }
@Bean(name = "SanctionRangeUpdateStep") @Bean(name = "ComplianceImportRangeStep")
public Step sanctionRangeUpdateStep() { public Step complianceImportRangeStep() {
return step(); return step();
} }

파일 보기

@ -1,4 +1,4 @@
package com.snp.batch.jobs.sanction.batch.dto; package com.snp.batch.jobs.compliance.batch.dto;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;

파일 보기

@ -1,4 +1,4 @@
package com.snp.batch.jobs.sanction.batch.dto; package com.snp.batch.jobs.compliance.batch.dto;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;

파일 보기

@ -1,4 +1,4 @@
package com.snp.batch.jobs.sanction.batch.entity; package com.snp.batch.jobs.compliance.batch.entity;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.snp.batch.common.batch.entity.BaseEntity; import com.snp.batch.common.batch.entity.BaseEntity;

파일 보기

@ -1,8 +1,8 @@
package com.snp.batch.jobs.sanction.batch.processor; package com.snp.batch.jobs.compliance.batch.processor;
import com.snp.batch.common.batch.processor.BaseProcessor; import com.snp.batch.common.batch.processor.BaseProcessor;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceDto; import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto;
import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity; import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

파일 보기

@ -1,8 +1,9 @@
package com.snp.batch.jobs.sanction.batch.reader; package com.snp.batch.jobs.compliance.batch.reader;
import com.snp.batch.common.batch.reader.BaseApiReader; import com.snp.batch.common.batch.reader.BaseApiReader;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceDto; import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto;
import com.snp.batch.jobs.shipMovementAnchorageCalls.batch.dto.AnchorageCallsDto; import com.snp.batch.jobs.shipdetail.batch.dto.ShipDetailComparisonData;
import com.snp.batch.service.BatchDateService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
@ -11,41 +12,23 @@ import org.springframework.web.reactive.function.client.WebClient;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
@Slf4j @Slf4j
public class ComplianceDataRangeReader extends BaseApiReader<ComplianceDto> { public class ComplianceDataRangeReader extends BaseApiReader<ComplianceDto> {
//TODO : private final JdbcTemplate jdbcTemplate;
// 1. Core20 IMO_NUMBER 전체 조회 private final BatchDateService batchDateService; // BatchDateService 필드 추가
// 2. IMO번호에 대한 마지막 AIS 신호 요청 (1회 최대 5000개 : Chunk 단위로 반복)
// 3. Response Data -> Core20에 업데이트 (Chunk 단위로 반복)
//private final JdbcTemplate jdbcTemplate;
private List<ComplianceDto> allData; private List<ComplianceDto> allData;
private int currentBatchIndex = 0; private int currentBatchIndex = 0;
private final int batchSize = 100; private final int batchSize = 1000;
private String fromDate; private String fromDate;
private String toDate; private String toDate;
public ComplianceDataRangeReader(WebClient webClient, public ComplianceDataRangeReader(WebClient webClient, JdbcTemplate jdbcTemplate, BatchDateService batchDateService) {
@Value("#{jobParameters['fromDate']}") String fromDate,
@Value("#{jobParameters['toDate']}") String toDate) {
super(webClient); super(webClient);
this.jdbcTemplate = jdbcTemplate;
// 날짜가 없으면 전날 하루 기준 this.batchDateService = batchDateService;
if (fromDate == null || fromDate.isBlank() ||
toDate == null || toDate.isBlank()) {
LocalDate yesterday = LocalDate.now().minusDays(1);
this.fromDate = yesterday.atStartOfDay().format(DateTimeFormatter.ISO_DATE_TIME) + "Z";
this.toDate = yesterday.plusDays(1).atStartOfDay().format(DateTimeFormatter.ISO_DATE_TIME) + "Z";
} else {
this.fromDate = fromDate;
this.toDate = toDate;
}
enableChunkMode(); enableChunkMode();
} }
@ -65,15 +48,8 @@ public class ComplianceDataRangeReader extends BaseApiReader<ComplianceDto> {
return "/RiskAndCompliance/UpdatedComplianceList"; return "/RiskAndCompliance/UpdatedComplianceList";
} }
private String getTargetTable(){ protected String getApiKey() {
return "snp_data.core20"; return "COMPLIANCE_IMPORT_API";
}
private String GET_CORE_IMO_LIST =
// "SELECT ihslrorimoshipno FROM " + getTargetTable() + " ORDER BY ihslrorimoshipno";
"select imo_number as ihslrorimoshipno from snp_data.ship_data order by imo_number";
@Override
protected void beforeFetch(){
log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), fromDate, toDate);
} }
@Override @Override
@ -81,7 +57,7 @@ public class ComplianceDataRangeReader extends BaseApiReader<ComplianceDto> {
// 모든 배치 처리 완료 확인 // 모든 배치 처리 완료 확인
if (allData == null) { if (allData == null) {
log.info("[{}] 최초 API 조회 실행: {} ~ {}", getReaderName(), fromDate, toDate); log.info("[{}] 최초 API 조회 실행: {} ~ {}", getReaderName(), fromDate, toDate);
allData = callApiWithBatch(fromDate, toDate); allData = callApiWithBatch();
if (allData == null || allData.isEmpty()) { if (allData == null || allData.isEmpty()) {
log.warn("[{}] 조회된 데이터 없음 → 종료", getReaderName()); log.warn("[{}] 조회된 데이터 없음 → 종료", getReaderName());
@ -115,11 +91,30 @@ public class ComplianceDataRangeReader extends BaseApiReader<ComplianceDto> {
return batch; return batch;
} }
private List<ComplianceDto> callApiWithBatch(String fromDate, String stopDate) { @Override
String url = getApiPath() + "?fromDate=" + fromDate +"&stopDate=" + stopDate; protected void afterFetch(List<ComplianceDto> data) {
try{
if (data == null) {
log.info("[{}] 배치 처리 성공", getReaderName());
}
}catch (Exception e){
log.info("[{}] 배치 처리 실패", getReaderName());
log.info("[{}] API 호출 종료", getReaderName());
}
}
private List<ComplianceDto> callApiWithBatch() {
Map<String, String> params = batchDateService.getRiskComplianceApiDateParams(getApiKey());
log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), params.get("fromDate"), params.get("toDate"));
String url = getApiPath();
log.debug("[{}] API 호출: {}", getReaderName(), url); log.debug("[{}] API 호출: {}", getReaderName(), url);
return webClient.get() return webClient.get()
.uri(url) .uri(url, uriBuilder -> uriBuilder
// 맵에서 파라미터 값을 동적으로 가져와 세팅
.queryParam("fromDate", params.get("fromDate"))
.queryParam("toDate", params.get("toDate"))
.build())
.retrieve() .retrieve()
.bodyToMono(new ParameterizedTypeReference<List<ComplianceDto>>() {}) .bodyToMono(new ParameterizedTypeReference<List<ComplianceDto>>() {})
.block(); .block();

파일 보기

@ -1,8 +1,7 @@
package com.snp.batch.jobs.sanction.batch.reader; package com.snp.batch.jobs.compliance.batch.reader;
import com.snp.batch.common.batch.reader.BaseApiReader; import com.snp.batch.common.batch.reader.BaseApiReader;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceDto; import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;

파일 보기

@ -1,6 +1,6 @@
package com.snp.batch.jobs.sanction.batch.repository; package com.snp.batch.jobs.compliance.batch.repository;
import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity; import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity;
import java.util.List; import java.util.List;

파일 보기

@ -1,7 +1,7 @@
package com.snp.batch.jobs.sanction.batch.repository; package com.snp.batch.jobs.compliance.batch.repository;
import com.snp.batch.common.batch.repository.BaseJdbcRepository; import com.snp.batch.common.batch.repository.BaseJdbcRepository;
import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity; import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
@ -12,7 +12,7 @@ import java.sql.Types;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@Repository("complianceRepository") @Repository("ComplianceRepository")
public class ComplianceRepositoryImpl extends BaseJdbcRepository<ComplianceEntity, Long> implements ComplianceRepository { public class ComplianceRepositoryImpl extends BaseJdbcRepository<ComplianceEntity, Long> implements ComplianceRepository {
public ComplianceRepositoryImpl(JdbcTemplate jdbcTemplate) { public ComplianceRepositoryImpl(JdbcTemplate jdbcTemplate) {
@ -42,7 +42,7 @@ public class ComplianceRepositoryImpl extends BaseJdbcRepository<ComplianceEntit
@Override @Override
protected String getUpdateSql() { protected String getUpdateSql() {
return """ return """
INSERT INTO new_snp.compliance ( INSERT INTO new_snp.compliance_history (
lrimoshipno, dateamended, legaloverall, shipbessanctionlist, shipdarkactivityindicator, lrimoshipno, dateamended, legaloverall, shipbessanctionlist, shipdarkactivityindicator,
shipdetailsnolongermaintained, shipeusanctionlist, shipflagdisputed, shipflagsanctionedcountry, shipdetailsnolongermaintained, shipeusanctionlist, shipflagdisputed, shipflagsanctionedcountry,
shiphistoricalflagsanctionedcountry, shipofacnonsdnsanctionlist, shipofacsanctionlist, shiphistoricalflagsanctionedcountry, shipofacnonsdnsanctionlist, shipofacsanctionlist,
@ -161,7 +161,6 @@ public class ComplianceRepositoryImpl extends BaseJdbcRepository<ComplianceEntit
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
log.info("{} 전체 저장 완료: 수정={} 건", getEntityName(), items.size()); log.info("{} 전체 저장 완료: 수정={} 건", getEntityName(), items.size());
} }
} }

파일 보기

@ -0,0 +1,33 @@
package com.snp.batch.jobs.compliance.batch.writer;
import com.snp.batch.common.batch.writer.BaseWriter;
import com.snp.batch.jobs.compliance.batch.repository.ComplianceRepository;
import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity;
import com.snp.batch.service.BatchDateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
@Slf4j
@Component
public class ComplianceDataWriter extends BaseWriter<ComplianceEntity> {
private final ComplianceRepository complianceRepository;
private final BatchDateService batchDateService; // BatchDateService 필드 추가
protected String getApiKey() {return "COMPLIANCE_IMPORT_API";}
public ComplianceDataWriter(ComplianceRepository complianceRepository, BatchDateService batchDateService) {
super("ComplianceRepository");
this.complianceRepository = complianceRepository;
this.batchDateService = batchDateService;
}
@Override
protected void writeItems(List<ComplianceEntity> items) throws Exception {
complianceRepository.saveComplianceAll(items);
LocalDate successDate = LocalDate.now(); // 현재 배치 실행 시점의 날짜 (Reader의 toDay와 동일한 )
batchDateService.updateLastSuccessDate(getApiKey(), successDate);
log.info("batch_last_execution update 완료 : {}", getApiKey());
}
}

파일 보기

@ -50,12 +50,12 @@ public class RiskImportJobConfig extends BaseJobConfig<RiskDto, RiskEntity> {
@Override @Override
protected String getJobName() { protected String getJobName() {
return "riskImportJob"; return "RiskImportJob";
} }
@Override @Override
protected String getStepName() { protected String getStepName() {
return "riskImportStep"; return "RiskImportStep";
} }
@Override @Override
@ -71,12 +71,12 @@ public class RiskImportJobConfig extends BaseJobConfig<RiskDto, RiskEntity> {
@Override @Override
protected ItemWriter<RiskEntity> createWriter() { return riskDataWriter; } protected ItemWriter<RiskEntity> createWriter() { return riskDataWriter; }
@Bean(name = "riskImportJob") @Bean(name = "RiskImportJob")
public Job riskImportJob() { public Job riskImportJob() {
return job(); return job();
} }
@Bean(name = "riskImportStep") @Bean(name = "RiskImportStep")
public Step riskImportStep() { public Step riskImportStep() {
return step(); return step();
} }

파일 보기

@ -5,9 +5,8 @@ import com.snp.batch.jobs.risk.batch.dto.RiskDto;
import com.snp.batch.jobs.risk.batch.entity.RiskEntity; import com.snp.batch.jobs.risk.batch.entity.RiskEntity;
import com.snp.batch.jobs.risk.batch.processor.RiskDataProcessor; import com.snp.batch.jobs.risk.batch.processor.RiskDataProcessor;
import com.snp.batch.jobs.risk.batch.reader.RiskDataRangeReader; import com.snp.batch.jobs.risk.batch.reader.RiskDataRangeReader;
import com.snp.batch.jobs.risk.batch.reader.RiskDataReader;
import com.snp.batch.jobs.risk.batch.writer.RiskDataWriter; import com.snp.batch.jobs.risk.batch.writer.RiskDataWriter;
import com.snp.batch.jobs.sanction.batch.reader.ComplianceDataRangeReader; import com.snp.batch.service.BatchDateService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job; import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
@ -31,10 +30,11 @@ public class RiskImportRangeJobConfig extends BaseJobConfig<RiskDto, RiskEntity>
private final RiskDataProcessor riskDataProcessor; private final RiskDataProcessor riskDataProcessor;
private final RiskDataWriter riskDataWriter; private final RiskDataWriter riskDataWriter;
private final RiskDataRangeReader riskDataRangeReader; private final RiskDataRangeReader riskDataRangeReader;
private final JdbcTemplate jdbcTemplate;
private final BatchDateService batchDateService;
@Override @Override
protected int getChunkSize() { protected int getChunkSize() {
return 5000; // API에서 5000개씩 가져오므로 chunk도 5000으로 설정 return 10000;
} }
public RiskImportRangeJobConfig( public RiskImportRangeJobConfig(
JobRepository jobRepository, JobRepository jobRepository,
@ -42,12 +42,16 @@ public class RiskImportRangeJobConfig extends BaseJobConfig<RiskDto, RiskEntity>
RiskDataProcessor riskDataProcessor, RiskDataProcessor riskDataProcessor,
RiskDataWriter riskDataWriter, RiskDataWriter riskDataWriter,
JdbcTemplate jdbcTemplate, JdbcTemplate jdbcTemplate,
@Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient, RiskDataRangeReader riskDataRangeReader) { @Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient,
RiskDataRangeReader riskDataRangeReader,
BatchDateService batchDateService) {
super(jobRepository, transactionManager); super(jobRepository, transactionManager);
this.maritimeServiceApiWebClient = maritimeServiceApiWebClient; this.maritimeServiceApiWebClient = maritimeServiceApiWebClient;
this.riskDataProcessor = riskDataProcessor; this.riskDataProcessor = riskDataProcessor;
this.riskDataWriter = riskDataWriter; this.riskDataWriter = riskDataWriter;
this.jdbcTemplate = jdbcTemplate;
this.riskDataRangeReader = riskDataRangeReader; this.riskDataRangeReader = riskDataRangeReader;
this.batchDateService = batchDateService;
} }
@Override @Override
@ -66,11 +70,8 @@ public class RiskImportRangeJobConfig extends BaseJobConfig<RiskDto, RiskEntity>
} }
@Bean @Bean
@StepScope @StepScope
public RiskDataRangeReader riskDataRangeReader( public RiskDataRangeReader riskDataRangeReader() {
@Value("#{jobParameters['fromDate']}") String startDate, return new RiskDataRangeReader(maritimeServiceApiWebClient, jdbcTemplate, batchDateService);
@Value("#{jobParameters['toDate']}") String stopDate
) {
return new RiskDataRangeReader(maritimeServiceApiWebClient, startDate, stopDate);
} }
@Override @Override

파일 보기

@ -1,8 +1,9 @@
package com.snp.batch.jobs.risk.batch.reader; package com.snp.batch.jobs.risk.batch.reader;
import com.snp.batch.common.batch.reader.BaseApiReader; import com.snp.batch.common.batch.reader.BaseApiReader;
import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto;
import com.snp.batch.jobs.risk.batch.dto.RiskDto; import com.snp.batch.jobs.risk.batch.dto.RiskDto;
import com.snp.batch.jobs.sanction.batch.dto.ComplianceDto; import com.snp.batch.service.BatchDateService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
@ -11,45 +12,29 @@ import org.springframework.web.reactive.function.client.WebClient;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
@Slf4j @Slf4j
public class RiskDataRangeReader extends BaseApiReader<RiskDto> { public class RiskDataRangeReader extends BaseApiReader<RiskDto> {
//TODO : private final JdbcTemplate jdbcTemplate;
// 1. Core20 IMO_NUMBER 전체 조회 private final BatchDateService batchDateService; // BatchDateService 필드 추가
// 2. IMO번호에 대한 마지막 AIS 신호 요청 (1회 최대 5000개 : Chunk 단위로 반복)
// 3. Response Data -> Core20에 업데이트 (Chunk 단위로 반복)
private List<RiskDto> allData; private List<RiskDto> allData;
private int currentBatchIndex = 0; private int currentBatchIndex = 0;
private final int batchSize = 100; private final int batchSize = 1000;
private String fromDate; private String fromDate;
private String toDate; private String toDate;
public RiskDataRangeReader(WebClient webClient, public RiskDataRangeReader(WebClient webClient, JdbcTemplate jdbcTemplate, BatchDateService batchDateService) {
@Value("#{jobParameters['fromDate']}") String fromDate,
@Value("#{jobParameters['toDate']}") String toDate) {
super(webClient); super(webClient);
this.jdbcTemplate = jdbcTemplate;
// 날짜가 없으면 전날 하루 기준 this.batchDateService = batchDateService;
if (fromDate == null || fromDate.isBlank() ||
toDate == null || toDate.isBlank()) {
LocalDate yesterday = LocalDate.now().minusDays(1);
this.fromDate = yesterday.atStartOfDay().format(DateTimeFormatter.ISO_DATE_TIME) + "Z";
this.toDate = yesterday.plusDays(1).atStartOfDay().format(DateTimeFormatter.ISO_DATE_TIME) + "Z";
} else {
this.fromDate = fromDate;
this.toDate = toDate;
}
enableChunkMode(); enableChunkMode();
} }
@Override @Override
protected String getReaderName() { protected String getReaderName() {
return "riskDataRangeReader"; return "RiskDataRangeReader";
} }
@Override @Override
@ -62,10 +47,8 @@ public class RiskDataRangeReader extends BaseApiReader<RiskDto> {
protected String getApiPath() { protected String getApiPath() {
return "/RiskAndCompliance/UpdatedRiskList"; return "/RiskAndCompliance/UpdatedRiskList";
} }
protected String getApiKey() {
@Override return "RISK_IMPORT_API";
protected void beforeFetch(){
log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), fromDate, toDate);
} }
@Override @Override
@ -73,7 +56,7 @@ public class RiskDataRangeReader extends BaseApiReader<RiskDto> {
// 모든 배치 처리 완료 확인 // 모든 배치 처리 완료 확인
if (allData == null) { if (allData == null) {
log.info("[{}] 최초 API 조회 실행: {} ~ {}", getReaderName(), fromDate, toDate); log.info("[{}] 최초 API 조회 실행: {} ~ {}", getReaderName(), fromDate, toDate);
allData = callApiWithBatch(fromDate, toDate); allData = callApiWithBatch();
if (allData == null || allData.isEmpty()) { if (allData == null || allData.isEmpty()) {
log.warn("[{}] 조회된 데이터 없음 → 종료", getReaderName()); log.warn("[{}] 조회된 데이터 없음 → 종료", getReaderName());
@ -106,12 +89,29 @@ public class RiskDataRangeReader extends BaseApiReader<RiskDto> {
return batch; return batch;
} }
@Override
protected void afterFetch(List<RiskDto> data) {
try{
if (data == null) {
log.info("[{}] 배치 처리 성공", getReaderName());
}
}catch (Exception e){
log.info("[{}] 배치 처리 실패", getReaderName());
log.info("[{}] API 호출 종료", getReaderName());
}
}
private List<RiskDto> callApiWithBatch(String fromDate, String stopDate) { private List<RiskDto> callApiWithBatch() {
String url = getApiPath() + "?fromDate=" + fromDate +"&stopDate=" + stopDate; Map<String, String> params = batchDateService.getRiskComplianceApiDateParams(getApiKey());
log.debug("[{}] API 호출: {}", getReaderName(), url); log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), params.get("fromDate"), params.get("toDate"));
String url = getApiPath();
return webClient.get() return webClient.get()
.uri(url) .uri(url, uriBuilder -> uriBuilder
// 맵에서 파라미터 값을 동적으로 가져와 세팅
.queryParam("fromDate", params.get("fromDate"))
.queryParam("toDate", params.get("toDate"))
.build())
.retrieve() .retrieve()
.bodyToMono(new ParameterizedTypeReference<List<RiskDto>>() {}) .bodyToMono(new ParameterizedTypeReference<List<RiskDto>>() {})
.block(); .block();

파일 보기

@ -41,7 +41,7 @@ public class RiskRepositoryImpl extends BaseJdbcRepository<RiskEntity, Long> imp
@Override @Override
protected String getUpdateSql() { protected String getUpdateSql() {
return """ return """
INSERT INTO new_snp.risk ( INSERT INTO new_snp.risk_history (
lrno, lastupdated, riskdatamaintained, dayssincelastseenonais, dayssincelastseenonaisnarrative, lrno, lastupdated, riskdatamaintained, dayssincelastseenonais, dayssincelastseenonaisnarrative,
daysunderais, daysunderaisnarrative, imocorrectonais, imocorrectonaisnarrative, sailingundername, daysunderais, daysunderaisnarrative, imocorrectonais, imocorrectonaisnarrative, sailingundername,
sailingundernamenarrative, anomalousmessagesfrommmsi, anomalousmessagesfrommmsinarrative, sailingundernamenarrative, anomalousmessagesfrommmsi, anomalousmessagesfrommmsinarrative,

파일 보기

@ -3,9 +3,11 @@ package com.snp.batch.jobs.risk.batch.writer;
import com.snp.batch.common.batch.writer.BaseWriter; import com.snp.batch.common.batch.writer.BaseWriter;
import com.snp.batch.jobs.risk.batch.entity.RiskEntity; import com.snp.batch.jobs.risk.batch.entity.RiskEntity;
import com.snp.batch.jobs.risk.batch.repository.RiskRepository; import com.snp.batch.jobs.risk.batch.repository.RiskRepository;
import com.snp.batch.service.BatchDateService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List; import java.util.List;
@Slf4j @Slf4j
@ -13,14 +15,19 @@ import java.util.List;
public class RiskDataWriter extends BaseWriter<RiskEntity> { public class RiskDataWriter extends BaseWriter<RiskEntity> {
private final RiskRepository riskRepository; private final RiskRepository riskRepository;
public RiskDataWriter(RiskRepository riskRepository) { private final BatchDateService batchDateService; // BatchDateService 필드 추가
protected String getApiKey() {return "RISK_IMPORT_API";}
public RiskDataWriter(RiskRepository riskRepository, BatchDateService batchDateService) {
super("riskRepository"); super("riskRepository");
this.riskRepository = riskRepository; this.riskRepository = riskRepository;
this.batchDateService = batchDateService;
} }
@Override @Override
protected void writeItems(List<RiskEntity> items) throws Exception { protected void writeItems(List<RiskEntity> items) throws Exception {
riskRepository.saveRiskAll(items); riskRepository.saveRiskAll(items);
log.info("Risk 저장 완료: 수정={} 건", items.size()); LocalDate successDate = LocalDate.now(); // 현재 배치 실행 시점의 날짜 (Reader의 toDay와 동일한 )
batchDateService.updateLastSuccessDate(getApiKey(), successDate);
log.info("batch_last_execution update 완료 : {}", getApiKey());
} }
} }

파일 보기

@ -1,153 +0,0 @@
package com.snp.batch.jobs.sample.batch.config;
import com.snp.batch.common.batch.config.BaseJobConfig;
import com.snp.batch.jobs.sample.batch.dto.OrderDto;
import com.snp.batch.jobs.sample.batch.processor.OrderDataProcessor;
import com.snp.batch.jobs.sample.batch.writer.OrderItemWriter;
import com.snp.batch.jobs.sample.batch.writer.OrderWriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.CompositeItemWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.Arrays;
/**
* 주문 데이터 Import Job Config (복잡한 JSON 처리 예제)
*
* 특징:
* - CompositeWriter 사용
* - 하나의 데이터 (OrderDto) 여러 테이블에 저장
* - OrderWriter: orders 테이블에 저장
* - OrderItemWriter: order_items 테이블에 저장
*
* 데이터 흐름:
* OrderDataReader
* (OrderDto)
* OrderDataProcessor
* (OrderWrapper)
* CompositeWriter {
* OrderWriter
* OrderItemWriter
* }
*
* 주의:
* - JobConfig는 예제용입니다
* - 실제 사용 OrderDataReader 구현 필요
* - OrderRepository, OrderItemRepository 구현 필요
*/
@Slf4j
@Configuration
public class OrderDataImportJobConfig extends BaseJobConfig<OrderDto, OrderDataProcessor.OrderWrapper> {
private final OrderDataProcessor orderDataProcessor;
private final OrderWriter orderWriter;
private final OrderItemWriter orderItemWriter;
public OrderDataImportJobConfig(
JobRepository jobRepository,
PlatformTransactionManager transactionManager,
OrderDataProcessor orderDataProcessor,
OrderWriter orderWriter,
OrderItemWriter orderItemWriter) {
super(jobRepository, transactionManager);
this.orderDataProcessor = orderDataProcessor;
this.orderWriter = orderWriter;
this.orderItemWriter = orderItemWriter;
}
@Override
protected String getJobName() {
return "orderDataImportJob";
}
@Override
protected ItemReader<OrderDto> createReader() {
// 실제 구현 OrderDataReader 생성
// 예제이므로 null 반환 (Job 등록 )
return null;
}
@Override
protected ItemProcessor<OrderDto, OrderDataProcessor.OrderWrapper> createProcessor() {
return orderDataProcessor;
}
/**
* CompositeWriter 생성
* OrderWriter와 OrderItemWriter를 조합
*/
@Override
protected ItemWriter<OrderDataProcessor.OrderWrapper> createWriter() {
CompositeItemWriter<OrderDataProcessor.OrderWrapper> compositeWriter =
new CompositeItemWriter<>();
// 여러 Writer를 순서대로 실행
compositeWriter.setDelegates(Arrays.asList(
orderWriter, // 1. 주문 저장
orderItemWriter // 2. 주문 상품 저장
));
return compositeWriter;
}
@Override
protected int getChunkSize() {
return 10;
}
/**
* Job Bean 등록 (주석 처리)
* 실제 사용 주석 해제하고 OrderDataReader 구현 필요
*/
// @Bean(name = "orderDataImportJob")
public Job orderDataImportJob() {
return job();
}
/**
* Step Bean 등록 (주석 처리)
*/
// @Bean(name = "orderDataImportStep")
public Step orderDataImportStep() {
return step();
}
}
/**
* ========================================
* CompositeWriter 사용 가이드
* ========================================
*
* 1. 언제 사용하는가?
* - 하나의 데이터를 여러 테이블에 저장해야
* - 중첩된 JSON을 분해하여 관계형 DB에 저장할
* - 1:N 관계 데이터 저장
*
* 2. 작동 방식:
* - Processor가 여러 Entity를 Wrapper에 담아 반환
* - CompositeWriter가 Writer를 순서대로 실행
* - 모든 Writer는 동일한 Wrapper를 받음
* - Writer는 필요한 Entity만 추출하여 저장
*
* 3. 트랜잭션:
* - 모든 Writer는 동일한 트랜잭션 내에서 실행
* - 하나라도 실패하면 전체 롤백
*
* 4. 주의사항:
* - Writer 실행 순서 중요 (부모 자식)
* - 외래 제약 조건 고려
* - 성능: Chunk 크기 조정 필요
*
* 5. 대안:
* - 간단한 경우: 단일 Writer에서 여러 Repository 호출
* - 복잡한 경우: Tasklet 사용
*/

파일 보기

@ -1,101 +0,0 @@
package com.snp.batch.jobs.sample.batch.config;
import com.snp.batch.common.batch.config.BaseJobConfig;
import com.snp.batch.jobs.sample.batch.dto.ProductDto;
import com.snp.batch.jobs.sample.batch.entity.ProductEntity;
import com.snp.batch.jobs.sample.batch.reader.ProductDataReader;
import com.snp.batch.jobs.sample.batch.processor.ProductDataProcessor;
import com.snp.batch.jobs.sample.batch.writer.ProductDataWriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
/**
* 제품 데이터 Import Job 설정
* BaseJobConfig를 상속하여 구현
*
* 샘플 데이터 배치 Job:
* - Mock API에서 10개의 샘플 제품 데이터 생성
* - 다양한 데이터 타입 (String, BigDecimal, Integer, Boolean, Double, LocalDate, Float, Long, TEXT) 포함
* - 필터링 테스트 (비활성 제품 제외)
* - PostgreSQL에 저장
*/
@Slf4j
@Configuration
public class ProductDataImportJobConfig extends BaseJobConfig<ProductDto, ProductEntity> {
private final ProductDataReader productDataReader;
private final ProductDataProcessor productDataProcessor;
private final ProductDataWriter productDataWriter;
/**
* 생성자 주입
*/
public ProductDataImportJobConfig(
JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ProductDataReader productDataReader,
ProductDataProcessor productDataProcessor,
ProductDataWriter productDataWriter) {
super(jobRepository, transactionManager);
this.productDataReader = productDataReader;
this.productDataProcessor = productDataProcessor;
this.productDataWriter = productDataWriter;
}
@Override
protected String getJobName() {
return "sampleProductImportJob";
}
@Override
protected String getStepName() {
return "sampleProductImportStep";
}
@Override
protected ItemReader<ProductDto> createReader() {
return productDataReader;
}
@Override
protected ItemProcessor<ProductDto, ProductEntity> createProcessor() {
return productDataProcessor;
}
@Override
protected ItemWriter<ProductEntity> createWriter() {
return productDataWriter;
}
@Override
protected int getChunkSize() {
// 샘플 데이터는 10개이므로 작은 Chunk 크기 사용
return 5;
}
/**
* Job Bean 등록
*/
@Bean(name = "sampleProductImportJob")
public Job sampleProductImportJob() {
return job();
}
/**
* Step Bean 등록
*/
@Bean(name = "sampleProductImportStep")
public Step sampleProductImportStep() {
return step();
}
}

파일 보기

@ -1,97 +0,0 @@
package com.snp.batch.jobs.sample.batch.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 주문 DTO (복잡한 JSON 예제용)
*
* API 응답 예제:
* {
* "orderId": "ORD-001",
* "customerName": "홍길동",
* "orderDate": "2025-10-16T10:30:00",
* "totalAmount": 150000,
* "items": [
* {
* "productId": "PROD-001",
* "productName": "노트북",
* "quantity": 1,
* "price": 100000
* },
* {
* "productId": "PROD-002",
* "productName": "마우스",
* "quantity": 2,
* "price": 25000
* }
* ]
* }
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderDto {
/**
* 주문 ID
*/
private String orderId;
/**
* 고객 이름
*/
private String customerName;
/**
* 주문 일시
*/
private LocalDateTime orderDate;
/**
* 주문 금액
*/
private BigDecimal totalAmount;
/**
* 주문 상품 목록 (중첩 데이터)
*/
private List<OrderItemDto> items;
/**
* 주문 상품 DTO (내부 클래스)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class OrderItemDto {
/**
* 상품 ID
*/
private String productId;
/**
* 상품명
*/
private String productName;
/**
* 수량
*/
private Integer quantity;
/**
* 가격
*/
private BigDecimal price;
}
}

파일 보기

@ -1,45 +0,0 @@
package com.snp.batch.jobs.sample.batch.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 제품 API 응답 래퍼
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ProductApiResponse {
/**
* 성공 여부
*/
@JsonProperty("success")
private Boolean success;
/**
* 개수
*/
@JsonProperty("total_count")
private Integer totalCount;
/**
* 제품 목록
*/
@JsonProperty("products")
private List<ProductDto> products;
/**
* 메시지
*/
@JsonProperty("message")
private String message;
}

파일 보기

@ -1,95 +0,0 @@
package com.snp.batch.jobs.sample.batch.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 제품 DTO (샘플 데이터)
* 다양한 데이터 타입 포함
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ProductDto {
/**
* 제품 ID (String)
*/
@JsonProperty("product_id")
private String productId;
/**
* 제품명 (String)
*/
@JsonProperty("product_name")
private String productName;
/**
* 카테고리 (String)
*/
@JsonProperty("category")
private String category;
/**
* 가격 (BigDecimal)
*/
@JsonProperty("price")
private BigDecimal price;
/**
* 재고 수량 (Integer)
*/
@JsonProperty("stock_quantity")
private Integer stockQuantity;
/**
* 활성 여부 (Boolean)
*/
@JsonProperty("is_active")
private Boolean isActive;
/**
* 평점 (Double)
*/
@JsonProperty("rating")
private Double rating;
/**
* 제조일자 (LocalDate)
*/
@JsonProperty("manufacture_date")
private LocalDate manufactureDate;
/**
* 무게 (kg) (Float)
*/
@JsonProperty("weight")
private Float weight;
/**
* 판매 횟수 (Long)
*/
@JsonProperty("sales_count")
private Long salesCount;
/**
* 설명 (Text)
*/
@JsonProperty("description")
private String description;
/**
* 태그 (JSON Array String으로 저장)
*/
@JsonProperty("tags")
private String tags;
}

파일 보기

@ -1,58 +0,0 @@
package com.snp.batch.jobs.sample.batch.entity;
import com.snp.batch.common.batch.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 주문 Entity (복잡한 JSON 예제용)
* BaseEntity를 상속하여 감사 필드 포함
*
* JPA 어노테이션 사용 금지 (JDBC 전용)
* 컬럼 매핑은 주석으로 명시
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class OrderEntity extends BaseEntity {
/**
* 기본 (자동 생성)
* 컬럼: id (BIGSERIAL)
*/
private Long id;
/**
* 주문 ID (비즈니스 )
* 컬럼: order_id (VARCHAR(50), UNIQUE, NOT NULL)
*/
private String orderId;
/**
* 고객 이름
* 컬럼: customer_name (VARCHAR(100))
*/
private String customerName;
/**
* 주문 일시
* 컬럼: order_date (TIMESTAMP)
*/
private LocalDateTime orderDate;
/**
* 주문 금액
* 컬럼: total_amount (DECIMAL(10, 2))
*/
private BigDecimal totalAmount;
// createdAt, updatedAt, createdBy, updatedBy는 BaseEntity에서 상속
}

파일 보기

@ -1,63 +0,0 @@
package com.snp.batch.jobs.sample.batch.entity;
import com.snp.batch.common.batch.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
/**
* 주문 상품 Entity (복잡한 JSON 예제용)
* BaseEntity를 상속하여 감사 필드 포함
*
* JPA 어노테이션 사용 금지 (JDBC 전용)
* 컬럼 매핑은 주석으로 명시
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class OrderItemEntity extends BaseEntity {
/**
* 기본 (자동 생성)
* 컬럼: id (BIGSERIAL)
*/
private Long id;
/**
* 주문 ID (외래 )
* 컬럼: order_id (VARCHAR(50), NOT NULL)
*/
private String orderId;
/**
* 상품 ID
* 컬럼: product_id (VARCHAR(50))
*/
private String productId;
/**
* 상품명
* 컬럼: product_name (VARCHAR(200))
*/
private String productName;
/**
* 수량
* 컬럼: quantity (INTEGER)
*/
private Integer quantity;
/**
* 가격
* 컬럼: price (DECIMAL(10, 2))
*/
private BigDecimal price;
// createdAt, updatedAt, createdBy, updatedBy는 BaseEntity에서 상속
}

파일 보기

@ -1,103 +0,0 @@
package com.snp.batch.jobs.sample.batch.entity;
import com.snp.batch.common.batch.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 제품 엔티티 (샘플 데이터) - JDBC 전용
* 다양한 데이터 타입 포함
*
* 테이블: sample_products
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ProductEntity extends BaseEntity {
/**
* 기본 (자동 생성)
* 컬럼: id (BIGSERIAL)
*/
private Long id;
/**
* 제품 ID (비즈니스 )
* 컬럼: product_id (VARCHAR(50), UNIQUE, NOT NULL)
*/
private String productId;
/**
* 제품명
* 컬럼: product_name (VARCHAR(200), NOT NULL)
*/
private String productName;
/**
* 카테고리
* 컬럼: category (VARCHAR(100))
*/
private String category;
/**
* 가격
* 컬럼: price (DECIMAL(10,2))
*/
private BigDecimal price;
/**
* 재고 수량
* 컬럼: stock_quantity (INTEGER)
*/
private Integer stockQuantity;
/**
* 활성 여부
* 컬럼: is_active (BOOLEAN)
*/
private Boolean isActive;
/**
* 평점
* 컬럼: rating (DOUBLE PRECISION)
*/
private Double rating;
/**
* 제조일자
* 컬럼: manufacture_date (DATE)
*/
private LocalDate manufactureDate;
/**
* 무게 (kg)
* 컬럼: weight (REAL/FLOAT)
*/
private Float weight;
/**
* 판매 횟수
* 컬럼: sales_count (BIGINT)
*/
private Long salesCount;
/**
* 설명
* 컬럼: description (TEXT)
*/
private String description;
/**
* 태그 (JSON 문자열)
* 컬럼: tags (VARCHAR(500))
*/
private String tags;
}

파일 보기

@ -1,103 +0,0 @@
package com.snp.batch.jobs.sample.batch.processor;
import com.snp.batch.common.batch.processor.BaseProcessor;
import com.snp.batch.jobs.sample.batch.dto.OrderDto;
import com.snp.batch.jobs.sample.batch.entity.OrderEntity;
import com.snp.batch.jobs.sample.batch.entity.OrderItemEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 주문 데이터 Processor (복잡한 JSON 처리 예제)
*
* 처리 방식:
* 1. 중첩된 JSON (OrderDto) 받아서
* 2. OrderEntity (부모) OrderItemEntity 리스트 (자식) 분해
* 3. OrderWrapper에 담아서 반환
* 4. CompositeWriter가 각각 다른 테이블에 저장
*
* 데이터 흐름:
* OrderDto (1개)
*
* OrderDataProcessor
*
* OrderWrapper {
* OrderEntity (1개)
* List<OrderItemEntity> (N개)
* }
*
* CompositeWriter {
* OrderWriter orders 테이블
* OrderItemWriter order_items 테이블
* }
*/
@Slf4j
@Component
public class OrderDataProcessor extends BaseProcessor<OrderDto, OrderDataProcessor.OrderWrapper> {
/**
* OrderDto를 OrderEntity와 OrderItemEntity 리스트로 분해
*/
@Override
protected OrderWrapper processItem(OrderDto dto) throws Exception {
log.debug("주문 데이터 처리 시작: orderId={}", dto.getOrderId());
// 1. OrderEntity 생성 (부모 데이터)
OrderEntity orderEntity = OrderEntity.builder()
.orderId(dto.getOrderId())
.customerName(dto.getCustomerName())
.orderDate(dto.getOrderDate())
.totalAmount(dto.getTotalAmount())
.build();
// 2. OrderItemEntity 리스트 생성 (자식 데이터)
List<OrderItemEntity> orderItems = new ArrayList<>();
if (dto.getItems() != null && !dto.getItems().isEmpty()) {
for (OrderDto.OrderItemDto itemDto : dto.getItems()) {
OrderItemEntity itemEntity = OrderItemEntity.builder()
.orderId(dto.getOrderId()) // 부모 orderId 연결
.productId(itemDto.getProductId())
.productName(itemDto.getProductName())
.quantity(itemDto.getQuantity())
.price(itemDto.getPrice())
.build();
orderItems.add(itemEntity);
}
}
log.debug("주문 데이터 처리 완료: orderId={}, items={}",
dto.getOrderId(), orderItems.size());
// 3. Wrapper에 담아서 반환
return new OrderWrapper(orderEntity, orderItems);
}
/**
* OrderWrapper 클래스
* OrderEntity와 OrderItemEntity 리스트를 함께 담는 컨테이너
*
* CompositeWriter가 Wrapper를 받아서 각각 다른 Writer로 전달
*/
public static class OrderWrapper {
private final OrderEntity order;
private final List<OrderItemEntity> items;
public OrderWrapper(OrderEntity order, List<OrderItemEntity> items) {
this.order = order;
this.items = items;
}
public OrderEntity getOrder() {
return order;
}
public List<OrderItemEntity> getItems() {
return items;
}
}
}

파일 보기

@ -1,46 +0,0 @@
package com.snp.batch.jobs.sample.batch.processor;
import com.snp.batch.common.batch.processor.BaseProcessor;
import com.snp.batch.jobs.sample.batch.dto.ProductDto;
import com.snp.batch.jobs.sample.batch.entity.ProductEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 제품 데이터 Processor
* BaseProcessor를 상속하여 구현
*/
@Slf4j
@Component
public class ProductDataProcessor extends BaseProcessor<ProductDto, ProductEntity> {
@Override
protected ProductEntity processItem(ProductDto dto) throws Exception {
// 필터링 조건: productId가 있고, 활성화된 제품만 처리
if (dto.getProductId() == null || dto.getProductId().isEmpty()) {
log.warn("제품 ID가 없어 필터링됨: {}", dto);
return null;
}
if (dto.getIsActive() == null || !dto.getIsActive()) {
log.info("비활성 제품 필터링: {} ({})", dto.getProductId(), dto.getProductName());
return null;
}
// DTO Entity 변환
return ProductEntity.builder()
.productId(dto.getProductId())
.productName(dto.getProductName())
.category(dto.getCategory())
.price(dto.getPrice())
.stockQuantity(dto.getStockQuantity())
.isActive(dto.getIsActive())
.rating(dto.getRating())
.manufactureDate(dto.getManufactureDate())
.weight(dto.getWeight())
.salesCount(dto.getSalesCount())
.description(dto.getDescription())
.tags(dto.getTags())
.build();
}
}

파일 보기

@ -1,287 +0,0 @@
package com.snp.batch.jobs.sample.batch.reader;
import com.snp.batch.common.batch.reader.BaseApiReader;
import com.snp.batch.jobs.sample.batch.dto.ProductApiResponse;
import com.snp.batch.jobs.sample.batch.dto.ProductDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 제품 데이터 API Reader (실전 예제)
* BaseApiReader v2.0을 사용한 실제 API 연동 예제
*
* 주요 기능:
* - GET/POST 요청 예제
* - Query Parameter 처리
* - Request Body 처리
* - Header 설정
* - 복잡한 JSON 응답 파싱
*
* 사용법:
* JobConfig에서 Reader를 사용하려면:
* 1. @Component 또는 @Bean으로 등록
* 2. WebClient Bean 주입
* 3. ProductApiReader 생성 WebClient 전달
* 4. application.yml에 API 설정 추가
*
* 참고:
* - 클래스는 예제용으로 @Component가 제거되어 있습니다
* - 실제 사용 JobConfig에서 @Bean으로 등록하세요
*/
@Slf4j
// @Component - 예제용이므로 주석 처리 (실제 사용 활성화)
public class ProductApiReader extends BaseApiReader<ProductDto> {
/**
* WebClient 주입 생성자
*
* @param webClient Spring WebClient 인스턴스
*/
public ProductApiReader(WebClient webClient) {
super(webClient);
}
// ========================================
// 필수 구현 메서드
// ========================================
@Override
protected String getReaderName() {
return "ProductApiReader";
}
@Override
protected List<ProductDto> fetchDataFromApi() {
try {
// callApi() 헬퍼 메서드 사용 (GET/POST 자동 처리)
ProductApiResponse response = callApi();
// 응답에서 데이터 추출
return extractDataFromResponse(response);
} catch (Exception e) {
// 에러 처리 ( 리스트 반환 또는 예외 던지기)
return handleApiError(e);
}
}
// ========================================
// HTTP 요청 설정 (예제: GET 요청)
// ========================================
/**
* HTTP Method 설정
*
* GET 예제:
* return "GET";
*
* POST 예제로 변경하려면:
* return "POST";
*/
@Override
protected String getHttpMethod() {
return "GET"; // GET 요청 예제
}
/**
* API 엔드포인트 경로
*
* 예제:
* - "/api/v1/products"
* - "/api/v1/products/search"
*/
@Override
protected String getApiPath() {
return "/api/v1/products";
}
/**
* Query Parameter 설정
*
* GET 요청 사용되는 파라미터
*
* 예제:
* ?status=active&category=전자제품&page=1&size=100
*/
@Override
protected Map<String, Object> getQueryParams() {
Map<String, Object> params = new HashMap<>();
params.put("status", "active"); // 활성 제품만
params.put("category", "전자제품"); // 카테고리 필터
params.put("page", 1); // 페이지 번호
params.put("size", 100); // 페이지 크기
return params;
}
/**
* HTTP Header 설정
*
* 인증 토큰, API Key 추가
*/
@Override
protected Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
// 예제: API Key 인증
// headers.put("X-API-Key", "your-api-key-here");
// 예제: Bearer 토큰 인증
// headers.put("Authorization", "Bearer " + getAccessToken());
return headers;
}
/**
* API 응답 타입 지정
*/
@Override
protected Class<?> getResponseType() {
return ProductApiResponse.class;
}
/**
* API 응답에서 데이터 리스트 추출
*
* 복잡한 JSON 구조 처리:
* {
* "success": true,
* "data": {
* "products": [...],
* "totalCount": 100
* }
* }
*/
@Override
protected List<ProductDto> extractDataFromResponse(Object response) {
if (response instanceof ProductApiResponse) {
ProductApiResponse apiResponse = (ProductApiResponse) response;
return apiResponse.getProducts();
}
return super.extractDataFromResponse(response);
}
// ========================================
// 라이프사이클 (선택적 오버라이드)
// ========================================
@Override
protected void beforeFetch() {
log.info("[{}] 제품 API 호출 준비 중...", getReaderName());
log.info("- Method: {}", getHttpMethod());
log.info("- Path: {}", getApiPath());
log.info("- Query Params: {}", getQueryParams());
}
@Override
protected void afterFetch(List<ProductDto> data) {
log.info("[{}] API 호출 성공: {}건 조회", getReaderName(), getDataSize(data));
// 데이터 검증
if (isEmpty(data)) {
log.warn("[{}] 조회된 데이터가 없습니다!", getReaderName());
}
}
@Override
protected List<ProductDto> handleApiError(Exception e) {
log.error("[{}] 제품 API 호출 실패", getReaderName(), e);
// 선택 1: 리스트 반환 (Job 실패 방지)
// return new ArrayList<>();
// 선택 2: 예외 던지기 (Job 실패 처리)
throw new RuntimeException("제품 데이터 조회 실패", e);
}
}
/**
* ========================================
* POST 요청 예제 (주석 참고)
* ========================================
*
* POST 요청으로 변경하려면:
*
* 1. getHttpMethod() 변경:
* @Override
* protected String getHttpMethod() {
* return "POST";
* }
*
* 2. getRequestBody() 추가:
* @Override
* protected Object getRequestBody() {
* return ProductSearchRequest.builder()
* .startDate("2025-01-01")
* .endDate("2025-12-31")
* .categories(Arrays.asList("전자제품", "가구"))
* .minPrice(10000)
* .maxPrice(1000000)
* .build();
* }
*
* 3. Request DTO 생성:
* @Data
* @Builder
* public class ProductSearchRequest {
* private String startDate;
* private String endDate;
* private List<String> categories;
* private Integer minPrice;
* private Integer maxPrice;
* }
*
* 4. Query Parameter와 혼용 가능:
* - Query Parameter: URL에 추가되는 파라미터
* - Request Body: POST Body에 포함되는 데이터
*
* ========================================
* Path Variable 예제 (주석 참고)
* ========================================
*
* Path Variable 사용하려면:
*
* 1. getApiPath() 변경:
* @Override
* protected String getApiPath() {
* return "/api/v1/products/{productId}/details";
* }
*
* 2. getPathVariables() 추가:
* @Override
* protected Map<String, Object> getPathVariables() {
* Map<String, Object> pathVars = new HashMap<>();
* pathVars.put("productId", "PROD-001");
* return pathVars;
* }
*
* 결과 URL: /api/v1/products/PROD-001/details
*
* ========================================
* 다중 depth JSON 응답 예제
* ========================================
*
* 복잡한 JSON 구조:
* {
* "status": "success",
* "result": {
* "data": {
* "items": [
* { "productId": "PROD-001", "name": "..." }
* ],
* "pagination": {
* "page": 1,
* "totalPages": 10
* }
* }
* }
* }
*
* extractDataFromResponse() 구현:
* @Override
* protected List<ProductDto> extractDataFromResponse(Object response) {
* ComplexApiResponse apiResponse = (ComplexApiResponse) response;
* return apiResponse.getResult().getData().getItems();
* }
*/

파일 보기

@ -1,247 +0,0 @@
package com.snp.batch.jobs.sample.batch.reader;
import com.snp.batch.common.batch.reader.BaseApiReader;
import com.snp.batch.jobs.sample.batch.dto.ProductDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* 제품 데이터 Reader (Mock 데이터 생성)
* BaseApiReader v2.0을 상속하여 구현
*
* 특징:
* - WebClient 없이 Mock 데이터 생성 (실제 API 호출 X)
* - 테스트 샘플용 Reader
*
* 실전 API 연동 예제는 ProductApiReader.java 참고
*/
@Slf4j
@Component
public class ProductDataReader extends BaseApiReader<ProductDto> {
/**
* 기본 생성자 (WebClient 없이 Mock 데이터 생성)
*/
public ProductDataReader() {
super(); // WebClient 없이 초기화
}
// ========================================
// 필수 구현 메서드
// ========================================
@Override
protected String getReaderName() {
return "ProductDataReader";
}
@Override
protected List<ProductDto> fetchDataFromApi() {
log.info("========================================");
log.info("Mock 샘플 데이터 생성 시작");
log.info("========================================");
return generateMockData();
}
// ========================================
// 라이프사이클 (선택적 오버라이드)
// ========================================
@Override
protected void beforeFetch() {
log.info("[{}] Mock 데이터 생성 준비...", getReaderName());
}
@Override
protected void afterFetch(List<ProductDto> data) {
log.info("[{}] Mock 데이터 생성 완료: {}건", getReaderName(), getDataSize(data));
}
/**
* Mock 샘플 데이터 생성
* 다양한 데이터 타입 포함
*/
private List<ProductDto> generateMockData() {
log.info("========================================");
log.info("Mock 샘플 데이터 생성 시작");
log.info("다양한 데이터 타입 테스트용");
log.info("========================================");
List<ProductDto> products = new ArrayList<>();
// 샘플 1: 전자제품
products.add(ProductDto.builder()
.productId("PROD-001")
.productName("노트북 - MacBook Pro 16")
.category("전자제품")
.price(new BigDecimal("2999000.00"))
.stockQuantity(15)
.isActive(true)
.rating(4.8)
.manufactureDate(LocalDate.of(2024, 11, 15))
.weight(2.1f)
.salesCount(1250L)
.description("Apple M3 Max 칩셋, 64GB RAM, 2TB SSD. 프로페셔널을 위한 최고 성능의 노트북.")
.tags("[\"Apple\", \"Laptop\", \"Premium\", \"M3\"]")
.build());
// 샘플 2: 가구
products.add(ProductDto.builder()
.productId("PROD-002")
.productName("인체공학 사무용 의자")
.category("가구")
.price(new BigDecimal("450000.00"))
.stockQuantity(30)
.isActive(true)
.rating(4.5)
.manufactureDate(LocalDate.of(2024, 9, 20))
.weight(18.5f)
.salesCount(890L)
.description("허리 건강을 위한 메쉬 의자. 10시간 이상 장시간 착석 가능.")
.tags("[\"Office\", \"Ergonomic\", \"Furniture\"]")
.build());
// 샘플 3: 식품
products.add(ProductDto.builder()
.productId("PROD-003")
.productName("유기농 블루베리 (500g)")
.category("식품")
.price(new BigDecimal("12900.00"))
.stockQuantity(100)
.isActive(true)
.rating(4.9)
.manufactureDate(LocalDate.of(2025, 10, 10))
.weight(0.5f)
.salesCount(3450L)
.description("100% 국내산 유기농 블루베리. 신선하고 달콤합니다.")
.tags("[\"Organic\", \"Fruit\", \"Fresh\", \"Healthy\"]")
.build());
// 샘플 4: 의류
products.add(ProductDto.builder()
.productId("PROD-004")
.productName("겨울용 패딩 점퍼")
.category("의류")
.price(new BigDecimal("189000.00"))
.stockQuantity(50)
.isActive(true)
.rating(4.6)
.manufactureDate(LocalDate.of(2024, 10, 1))
.weight(1.2f)
.salesCount(2100L)
.description("방수 기능이 있는 오리털 패딩. 영하 20도까지 견딜 수 있습니다.")
.tags("[\"Winter\", \"Padding\", \"Waterproof\"]")
.build());
// 샘플 5: 도서
products.add(ProductDto.builder()
.productId("PROD-005")
.productName("클린 코드 (Clean Code)")
.category("도서")
.price(new BigDecimal("33000.00"))
.stockQuantity(200)
.isActive(true)
.rating(5.0)
.manufactureDate(LocalDate.of(2013, 12, 24))
.weight(0.8f)
.salesCount(15000L)
.description("Robert C. Martin의 명저. 읽기 좋은 코드를 작성하는 방법.")
.tags("[\"Programming\", \"Book\", \"Classic\", \"BestSeller\"]")
.build());
// 샘플 6: 비활성 제품 (테스트용)
products.add(ProductDto.builder()
.productId("PROD-006")
.productName("단종된 구형 스마트폰")
.category("전자제품")
.price(new BigDecimal("99000.00"))
.stockQuantity(0)
.isActive(false) // 비활성
.rating(3.2)
.manufactureDate(LocalDate.of(2020, 1, 15))
.weight(0.18f)
.salesCount(5000L)
.description("단종된 제품입니다.")
.tags("[\"Discontinued\", \"Old\"]")
.build());
// 샘플 7: NULL 테스트용
products.add(ProductDto.builder()
.productId("PROD-007")
.productName("일부 정보 누락된 제품")
.category("기타")
.price(new BigDecimal("10000.00"))
.stockQuantity(5)
.isActive(true)
.rating(null) // NULL
.manufactureDate(null) // NULL
.weight(null) // NULL
.salesCount(0L)
.description("일부 필드가 NULL인 테스트 데이터")
.tags(null) // NULL
.build());
// 샘플 8: 극단값 테스트
products.add(ProductDto.builder()
.productId("PROD-008")
.productName("초고가 명품 시계")
.category("악세서리")
.price(new BigDecimal("99999999.99")) // 최대값
.stockQuantity(1)
.isActive(true)
.rating(5.0)
.manufactureDate(LocalDate.of(2025, 1, 1))
.weight(0.15f)
.salesCount(999999999L) // 최대값
.description("세계 최고가의 명품 시계. 한정판 1개.")
.tags("[\"Luxury\", \"Watch\", \"Limited\"]")
.build());
// 샘플 9: 소수점 테스트
products.add(ProductDto.builder()
.productId("PROD-009")
.productName("초경량 블루투스 이어폰")
.category("전자제품")
.price(new BigDecimal("79900.50")) // 소수점
.stockQuantity(75)
.isActive(true)
.rating(4.35) // 소수점
.manufactureDate(LocalDate.of(2025, 8, 20))
.weight(0.045f) // 소수점
.salesCount(8765L)
.description("초경량 무선 이어폰. 배터리 24시간 사용 가능.")
.tags("[\"Bluetooth\", \"Earbuds\", \"Lightweight\"]")
.build());
// 샘플 10: 텍스트 테스트
products.add(ProductDto.builder()
.productId("PROD-010")
.productName("프리미엄 멀티 비타민")
.category("건강식품")
.price(new BigDecimal("45000.00"))
.stockQuantity(120)
.isActive(true)
.rating(4.7)
.manufactureDate(LocalDate.of(2025, 6, 1))
.weight(0.3f)
.salesCount(5432L)
.description("하루 한 알로 간편하게 섭취하는 종합 비타민입니다. " +
"비타민 A, B, C, D, E를 포함하여 총 12가지 필수 영양소가 함유되어 있습니다. " +
"GMP 인증 시설에서 제조되었으며, 식약처 인증을 받았습니다. " +
"현대인의 부족한 영양소를 한 번에 보충할 수 있습니다. " +
"임산부, 수유부, 어린이는 전문가와 상담 후 복용하시기 바랍니다.")
.tags("[\"Vitamin\", \"Health\", \"Supplement\", \"Daily\", \"GMP\"]")
.build());
log.info("총 {}개의 Mock 샘플 데이터 생성 완료", products.size());
log.info("데이터 타입: String, BigDecimal, Integer, Boolean, Double, LocalDate, Float, Long, TEXT");
return products;
}
}

파일 보기

@ -1,39 +0,0 @@
package com.snp.batch.jobs.sample.batch.repository;
import com.snp.batch.jobs.sample.batch.entity.ProductEntity;
import java.util.List;
import java.util.Optional;
/**
* 제품 Repository 인터페이스
* 구현체: ProductRepositoryImpl (JdbcTemplate 기반)
*/
public interface ProductRepository {
// CRUD 메서드
Optional<ProductEntity> findById(Long id);
List<ProductEntity> findAll();
long count();
boolean existsById(Long id);
ProductEntity save(ProductEntity entity);
void saveAll(List<ProductEntity> entities);
void deleteById(Long id);
void deleteAll();
// 커스텀 메서드
/**
* 제품 ID로 조회
*/
Optional<ProductEntity> findByProductId(String productId);
/**
* 제품 ID 존재 여부 확인
*/
boolean existsByProductId(String productId);
/**
* 페이징 조회
*/
List<ProductEntity> findAllWithPaging(int offset, int limit);
}

파일 보기

@ -1,191 +0,0 @@
package com.snp.batch.jobs.sample.batch.repository;
import com.snp.batch.common.batch.repository.BaseJdbcRepository;
import com.snp.batch.jobs.sample.batch.entity.ProductEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
/**
* Product Repository (JdbcTemplate 기반)
*/
@Slf4j
@Repository("productRepository")
public class ProductRepositoryImpl extends BaseJdbcRepository<ProductEntity, Long> implements ProductRepository {
public ProductRepositoryImpl(JdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}
@Override
protected String getTableName() {
return "sample_products";
}
@Override
protected String getEntityName() {
return "Product";
}
@Override
protected RowMapper<ProductEntity> getRowMapper() {
return new ProductEntityRowMapper();
}
@Override
protected Long extractId(ProductEntity entity) {
return entity.getId();
}
@Override
protected String getInsertSql() {
return """
INSERT INTO sample_products (
product_id, product_name, category, price, stock_quantity,
is_active, rating, manufacture_date, weight, sales_count,
description, tags, created_at, updated_at, created_by, updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
}
@Override
protected String getUpdateSql() {
return """
UPDATE sample_products
SET product_name = ?,
category = ?,
price = ?,
stock_quantity = ?,
is_active = ?,
rating = ?,
manufacture_date = ?,
weight = ?,
sales_count = ?,
description = ?,
tags = ?,
updated_at = ?,
updated_by = ?
WHERE id = ?
""";
}
@Override
protected void setInsertParameters(PreparedStatement ps, ProductEntity entity) throws Exception {
int idx = 1;
ps.setString(idx++, entity.getProductId());
ps.setString(idx++, entity.getProductName());
ps.setString(idx++, entity.getCategory());
ps.setBigDecimal(idx++, entity.getPrice());
ps.setObject(idx++, entity.getStockQuantity());
ps.setObject(idx++, entity.getIsActive());
ps.setObject(idx++, entity.getRating());
ps.setObject(idx++, entity.getManufactureDate());
ps.setObject(idx++, entity.getWeight());
ps.setObject(idx++, entity.getSalesCount());
ps.setString(idx++, entity.getDescription());
ps.setString(idx++, entity.getTags());
ps.setTimestamp(idx++, entity.getCreatedAt() != null ?
Timestamp.valueOf(entity.getCreatedAt()) : Timestamp.valueOf(now()));
ps.setTimestamp(idx++, entity.getUpdatedAt() != null ?
Timestamp.valueOf(entity.getUpdatedAt()) : Timestamp.valueOf(now()));
ps.setString(idx++, entity.getCreatedBy() != null ? entity.getCreatedBy() : "SYSTEM");
ps.setString(idx++, entity.getUpdatedBy() != null ? entity.getUpdatedBy() : "SYSTEM");
}
@Override
protected void setUpdateParameters(PreparedStatement ps, ProductEntity entity) throws Exception {
int idx = 1;
ps.setString(idx++, entity.getProductName());
ps.setString(idx++, entity.getCategory());
ps.setBigDecimal(idx++, entity.getPrice());
ps.setObject(idx++, entity.getStockQuantity());
ps.setObject(idx++, entity.getIsActive());
ps.setObject(idx++, entity.getRating());
ps.setObject(idx++, entity.getManufactureDate());
ps.setObject(idx++, entity.getWeight());
ps.setObject(idx++, entity.getSalesCount());
ps.setString(idx++, entity.getDescription());
ps.setString(idx++, entity.getTags());
ps.setTimestamp(idx++, Timestamp.valueOf(now()));
ps.setString(idx++, entity.getUpdatedBy() != null ? entity.getUpdatedBy() : "SYSTEM");
ps.setLong(idx++, entity.getId());
}
// ==================== 커스텀 쿼리 메서드 ====================
/**
* Product ID로 조회
*/
@Override
public Optional<ProductEntity> findByProductId(String productId) {
String sql = "SELECT * FROM sample_products WHERE product_id = ?";
return executeQueryForObject(sql, productId);
}
/**
* Product ID 존재 여부 확인
*/
@Override
public boolean existsByProductId(String productId) {
String sql = "SELECT COUNT(*) FROM sample_products WHERE product_id = ?";
Long count = jdbcTemplate.queryForObject(sql, Long.class, productId);
return count != null && count > 0;
}
/**
* 페이징 조회
*/
@Override
public List<ProductEntity> findAllWithPaging(int offset, int limit) {
String sql = "SELECT * FROM sample_products ORDER BY id DESC LIMIT ? OFFSET ?";
return executeQueryForList(sql, limit, offset);
}
// ==================== RowMapper ====================
private static class ProductEntityRowMapper implements RowMapper<ProductEntity> {
@Override
public ProductEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
ProductEntity entity = ProductEntity.builder()
.id(rs.getLong("id"))
.productId(rs.getString("product_id"))
.productName(rs.getString("product_name"))
.category(rs.getString("category"))
.price(rs.getBigDecimal("price"))
.stockQuantity((Integer) rs.getObject("stock_quantity"))
.isActive((Boolean) rs.getObject("is_active"))
.rating((Double) rs.getObject("rating"))
.manufactureDate(rs.getDate("manufacture_date") != null ?
rs.getDate("manufacture_date").toLocalDate() : null)
.weight((Float) rs.getObject("weight"))
.salesCount((Long) rs.getObject("sales_count"))
.description(rs.getString("description"))
.tags(rs.getString("tags"))
.build();
// BaseEntity 필드 매핑
Timestamp createdAt = rs.getTimestamp("created_at");
if (createdAt != null) {
entity.setCreatedAt(createdAt.toLocalDateTime());
}
Timestamp updatedAt = rs.getTimestamp("updated_at");
if (updatedAt != null) {
entity.setUpdatedAt(updatedAt.toLocalDateTime());
}
entity.setCreatedBy(rs.getString("created_by"));
entity.setUpdatedBy(rs.getString("updated_by"));
return entity;
}
}
}

파일 보기

@ -1,43 +0,0 @@
package com.snp.batch.jobs.sample.batch.writer;
import com.snp.batch.common.batch.writer.BaseWriter;
import com.snp.batch.jobs.sample.batch.entity.OrderItemEntity;
import com.snp.batch.jobs.sample.batch.processor.OrderDataProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 주문 상품 Writer (복잡한 JSON 예제용)
* OrderWrapper에서 OrderItemEntity 리스트만 추출하여 저장
*/
@Slf4j
@Component
public class OrderItemWriter extends BaseWriter<OrderDataProcessor.OrderWrapper> {
public OrderItemWriter() {
super("OrderItem");
}
@Override
protected void writeItems(List<OrderDataProcessor.OrderWrapper> wrappers) throws Exception {
// OrderWrapper에서 OrderItemEntity 리스트만 추출 (flatten)
List<OrderItemEntity> allItems = wrappers.stream()
.flatMap(wrapper -> wrapper.getItems().stream())
.collect(Collectors.toList());
log.info("주문 상품 데이터 저장: {} 건", allItems.size());
// 실제 구현 OrderItemRepository.saveAll(allItems) 호출
// 예제이므로 로그만 출력
for (OrderItemEntity item : allItems) {
log.info("주문 상품 저장: orderId={}, productId={}, quantity={}, price={}",
item.getOrderId(),
item.getProductId(),
item.getQuantity(),
item.getPrice());
}
}
}

파일 보기

@ -1,42 +0,0 @@
package com.snp.batch.jobs.sample.batch.writer;
import com.snp.batch.common.batch.writer.BaseWriter;
import com.snp.batch.jobs.sample.batch.entity.OrderEntity;
import com.snp.batch.jobs.sample.batch.processor.OrderDataProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 주문 Writer (복잡한 JSON 예제용)
* OrderWrapper에서 OrderEntity만 추출하여 저장
*/
@Slf4j
@Component
public class OrderWriter extends BaseWriter<OrderDataProcessor.OrderWrapper> {
public OrderWriter() {
super("Order");
}
@Override
protected void writeItems(List<OrderDataProcessor.OrderWrapper> wrappers) throws Exception {
// OrderWrapper에서 OrderEntity만 추출
List<OrderEntity> orders = wrappers.stream()
.map(OrderDataProcessor.OrderWrapper::getOrder)
.collect(Collectors.toList());
log.info("주문 데이터 저장: {} 건", orders.size());
// 실제 구현 OrderRepository.saveAll(orders) 호출
// 예제이므로 로그만 출력
for (OrderEntity order : orders) {
log.info("주문 저장: orderId={}, customer={}, total={}",
order.getOrderId(),
order.getCustomerName(),
order.getTotalAmount());
}
}
}

파일 보기

@ -1,42 +0,0 @@
package com.snp.batch.jobs.sample.batch.writer;
import com.snp.batch.common.batch.writer.BaseWriter;
import com.snp.batch.jobs.sample.batch.entity.ProductEntity;
import com.snp.batch.jobs.sample.batch.repository.ProductRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 제품 데이터 Writer
* BaseWriter를 상속하여 구현
*/
@Slf4j
@Component
public class ProductDataWriter extends BaseWriter<ProductEntity> {
private final ProductRepository productRepository;
public ProductDataWriter(ProductRepository productRepository) {
super("Product");
this.productRepository = productRepository;
}
@Override
protected void writeItems(List<ProductEntity> items) throws Exception {
// Repository의 saveAll() 메서드 호출
productRepository.saveAll(items);
// 저장된 제품 목록 출력
log.info("========================================");
items.forEach(product ->
log.info("✓ 저장 완료: {} - {} (가격: {}원, 재고: {}개)",
product.getProductId(),
product.getProductName(),
product.getPrice(),
product.getStockQuantity())
);
log.info("========================================");
}
}

파일 보기

@ -1,129 +0,0 @@
package com.snp.batch.jobs.sample.web.controller;
import com.snp.batch.common.web.ApiResponse;
import com.snp.batch.common.web.controller.BaseController;
import com.snp.batch.common.web.service.BaseService;
import com.snp.batch.jobs.sample.web.dto.ProductWebDto;
import com.snp.batch.jobs.sample.web.service.ProductWebService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 제품 API 컨트롤러 (샘플)
* BaseController를 상속하여 공통 CRUD 엔드포인트 자동 생성
*
* 제공되는 엔드포인트:
* - POST /api/products : 제품 생성
* - GET /api/products/{id} : 제품 조회
* - GET /api/products : 전체 제품 조회
* - GET /api/products/page : 페이징 조회
* - PUT /api/products/{id} : 제품 수정
* - DELETE /api/products/{id} : 제품 삭제
* - GET /api/products/{id}/exists : 존재 여부 확인
*
* 커스텀 엔드포인트:
* - GET /api/products/by-product-id/{productId} : 제품 ID로 조회
* - GET /api/products/stats/active-count : 활성 제품 개수
*/
@Slf4j
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
@Tag(name = "Product API", description = "제품 관리 API (샘플)")
public class ProductWebController extends BaseController<ProductWebDto, Long> {
private final ProductWebService productWebService;
@Override
protected BaseService<?, ProductWebDto, Long> getService() {
return productWebService;
}
@Override
protected String getResourceName() {
return "Product";
}
// ==================== 커스텀 엔드포인트 ====================
/**
* 제품 ID로 조회 (비즈니스 조회)
*
* @param productId 제품 ID (: PROD-001)
* @return 제품 DTO
*/
@Operation(
summary = "제품 코드로 조회",
description = "제품 코드(비즈니스 키)로 제품을 조회합니다",
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "조회 성공"
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "제품 없음"
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "500",
description = "서버 오류"
)
}
)
@GetMapping("/by-product-id/{productId}")
public ResponseEntity<ApiResponse<ProductWebDto>> getByProductId(
@Parameter(description = "제품 코드", required = true, example = "PROD-001")
@PathVariable String productId) {
log.info("제품 ID로 조회 요청: {}", productId);
try {
ProductWebDto product = productWebService.findByProductId(productId);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(ApiResponse.success(product));
} catch (Exception e) {
log.error("제품 ID 조회 실패: {}", productId, e);
return ResponseEntity.internalServerError().body(
ApiResponse.error("Failed to get product by productId: " + e.getMessage())
);
}
}
/**
* 활성 제품 개수 조회
*
* @return 활성 제품
*/
@Operation(
summary = "활성 제품 개수 조회",
description = "현재 활성화된 제품의 총 개수를 조회합니다",
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "조회 성공"
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "500",
description = "서버 오류"
)
}
)
@GetMapping("/stats/active-count")
public ResponseEntity<ApiResponse<Long>> getActiveCount() {
log.info("활성 제품 개수 조회 요청");
try {
long count = productWebService.countActiveProducts();
return ResponseEntity.ok(ApiResponse.success("Active product count", count));
} catch (Exception e) {
log.error("활성 제품 개수 조회 실패", e);
return ResponseEntity.internalServerError().body(
ApiResponse.error("Failed to get active product count: " + e.getMessage())
);
}
}
}

파일 보기

@ -1,61 +0,0 @@
package com.snp.batch.jobs.sample.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 제품 API 응답 DTO
* DB에 저장된 제품 데이터를 외부에 제공할 사용
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "제품 정보 응답 DTO")
public class ProductResponseDto {
@Schema(description = "제품 ID (Primary Key)", example = "1")
private Long id;
@Schema(description = "제품 코드", example = "PROD-001")
private String productId;
@Schema(description = "제품명", example = "노트북 - MacBook Pro 16")
private String productName;
@Schema(description = "카테고리", example = "전자제품")
private String category;
@Schema(description = "가격", example = "2999000.00")
private BigDecimal price;
@Schema(description = "재고 수량", example = "15")
private Integer stockQuantity;
@Schema(description = "활성화 여부", example = "true")
private Boolean isActive;
@Schema(description = "평점", example = "4.8")
private Double rating;
@Schema(description = "제조일", example = "2024-11-15")
private LocalDate manufactureDate;
@Schema(description = "무게 (kg)", example = "2.1")
private Float weight;
@Schema(description = "판매 수량", example = "1250")
private Long salesCount;
@Schema(description = "제품 설명", example = "Apple M3 Max 칩셋, 64GB RAM, 2TB SSD")
private String description;
@Schema(description = "태그 (JSON 문자열)", example = "[\"Apple\", \"Laptop\", \"Premium\"]")
private String tags;
}

파일 보기

@ -1,85 +0,0 @@
package com.snp.batch.jobs.sample.web.dto;
import com.snp.batch.common.web.dto.BaseDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 제품 DTO (샘플)
* BaseDto를 상속하여 감사 필드 자동 포함
*
* DTO는 API에서 사용되며, 배치 DTO와는 별도로 관리됩니다.
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductWebDto extends BaseDto {
/**
* 제품 ID (비즈니스 )
*/
private String productId;
/**
* 제품명
*/
private String productName;
/**
* 카테고리
*/
private String category;
/**
* 가격
*/
private BigDecimal price;
/**
* 재고 수량
*/
private Integer stockQuantity;
/**
* 활성 여부
*/
private Boolean isActive;
/**
* 평점
*/
private Double rating;
/**
* 제조일자
*/
private LocalDate manufactureDate;
/**
* 무게 (kg)
*/
private Float weight;
/**
* 판매 횟수
*/
private Long salesCount;
/**
* 설명
*/
private String description;
/**
* 태그 (JSON 문자열)
*/
private String tags;
}

파일 보기

@ -1,144 +0,0 @@
package com.snp.batch.jobs.sample.web.service;
import com.snp.batch.common.batch.repository.BaseJdbcRepository;
import com.snp.batch.common.web.service.BaseServiceImpl;
import com.snp.batch.jobs.sample.batch.entity.ProductEntity;
import com.snp.batch.jobs.sample.batch.repository.ProductRepository;
import com.snp.batch.jobs.sample.batch.repository.ProductRepositoryImpl;
import com.snp.batch.jobs.sample.web.dto.ProductWebDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 제품 서비스 (샘플) - JDBC 기반
* BaseServiceImpl을 상속하여 공통 CRUD 기능 구현
*
* 서비스는 API에서 사용되며, 배치 작업과는 별도로 동작합니다.
* - Batch: ProductDataReader/Processor/Writer (배치 데이터 처리)
* - Web: ProductWebService/Controller (REST API)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductWebService extends BaseServiceImpl<ProductEntity, ProductWebDto, Long> {
private final ProductRepositoryImpl productRepository;
@Override
protected BaseJdbcRepository<ProductEntity, Long> getRepository() {
return productRepository;
}
@Override
protected String getEntityName() {
return "Product";
}
@Override
public ProductWebDto toDto(ProductEntity entity) {
if (entity == null) {
return null;
}
ProductWebDto dto = ProductWebDto.builder()
.productId(entity.getProductId())
.productName(entity.getProductName())
.category(entity.getCategory())
.price(entity.getPrice())
.stockQuantity(entity.getStockQuantity())
.isActive(entity.getIsActive())
.rating(entity.getRating())
.manufactureDate(entity.getManufactureDate())
.weight(entity.getWeight())
.salesCount(entity.getSalesCount())
.description(entity.getDescription())
.tags(entity.getTags())
.build();
// BaseDto 필드 설정
dto.setCreatedAt(entity.getCreatedAt());
dto.setUpdatedAt(entity.getUpdatedAt());
dto.setCreatedBy(entity.getCreatedBy());
dto.setUpdatedBy(entity.getUpdatedBy());
return dto;
}
@Override
public ProductEntity toEntity(ProductWebDto dto) {
if (dto == null) {
return null;
}
return ProductEntity.builder()
.productId(dto.getProductId())
.productName(dto.getProductName())
.category(dto.getCategory())
.price(dto.getPrice())
.stockQuantity(dto.getStockQuantity())
.isActive(dto.getIsActive())
.rating(dto.getRating())
.manufactureDate(dto.getManufactureDate())
.weight(dto.getWeight())
.salesCount(dto.getSalesCount())
.description(dto.getDescription())
.tags(dto.getTags())
.build();
}
@Override
protected void updateEntity(ProductEntity entity, ProductWebDto dto) {
// 필드 업데이트
entity.setProductName(dto.getProductName());
entity.setCategory(dto.getCategory());
entity.setPrice(dto.getPrice());
entity.setStockQuantity(dto.getStockQuantity());
entity.setIsActive(dto.getIsActive());
entity.setRating(dto.getRating());
entity.setManufactureDate(dto.getManufactureDate());
entity.setWeight(dto.getWeight());
entity.setSalesCount(dto.getSalesCount());
entity.setDescription(dto.getDescription());
entity.setTags(dto.getTags());
log.debug("Product 업데이트: {}", entity.getProductId());
}
@Override
protected Long extractId(ProductEntity entity) {
return entity.getId();
}
@Override
protected List<ProductEntity> executePagingQuery(int offset, int limit) {
// JDBC 페이징 쿼리 실행
return productRepository.findAllWithPaging(offset, limit);
}
/**
* 커스텀 메서드: 제품 ID로 조회
*
* @param productId 제품 ID (비즈니스 )
* @return 제품 DTO
*/
public ProductWebDto findByProductId(String productId) {
log.debug("제품 ID로 조회: {}", productId);
return productRepository.findByProductId(productId)
.map(this::toDto)
.orElse(null);
}
/**
* 커스텀 메서드: 활성 제품 개수
*
* @return 활성 제품
*/
public long countActiveProducts() {
long total = productRepository.count();
log.debug("전체 제품 수: {}", total);
return total;
}
}

파일 보기

@ -1,25 +0,0 @@
package com.snp.batch.jobs.sanction.batch.writer;
import com.snp.batch.common.batch.writer.BaseWriter;
import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity;
import com.snp.batch.jobs.sanction.batch.repository.ComplianceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class ComplianceDataWriter extends BaseWriter<ComplianceEntity> {
private final ComplianceRepository complianceRepository;
public ComplianceDataWriter(ComplianceRepository complianceRepository) {
super("complianceRepository");
this.complianceRepository = complianceRepository;
}
@Override
protected void writeItems(List<ComplianceEntity> items) throws Exception {
complianceRepository.saveComplianceAll(items);
}
}

파일 보기

@ -5,6 +5,9 @@ import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
@Service @Service
@ -46,6 +49,29 @@ public class BatchDateService {
return params; return params;
} }
public Map<String, String> getRiskComplianceApiDateParams(String apiKey) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SS'Z'");
// 1. 마지막 성공 일자 (FROM 날짜) DB에서 조회
// 조회된 값이 없으면 (최초 실행), API 호출 시점의 하루 날짜를 사용합니다.
LocalDate lastDate = repository.findLastSuccessDate(apiKey)
.orElse(LocalDate.now().minusDays(1));
// 2. 현재 실행 시점의 일자 (TO 날짜) 계산
ZonedDateTime nowUtc = ZonedDateTime.now(ZoneOffset.UTC);
// 3. 파라미터 Map 구성
Map<String, String> params = new HashMap<>();
// FROM Parameters (DB 조회 )
String fromDateStr = lastDate.atStartOfDay().format(formatter);
params.put("fromDate", fromDateStr);
// TO Parameters (현재 시점 )
String toDateStr = nowUtc.format(formatter);
params.put("toDate", toDateStr);
return params;
}
/** /**
* 배치 성공 , 다음 실행을 위해 to 날짜를 DB에 저장 업데이트합니다. * 배치 성공 , 다음 실행을 위해 to 날짜를 DB에 저장 업데이트합니다.
* @param successDate API 호출 성공 사용된 to 날짜 * @param successDate API 호출 성공 사용된 to 날짜