✨ Risk&Compliance Range Import Update
This commit is contained in:
부모
5683000024
커밋
fcf1d74c38
@ -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 날짜
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user