diff --git a/src/main/java/com/snp/batch/jobs/sanction/batch/config/SanctionUpdateJobConfig.java b/src/main/java/com/snp/batch/jobs/compliance/batch/config/ComplianceImportJobConfig.java similarity index 75% rename from src/main/java/com/snp/batch/jobs/sanction/batch/config/SanctionUpdateJobConfig.java rename to src/main/java/com/snp/batch/jobs/compliance/batch/config/ComplianceImportJobConfig.java index 1707cdd..5dfff81 100644 --- a/src/main/java/com/snp/batch/jobs/sanction/batch/config/SanctionUpdateJobConfig.java +++ b/src/main/java/com/snp/batch/jobs/compliance/batch/config/ComplianceImportJobConfig.java @@ -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.jobs.sanction.batch.dto.ComplianceDto; -import com.snp.batch.jobs.sanction.batch.entity.ComplianceEntity; -import com.snp.batch.jobs.sanction.batch.processor.ComplianceDataProcessor; -import com.snp.batch.jobs.sanction.batch.reader.ComplianceDataReader; -import com.snp.batch.jobs.sanction.batch.writer.ComplianceDataWriter; +import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto; +import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity; +import com.snp.batch.jobs.compliance.batch.processor.ComplianceDataProcessor; +import com.snp.batch.jobs.compliance.batch.reader.ComplianceDataReader; +import com.snp.batch.jobs.compliance.batch.writer.ComplianceDataWriter; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; @@ -22,7 +22,7 @@ import org.springframework.web.reactive.function.client.WebClient; @Slf4j @Configuration -public class SanctionUpdateJobConfig extends BaseJobConfig { +public class ComplianceImportJobConfig extends BaseJobConfig { private final JdbcTemplate jdbcTemplate; private final WebClient maritimeServiceApiWebClient; @@ -34,7 +34,7 @@ public class SanctionUpdateJobConfig extends BaseJobConfig { +public class ComplianceImportRangeJobConfig extends BaseJobConfig { private final JdbcTemplate jdbcTemplate; private final WebClient maritimeServiceApiWebClient; private final ComplianceDataProcessor complianceDataProcessor; private final ComplianceDataWriter complianceDataWriter; private final ComplianceDataRangeReader complianceDataRangeReader; + private final BatchDateService batchDateService; @Override protected int getChunkSize() { - return 5000; // API에서 5000개씩 가져오므로 chunk도 5000으로 설정 + return 10000; } - public SanctionUpdateRangeJobConfig( + public ComplianceImportRangeJobConfig( JobRepository jobRepository, PlatformTransactionManager transactionManager, ComplianceDataProcessor complianceDataProcessor, ComplianceDataWriter complianceDataWriter, JdbcTemplate jdbcTemplate, - @Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient, ComplianceDataRangeReader complianceDataRangeReader) { + @Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient, + ComplianceDataRangeReader complianceDataRangeReader, + BatchDateService batchDateService) { super(jobRepository, transactionManager); this.jdbcTemplate = jdbcTemplate; this.maritimeServiceApiWebClient = maritimeServiceApiWebClient; this.complianceDataProcessor = complianceDataProcessor; this.complianceDataWriter = complianceDataWriter; this.complianceDataRangeReader = complianceDataRangeReader; + this.batchDateService = batchDateService; } @Override protected String getJobName() { - return "SanctionRangeUpdateJob"; + return "ComplianceImportRangeJob"; } @Override protected String getStepName() { - return "SanctionRangeUpdateStep"; + return "ComplianceImportRangeStep"; } @Override @@ -69,11 +72,8 @@ public class SanctionUpdateRangeJobConfig extends BaseJobConfig createProcessor() { @@ -85,13 +85,13 @@ public class SanctionUpdateRangeJobConfig extends BaseJobConfig { - //TODO : - // 1. Core20 IMO_NUMBER 전체 조회 - // 2. IMO번호에 대한 마지막 AIS 신호 요청 (1회 최대 5000개 : Chunk 단위로 반복) - // 3. Response Data -> Core20에 업데이트 (Chunk 단위로 반복) - - //private final JdbcTemplate jdbcTemplate; - + private final JdbcTemplate jdbcTemplate; + private final BatchDateService batchDateService; // ✨ BatchDateService 필드 추가 private List allData; private int currentBatchIndex = 0; - private final int batchSize = 100; + private final int batchSize = 1000; private String fromDate; private String toDate; - public ComplianceDataRangeReader(WebClient webClient, - @Value("#{jobParameters['fromDate']}") String fromDate, - @Value("#{jobParameters['toDate']}") String toDate) { + public ComplianceDataRangeReader(WebClient webClient, JdbcTemplate jdbcTemplate, BatchDateService batchDateService) { super(webClient); - - // 날짜가 없으면 전날 하루 기준 - 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; - } - + this.jdbcTemplate = jdbcTemplate; + this.batchDateService = batchDateService; enableChunkMode(); } @@ -65,15 +48,8 @@ public class ComplianceDataRangeReader extends BaseApiReader { return "/RiskAndCompliance/UpdatedComplianceList"; } - private String getTargetTable(){ - return "snp_data.core20"; - } - 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); + protected String getApiKey() { + return "COMPLIANCE_IMPORT_API"; } @Override @@ -81,7 +57,7 @@ public class ComplianceDataRangeReader extends BaseApiReader { // 모든 배치 처리 완료 확인 if (allData == null) { log.info("[{}] 최초 API 조회 실행: {} ~ {}", getReaderName(), fromDate, toDate); - allData = callApiWithBatch(fromDate, toDate); + allData = callApiWithBatch(); if (allData == null || allData.isEmpty()) { log.warn("[{}] 조회된 데이터 없음 → 종료", getReaderName()); @@ -115,11 +91,30 @@ public class ComplianceDataRangeReader extends BaseApiReader { return batch; } - private List callApiWithBatch(String fromDate, String stopDate) { - String url = getApiPath() + "?fromDate=" + fromDate +"&stopDate=" + stopDate; + @Override + protected void afterFetch(List data) { + try{ + if (data == null) { + log.info("[{}] 배치 처리 성공", getReaderName()); + } + }catch (Exception e){ + log.info("[{}] 배치 처리 실패", getReaderName()); + log.info("[{}] API 호출 종료", getReaderName()); + } + } + + private List callApiWithBatch() { + Map params = batchDateService.getRiskComplianceApiDateParams(getApiKey()); + log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), params.get("fromDate"), params.get("toDate")); + + String url = getApiPath(); log.debug("[{}] API 호출: {}", getReaderName(), url); return webClient.get() - .uri(url) + .uri(url, uriBuilder -> uriBuilder + // 맵에서 파라미터 값을 동적으로 가져와 세팅 + .queryParam("fromDate", params.get("fromDate")) + .queryParam("toDate", params.get("toDate")) + .build()) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) .block(); diff --git a/src/main/java/com/snp/batch/jobs/sanction/batch/reader/ComplianceDataReader.java b/src/main/java/com/snp/batch/jobs/compliance/batch/reader/ComplianceDataReader.java similarity index 96% rename from src/main/java/com/snp/batch/jobs/sanction/batch/reader/ComplianceDataReader.java rename to src/main/java/com/snp/batch/jobs/compliance/batch/reader/ComplianceDataReader.java index 1ba57a9..aff76d7 100644 --- a/src/main/java/com/snp/batch/jobs/sanction/batch/reader/ComplianceDataReader.java +++ b/src/main/java/com/snp/batch/jobs/compliance/batch/reader/ComplianceDataReader.java @@ -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.jobs.sanction.batch.dto.ComplianceDto; -import com.snp.batch.jobs.sanction.batch.dto.ComplianceResponse; +import com.snp.batch.jobs.compliance.batch.dto.ComplianceDto; import lombok.extern.slf4j.Slf4j; import org.springframework.core.ParameterizedTypeReference; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/src/main/java/com/snp/batch/jobs/sanction/batch/repository/ComplianceRepository.java b/src/main/java/com/snp/batch/jobs/compliance/batch/repository/ComplianceRepository.java similarity index 50% rename from src/main/java/com/snp/batch/jobs/sanction/batch/repository/ComplianceRepository.java rename to src/main/java/com/snp/batch/jobs/compliance/batch/repository/ComplianceRepository.java index e3270b3..0656693 100644 --- a/src/main/java/com/snp/batch/jobs/sanction/batch/repository/ComplianceRepository.java +++ b/src/main/java/com/snp/batch/jobs/compliance/batch/repository/ComplianceRepository.java @@ -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; diff --git a/src/main/java/com/snp/batch/jobs/sanction/batch/repository/ComplianceRepositoryImpl.java b/src/main/java/com/snp/batch/jobs/compliance/batch/repository/ComplianceRepositoryImpl.java similarity index 97% rename from src/main/java/com/snp/batch/jobs/sanction/batch/repository/ComplianceRepositoryImpl.java rename to src/main/java/com/snp/batch/jobs/compliance/batch/repository/ComplianceRepositoryImpl.java index e4ace50..64df157 100644 --- a/src/main/java/com/snp/batch/jobs/sanction/batch/repository/ComplianceRepositoryImpl.java +++ b/src/main/java/com/snp/batch/jobs/compliance/batch/repository/ComplianceRepositoryImpl.java @@ -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.jobs.sanction.batch.entity.ComplianceEntity; +import com.snp.batch.jobs.compliance.batch.entity.ComplianceEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -12,7 +12,7 @@ import java.sql.Types; import java.util.List; @Slf4j -@Repository("complianceRepository") +@Repository("ComplianceRepository") public class ComplianceRepositoryImpl extends BaseJdbcRepository implements ComplianceRepository { public ComplianceRepositoryImpl(JdbcTemplate jdbcTemplate) { @@ -42,7 +42,7 @@ public class ComplianceRepositoryImpl extends BaseJdbcRepository { + + 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 items) throws Exception { + complianceRepository.saveComplianceAll(items); + LocalDate successDate = LocalDate.now(); // 현재 배치 실행 시점의 날짜 (Reader의 toDay와 동일한 값) + batchDateService.updateLastSuccessDate(getApiKey(), successDate); + log.info("batch_last_execution update 완료 : {}", getApiKey()); + } +} diff --git a/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportJobConfig.java b/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportJobConfig.java index 5aba1ab..4e04f9d 100644 --- a/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportJobConfig.java +++ b/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportJobConfig.java @@ -50,12 +50,12 @@ public class RiskImportJobConfig extends BaseJobConfig { @Override protected String getJobName() { - return "riskImportJob"; + return "RiskImportJob"; } @Override protected String getStepName() { - return "riskImportStep"; + return "RiskImportStep"; } @Override @@ -71,12 +71,12 @@ public class RiskImportJobConfig extends BaseJobConfig { @Override protected ItemWriter createWriter() { return riskDataWriter; } - @Bean(name = "riskImportJob") + @Bean(name = "RiskImportJob") public Job riskImportJob() { return job(); } - @Bean(name = "riskImportStep") + @Bean(name = "RiskImportStep") public Step riskImportStep() { return step(); } diff --git a/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportRangeJobConfig.java b/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportRangeJobConfig.java index 1ff38d5..00e8bb8 100644 --- a/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportRangeJobConfig.java +++ b/src/main/java/com/snp/batch/jobs/risk/batch/config/RiskImportRangeJobConfig.java @@ -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.processor.RiskDataProcessor; 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.sanction.batch.reader.ComplianceDataRangeReader; +import com.snp.batch.service.BatchDateService; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; @@ -31,10 +30,11 @@ public class RiskImportRangeJobConfig extends BaseJobConfig private final RiskDataProcessor riskDataProcessor; private final RiskDataWriter riskDataWriter; private final RiskDataRangeReader riskDataRangeReader; - + private final JdbcTemplate jdbcTemplate; + private final BatchDateService batchDateService; @Override protected int getChunkSize() { - return 5000; // API에서 5000개씩 가져오므로 chunk도 5000으로 설정 + return 10000; } public RiskImportRangeJobConfig( JobRepository jobRepository, @@ -42,12 +42,16 @@ public class RiskImportRangeJobConfig extends BaseJobConfig RiskDataProcessor riskDataProcessor, RiskDataWriter riskDataWriter, JdbcTemplate jdbcTemplate, - @Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient, RiskDataRangeReader riskDataRangeReader) { + @Qualifier("maritimeServiceApiWebClient")WebClient maritimeServiceApiWebClient, + RiskDataRangeReader riskDataRangeReader, + BatchDateService batchDateService) { super(jobRepository, transactionManager); this.maritimeServiceApiWebClient = maritimeServiceApiWebClient; this.riskDataProcessor = riskDataProcessor; this.riskDataWriter = riskDataWriter; + this.jdbcTemplate = jdbcTemplate; this.riskDataRangeReader = riskDataRangeReader; + this.batchDateService = batchDateService; } @Override @@ -66,11 +70,8 @@ public class RiskImportRangeJobConfig extends BaseJobConfig } @Bean @StepScope - public RiskDataRangeReader riskDataRangeReader( - @Value("#{jobParameters['fromDate']}") String startDate, - @Value("#{jobParameters['toDate']}") String stopDate - ) { - return new RiskDataRangeReader(maritimeServiceApiWebClient, startDate, stopDate); + public RiskDataRangeReader riskDataRangeReader() { + return new RiskDataRangeReader(maritimeServiceApiWebClient, jdbcTemplate, batchDateService); } @Override diff --git a/src/main/java/com/snp/batch/jobs/risk/batch/reader/RiskDataRangeReader.java b/src/main/java/com/snp/batch/jobs/risk/batch/reader/RiskDataRangeReader.java index a29dd09..37b5550 100644 --- a/src/main/java/com/snp/batch/jobs/risk/batch/reader/RiskDataRangeReader.java +++ b/src/main/java/com/snp/batch/jobs/risk/batch/reader/RiskDataRangeReader.java @@ -1,8 +1,9 @@ package com.snp.batch.jobs.risk.batch.reader; 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.sanction.batch.dto.ComplianceDto; +import com.snp.batch.service.BatchDateService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; @@ -11,45 +12,29 @@ import org.springframework.web.reactive.function.client.WebClient; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.Collections; import java.util.List; +import java.util.Map; @Slf4j public class RiskDataRangeReader extends BaseApiReader { - //TODO : - // 1. Core20 IMO_NUMBER 전체 조회 - // 2. IMO번호에 대한 마지막 AIS 신호 요청 (1회 최대 5000개 : Chunk 단위로 반복) - // 3. Response Data -> Core20에 업데이트 (Chunk 단위로 반복) - + private final JdbcTemplate jdbcTemplate; + private final BatchDateService batchDateService; // ✨ BatchDateService 필드 추가 private List allData; private int currentBatchIndex = 0; - private final int batchSize = 100; + private final int batchSize = 1000; private String fromDate; private String toDate; - public RiskDataRangeReader(WebClient webClient, - @Value("#{jobParameters['fromDate']}") String fromDate, - @Value("#{jobParameters['toDate']}") String toDate) { + public RiskDataRangeReader(WebClient webClient, JdbcTemplate jdbcTemplate, BatchDateService batchDateService) { super(webClient); - - // 날짜가 없으면 전날 하루 기준 - 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; - } - + this.jdbcTemplate = jdbcTemplate; + this.batchDateService = batchDateService; enableChunkMode(); } @Override protected String getReaderName() { - return "riskDataRangeReader"; + return "RiskDataRangeReader"; } @Override @@ -62,10 +47,8 @@ public class RiskDataRangeReader extends BaseApiReader { protected String getApiPath() { return "/RiskAndCompliance/UpdatedRiskList"; } - - @Override - protected void beforeFetch(){ - log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), fromDate, toDate); + protected String getApiKey() { + return "RISK_IMPORT_API"; } @Override @@ -73,7 +56,7 @@ public class RiskDataRangeReader extends BaseApiReader { // 모든 배치 처리 완료 확인 if (allData == null) { log.info("[{}] 최초 API 조회 실행: {} ~ {}", getReaderName(), fromDate, toDate); - allData = callApiWithBatch(fromDate, toDate); + allData = callApiWithBatch(); if (allData == null || allData.isEmpty()) { log.warn("[{}] 조회된 데이터 없음 → 종료", getReaderName()); @@ -106,12 +89,29 @@ public class RiskDataRangeReader extends BaseApiReader { return batch; } + @Override + protected void afterFetch(List data) { + try{ + if (data == null) { + log.info("[{}] 배치 처리 성공", getReaderName()); + } + }catch (Exception e){ + log.info("[{}] 배치 처리 실패", getReaderName()); + log.info("[{}] API 호출 종료", getReaderName()); + } + } - private List callApiWithBatch(String fromDate, String stopDate) { - String url = getApiPath() + "?fromDate=" + fromDate +"&stopDate=" + stopDate; - log.debug("[{}] API 호출: {}", getReaderName(), url); + private List callApiWithBatch() { + Map params = batchDateService.getRiskComplianceApiDateParams(getApiKey()); + log.info("[{}] 요청 날짜 범위: {} → {}", getReaderName(), params.get("fromDate"), params.get("toDate")); + + String url = getApiPath(); return webClient.get() - .uri(url) + .uri(url, uriBuilder -> uriBuilder + // 맵에서 파라미터 값을 동적으로 가져와 세팅 + .queryParam("fromDate", params.get("fromDate")) + .queryParam("toDate", params.get("toDate")) + .build()) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) .block(); diff --git a/src/main/java/com/snp/batch/jobs/risk/batch/repository/RiskRepositoryImpl.java b/src/main/java/com/snp/batch/jobs/risk/batch/repository/RiskRepositoryImpl.java index a6a07ff..b5f5232 100644 --- a/src/main/java/com/snp/batch/jobs/risk/batch/repository/RiskRepositoryImpl.java +++ b/src/main/java/com/snp/batch/jobs/risk/batch/repository/RiskRepositoryImpl.java @@ -41,7 +41,7 @@ public class RiskRepositoryImpl extends BaseJdbcRepository imp @Override protected String getUpdateSql() { return """ - INSERT INTO new_snp.risk ( + INSERT INTO new_snp.risk_history ( lrno, lastupdated, riskdatamaintained, dayssincelastseenonais, dayssincelastseenonaisnarrative, daysunderais, daysunderaisnarrative, imocorrectonais, imocorrectonaisnarrative, sailingundername, sailingundernamenarrative, anomalousmessagesfrommmsi, anomalousmessagesfrommmsinarrative, diff --git a/src/main/java/com/snp/batch/jobs/risk/batch/writer/RiskDataWriter.java b/src/main/java/com/snp/batch/jobs/risk/batch/writer/RiskDataWriter.java index 9bb7a6a..eb642fa 100644 --- a/src/main/java/com/snp/batch/jobs/risk/batch/writer/RiskDataWriter.java +++ b/src/main/java/com/snp/batch/jobs/risk/batch/writer/RiskDataWriter.java @@ -3,9 +3,11 @@ package com.snp.batch.jobs.risk.batch.writer; import com.snp.batch.common.batch.writer.BaseWriter; import com.snp.batch.jobs.risk.batch.entity.RiskEntity; import com.snp.batch.jobs.risk.batch.repository.RiskRepository; +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 @@ -13,14 +15,19 @@ import java.util.List; public class RiskDataWriter extends BaseWriter { 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"); this.riskRepository = riskRepository; + this.batchDateService = batchDateService; } @Override protected void writeItems(List items) throws Exception { 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()); } } diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/config/OrderDataImportJobConfig.java b/src/main/java/com/snp/batch/jobs/sample/batch/config/OrderDataImportJobConfig.java deleted file mode 100644 index 890c527..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/config/OrderDataImportJobConfig.java +++ /dev/null @@ -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 { - - 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 createReader() { - // 실제 구현 시 OrderDataReader 생성 - // 예제이므로 null 반환 (Job 등록 안 함) - return null; - } - - @Override - protected ItemProcessor createProcessor() { - return orderDataProcessor; - } - - /** - * CompositeWriter 생성 - * OrderWriter와 OrderItemWriter를 조합 - */ - @Override - protected ItemWriter createWriter() { - CompositeItemWriter 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 사용 - */ diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/config/ProductDataImportJobConfig.java b/src/main/java/com/snp/batch/jobs/sample/batch/config/ProductDataImportJobConfig.java deleted file mode 100644 index d108fc0..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/config/ProductDataImportJobConfig.java +++ /dev/null @@ -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 { - - 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 createReader() { - return productDataReader; - } - - @Override - protected ItemProcessor createProcessor() { - return productDataProcessor; - } - - @Override - protected ItemWriter 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(); - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/dto/OrderDto.java b/src/main/java/com/snp/batch/jobs/sample/batch/dto/OrderDto.java deleted file mode 100644 index b49915f..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/dto/OrderDto.java +++ /dev/null @@ -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 items; - - /** - * 주문 상품 DTO (내부 클래스) - */ - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class OrderItemDto { - - /** - * 상품 ID - */ - private String productId; - - /** - * 상품명 - */ - private String productName; - - /** - * 수량 - */ - private Integer quantity; - - /** - * 가격 - */ - private BigDecimal price; - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/dto/ProductApiResponse.java b/src/main/java/com/snp/batch/jobs/sample/batch/dto/ProductApiResponse.java deleted file mode 100644 index 4c1d24a..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/dto/ProductApiResponse.java +++ /dev/null @@ -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 products; - - /** - * 메시지 - */ - @JsonProperty("message") - private String message; -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/dto/ProductDto.java b/src/main/java/com/snp/batch/jobs/sample/batch/dto/ProductDto.java deleted file mode 100644 index d944bd1..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/dto/ProductDto.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/entity/OrderEntity.java b/src/main/java/com/snp/batch/jobs/sample/batch/entity/OrderEntity.java deleted file mode 100644 index a566db9..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/entity/OrderEntity.java +++ /dev/null @@ -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에서 상속 -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/entity/OrderItemEntity.java b/src/main/java/com/snp/batch/jobs/sample/batch/entity/OrderItemEntity.java deleted file mode 100644 index 18dd382..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/entity/OrderItemEntity.java +++ /dev/null @@ -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에서 상속 -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/entity/ProductEntity.java b/src/main/java/com/snp/batch/jobs/sample/batch/entity/ProductEntity.java deleted file mode 100644 index e101992..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/entity/ProductEntity.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/processor/OrderDataProcessor.java b/src/main/java/com/snp/batch/jobs/sample/batch/processor/OrderDataProcessor.java deleted file mode 100644 index be24730..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/processor/OrderDataProcessor.java +++ /dev/null @@ -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 (N개) - * } - * ↓ - * CompositeWriter { - * OrderWriter → orders 테이블 - * OrderItemWriter → order_items 테이블 - * } - */ -@Slf4j -@Component -public class OrderDataProcessor extends BaseProcessor { - - /** - * 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 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 items; - - public OrderWrapper(OrderEntity order, List items) { - this.order = order; - this.items = items; - } - - public OrderEntity getOrder() { - return order; - } - - public List getItems() { - return items; - } - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/processor/ProductDataProcessor.java b/src/main/java/com/snp/batch/jobs/sample/batch/processor/ProductDataProcessor.java deleted file mode 100644 index 7e05d53..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/processor/ProductDataProcessor.java +++ /dev/null @@ -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 { - - @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(); - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/reader/ProductApiReader.java b/src/main/java/com/snp/batch/jobs/sample/batch/reader/ProductApiReader.java deleted file mode 100644 index 10536fe..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/reader/ProductApiReader.java +++ /dev/null @@ -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 { - - /** - * WebClient 주입 생성자 - * - * @param webClient Spring WebClient 인스턴스 - */ - public ProductApiReader(WebClient webClient) { - super(webClient); - } - - // ======================================== - // 필수 구현 메서드 - // ======================================== - - @Override - protected String getReaderName() { - return "ProductApiReader"; - } - - @Override - protected List 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 getQueryParams() { - Map 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 getHeaders() { - Map 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 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 data) { - log.info("[{}] API 호출 성공: {}건 조회", getReaderName(), getDataSize(data)); - - // 데이터 검증 - if (isEmpty(data)) { - log.warn("[{}] 조회된 데이터가 없습니다!", getReaderName()); - } - } - - @Override - protected List 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 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 getPathVariables() { - * Map 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 extractDataFromResponse(Object response) { - * ComplexApiResponse apiResponse = (ComplexApiResponse) response; - * return apiResponse.getResult().getData().getItems(); - * } - */ diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/reader/ProductDataReader.java b/src/main/java/com/snp/batch/jobs/sample/batch/reader/ProductDataReader.java deleted file mode 100644 index 47dff0c..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/reader/ProductDataReader.java +++ /dev/null @@ -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 { - - /** - * 기본 생성자 (WebClient 없이 Mock 데이터 생성) - */ - public ProductDataReader() { - super(); // WebClient 없이 초기화 - } - - // ======================================== - // 필수 구현 메서드 - // ======================================== - - @Override - protected String getReaderName() { - return "ProductDataReader"; - } - - @Override - protected List fetchDataFromApi() { - log.info("========================================"); - log.info("Mock 샘플 데이터 생성 시작"); - log.info("========================================"); - - return generateMockData(); - } - - // ======================================== - // 라이프사이클 훅 (선택적 오버라이드) - // ======================================== - - @Override - protected void beforeFetch() { - log.info("[{}] Mock 데이터 생성 준비...", getReaderName()); - } - - @Override - protected void afterFetch(List data) { - log.info("[{}] Mock 데이터 생성 완료: {}건", getReaderName(), getDataSize(data)); - } - - /** - * Mock 샘플 데이터 생성 - * 다양한 데이터 타입 포함 - */ - private List generateMockData() { - log.info("========================================"); - log.info("Mock 샘플 데이터 생성 시작"); - log.info("다양한 데이터 타입 테스트용"); - log.info("========================================"); - - List 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; - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/repository/ProductRepository.java b/src/main/java/com/snp/batch/jobs/sample/batch/repository/ProductRepository.java deleted file mode 100644 index 00c27fa..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/repository/ProductRepository.java +++ /dev/null @@ -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 findById(Long id); - List findAll(); - long count(); - boolean existsById(Long id); - ProductEntity save(ProductEntity entity); - void saveAll(List entities); - void deleteById(Long id); - void deleteAll(); - - // 커스텀 메서드 - /** - * 제품 ID로 조회 - */ - Optional findByProductId(String productId); - - /** - * 제품 ID 존재 여부 확인 - */ - boolean existsByProductId(String productId); - - /** - * 페이징 조회 - */ - List findAllWithPaging(int offset, int limit); -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/repository/ProductRepositoryImpl.java b/src/main/java/com/snp/batch/jobs/sample/batch/repository/ProductRepositoryImpl.java deleted file mode 100644 index 4916592..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/repository/ProductRepositoryImpl.java +++ /dev/null @@ -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 implements ProductRepository { - - public ProductRepositoryImpl(JdbcTemplate jdbcTemplate) { - super(jdbcTemplate); - } - - @Override - protected String getTableName() { - return "sample_products"; - } - - @Override - protected String getEntityName() { - return "Product"; - } - - @Override - protected RowMapper 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 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 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 { - @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; - } - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/writer/OrderItemWriter.java b/src/main/java/com/snp/batch/jobs/sample/batch/writer/OrderItemWriter.java deleted file mode 100644 index 481d666..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/writer/OrderItemWriter.java +++ /dev/null @@ -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 { - - public OrderItemWriter() { - super("OrderItem"); - } - - @Override - protected void writeItems(List wrappers) throws Exception { - // OrderWrapper에서 OrderItemEntity 리스트만 추출 (flatten) - List 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()); - } - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/writer/OrderWriter.java b/src/main/java/com/snp/batch/jobs/sample/batch/writer/OrderWriter.java deleted file mode 100644 index d91ceee..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/writer/OrderWriter.java +++ /dev/null @@ -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 { - - public OrderWriter() { - super("Order"); - } - - @Override - protected void writeItems(List wrappers) throws Exception { - // OrderWrapper에서 OrderEntity만 추출 - List 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()); - } - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/batch/writer/ProductDataWriter.java b/src/main/java/com/snp/batch/jobs/sample/batch/writer/ProductDataWriter.java deleted file mode 100644 index 94f7865..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/batch/writer/ProductDataWriter.java +++ /dev/null @@ -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 { - - private final ProductRepository productRepository; - - public ProductDataWriter(ProductRepository productRepository) { - super("Product"); - this.productRepository = productRepository; - } - - @Override - protected void writeItems(List 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("========================================"); - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/web/controller/ProductWebController.java b/src/main/java/com/snp/batch/jobs/sample/web/controller/ProductWebController.java deleted file mode 100644 index 01700cd..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/web/controller/ProductWebController.java +++ /dev/null @@ -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 { - - private final ProductWebService productWebService; - - @Override - protected BaseService 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> 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> 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()) - ); - } - } -} diff --git a/src/main/java/com/snp/batch/jobs/sample/web/dto/ProductResponseDto.java b/src/main/java/com/snp/batch/jobs/sample/web/dto/ProductResponseDto.java deleted file mode 100644 index ac79dc5..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/web/dto/ProductResponseDto.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/snp/batch/jobs/sample/web/dto/ProductWebDto.java b/src/main/java/com/snp/batch/jobs/sample/web/dto/ProductWebDto.java deleted file mode 100644 index e12a5dc..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/web/dto/ProductWebDto.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/com/snp/batch/jobs/sample/web/service/ProductWebService.java b/src/main/java/com/snp/batch/jobs/sample/web/service/ProductWebService.java deleted file mode 100644 index 6c876d6..0000000 --- a/src/main/java/com/snp/batch/jobs/sample/web/service/ProductWebService.java +++ /dev/null @@ -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 { - - private final ProductRepositoryImpl productRepository; - - @Override - protected BaseJdbcRepository 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 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; - } -} diff --git a/src/main/java/com/snp/batch/jobs/sanction/batch/writer/ComplianceDataWriter.java b/src/main/java/com/snp/batch/jobs/sanction/batch/writer/ComplianceDataWriter.java deleted file mode 100644 index 89b4c3e..0000000 --- a/src/main/java/com/snp/batch/jobs/sanction/batch/writer/ComplianceDataWriter.java +++ /dev/null @@ -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 { - - private final ComplianceRepository complianceRepository; - public ComplianceDataWriter(ComplianceRepository complianceRepository) { - super("complianceRepository"); - this.complianceRepository = complianceRepository; - } - - @Override - protected void writeItems(List items) throws Exception { - complianceRepository.saveComplianceAll(items); - } -} diff --git a/src/main/java/com/snp/batch/service/BatchDateService.java b/src/main/java/com/snp/batch/service/BatchDateService.java index ef00916..5f06726 100644 --- a/src/main/java/com/snp/batch/service/BatchDateService.java +++ b/src/main/java/com/snp/batch/service/BatchDateService.java @@ -5,6 +5,9 @@ import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; @Service @@ -46,6 +49,29 @@ public class BatchDateService { return params; } + public Map 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 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에 저장 및 업데이트합니다. * @param successDate API 호출 성공 시 사용된 to 날짜