-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/38: 가맹점 일일 정산(매출 전표 csv 파일 생성 및 SFTP 전송 Batch Job) 구현 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
dc58900
Chore: Spring Batch 의존성 라이브러리 추가(latest 버전)
yxhwxn 83d0862
Feat: Meta_DB(배치 메타 데이터 DB), Data_DB 복수 DB Config 클래스 작성
yxhwxn 1af26b5
chore: sftp 프로토콜 지원용 의존성 라이브러리 추가
yxhwxn eafff73
Feat: SFTP 파일 전송용 유틸 클래스 작성
yxhwxn ecd0c03
Feat: CSV 가맹점 매입 전표 생성용 유틸 클래스 추가
yxhwxn 0f1b150
Feat: 가맹점 일일 정산(매입 전표 생성 및 csv 파일 전송) Batch Job 스텁 추가
yxhwxn 953a62b
Fix: 일일 정산 시 approvedAt Datetime을 기반으로 값을 조회할 수 있도록 수정
yxhwxn e13dc53
Fix: HostKeyVerifier 인터페이스 필수 메서드("findExistingAlgorithms()") 추가
yxhwxn c1b9692
Fix: LocalSourceFile 메서드 시그니처 추가
yxhwxn 5804944
Fix: sftp 송수신 간 파일명 중복 방지 로직 추가
yxhwxn c331562
Chore: Spring Batch 프로세서가 결제 내역 조회 시 사용하는 chunk size 수정(10 -> 5000)
yxhwxn 4ae58ce
Chore: Batch Job Retry 전략을 위한 라이브러리 추가
yxhwxn c387284
Feat: 실패 시 SFTP 전송을 자동 재시도(retry) 하도록 RetryTemplate 적용
yxhwxn 18e8d82
Feat: Spring Batch chunk 기반 재시도 설정 추가
yxhwxn 49c4190
Refactor: 매입 전표 csv 형식 변경에 따른 record 불변 객체 추가
yxhwxn 52ccc41
Fix: DTO 타입 수정으로 인한 CSV 생성 코드 수정
yxhwxn f54eadb
Chore: Quartz 스케줄러 의존성 라이브러리 추가
yxhwxn 073f294
Feat: Quartz 기반 Batch Job 트리거 설정 및 Batch 빈 주입
yxhwxn b676125
Feat: 활성 가맹점 조회 메서드 추가
yxhwxn 0a523a1
Fix: Spring Batch 5.x 부터는 tasklet(Tasklet, PlatformTransactionManager…
yxhwxn d587b0b
Refactor: Chunk 모델 -> Tasklet 단일 모델 변경으로 인한 재시도 전략 수정
yxhwxn 17bd430
Refactor: 불필요한 LocalSourceFile 메서드 제거, FileSystemFile을 사용하여 로컬 파일을 래핑…
yxhwxn 157e44c
Chore: csv 파일명 수정
yxhwxn 02e04a2
Fix: 가맹점별 일일 정산 요약 시 신용카드, 체크카드 결제액 구분(각 수수료율을 다르게 정산해줘야 하기 때문)
yxhwxn 0d99f3f
Fix: 카드 타입에 따른 차별적인 우대 수수료 적용을 위한 csv 생성 코드 수정
yxhwxn d98f81a
Chore: 응답 객체 필드 수정
yxhwxn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.fisa.pg.batch.dto; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * 가맹점별 일일 정산 요약 정보를 담는 불변 객체 | ||
| */ | ||
| public record MerchantSummary( | ||
| Long merchantId, | ||
| String merchantName, | ||
| BigDecimal creditAmount, | ||
| BigDecimal debitAmount, | ||
| BigDecimal totalAmount, | ||
| String currency, | ||
| LocalDateTime settlementDate | ||
| ) { | ||
| } |
128 changes: 128 additions & 0 deletions
128
src/main/java/com/fisa/pg/batch/job_config/GenerateAndSendPurchaseCsvJob.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| package com.fisa.pg.batch.job_config; | ||
|
|
||
| import com.fisa.pg.batch.dto.MerchantSummary; | ||
| import com.fisa.pg.batch.utils.CsvGenerator; | ||
| import com.fisa.pg.batch.utils.SftpUploader; | ||
| import com.fisa.pg.entity.card.CardType; | ||
| import com.fisa.pg.entity.payment.Payment; | ||
| import com.fisa.pg.entity.payment.PaymentStatus; | ||
| import com.fisa.pg.entity.user.Merchant; | ||
| import com.fisa.pg.repository.MerchantRepository; | ||
| import com.fisa.pg.repository.PaymentRepository; | ||
| import org.springframework.batch.core.Job; | ||
| import org.springframework.batch.core.Step; | ||
| import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; | ||
| import org.springframework.batch.core.job.builder.JobBuilder; | ||
| import org.springframework.batch.core.launch.support.RunIdIncrementer; | ||
| import org.springframework.batch.core.repository.JobRepository; | ||
| import org.springframework.batch.core.step.builder.StepBuilder; | ||
| import org.springframework.batch.repeat.RepeatStatus; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.transaction.PlatformTransactionManager; | ||
|
|
||
| import java.io.File; | ||
| import java.math.BigDecimal; | ||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Configuration | ||
| @EnableBatchProcessing | ||
| public class GenerateAndSendPurchaseCsvJob { | ||
|
|
||
| private final PaymentRepository paymentRepository; | ||
| private final MerchantRepository merchantRepository; | ||
| private final CsvGenerator csvGenerator; | ||
| private final SftpUploader sftpUploader; | ||
|
|
||
| public GenerateAndSendPurchaseCsvJob( | ||
| PaymentRepository paymentRepository, | ||
| MerchantRepository merchantRepository, | ||
| CsvGenerator csvGenerator, | ||
| SftpUploader sftpUploader | ||
| ) { | ||
| this.paymentRepository = paymentRepository; | ||
| this.merchantRepository = merchantRepository; | ||
| this.csvGenerator = csvGenerator; | ||
| this.sftpUploader = sftpUploader; | ||
| } | ||
|
|
||
| @Bean | ||
| public Job generateAndSendPurchaseCsvJob(JobRepository jobRepository, | ||
| Step settlementStep) { | ||
| return new JobBuilder("GenerateAndSendPurchaseCsvJob", jobRepository) | ||
| .incrementer(new RunIdIncrementer()) | ||
| .start(settlementStep) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public Step settlementStep(JobRepository jobRepository, | ||
| PlatformTransactionManager transactionManager, | ||
| org.springframework.retry.support.RetryTemplate retryTemplate) { | ||
|
|
||
| return new StepBuilder("SettlementSummaryStep", jobRepository) | ||
| .tasklet((contrib, ctx) -> | ||
| retryTemplate.execute(retryCtx -> { | ||
| // 1) 어제 00:00:00 ~ 23:59:59 | ||
| LocalDate yesterday = LocalDate.now().minusDays(1); | ||
| LocalDateTime start = yesterday.atStartOfDay(); | ||
| LocalDateTime end = yesterday.atTime(23, 59, 59); | ||
|
|
||
| // 2) 정산 시각: 오늘 02:00 | ||
| LocalDateTime settlementTime = LocalDateTime.now() | ||
| .withHour(2).withMinute(0).withSecond(0).withNano(0); | ||
|
|
||
| // 3) 활성 가맹점 조회 | ||
| List<Merchant> merchants = merchantRepository.findByIsActiveTrue(); | ||
|
|
||
| // 4) 일일 매출 합계 계산 및 요약 DTO 생성 | ||
| List<MerchantSummary> summaries = merchantRepository.findByIsActiveTrue().stream() | ||
| .map(m -> { | ||
| List<Payment> payments = paymentRepository | ||
| .findByMerchantAndPaymentStatusAndApprovedAtBetween( | ||
| m, PaymentStatus.SUCCEEDED, start, end); | ||
| if (payments.isEmpty()) return null; | ||
|
|
||
| // 신용카드 합계 | ||
| BigDecimal creditSum = payments.stream() | ||
| .filter(p -> p.getUserCard() != null | ||
| && p.getUserCard().getCardType() == CardType.CREDIT) | ||
| .map(p -> BigDecimal.valueOf(p.getAmount())) | ||
| .reduce(BigDecimal.ZERO, BigDecimal::add); | ||
|
|
||
| // 체크카드 합계 | ||
| BigDecimal debitSum = payments.stream() | ||
| .filter(p -> p.getUserCard() != null | ||
| && p.getUserCard().getCardType() == CardType.DEBIT) | ||
| .map(p -> BigDecimal.valueOf(p.getAmount())) | ||
| .reduce(BigDecimal.ZERO, BigDecimal::add); | ||
|
|
||
| // 전체 합계 | ||
| BigDecimal totalSum = creditSum.add(debitSum); | ||
|
|
||
| return new MerchantSummary( | ||
| m.getId(), | ||
| m.getName(), | ||
| creditSum, | ||
| debitSum, | ||
| totalSum, | ||
| payments.get(0).getCurrency(), | ||
| settlementTime | ||
| ); | ||
| }) | ||
| .filter(Objects::nonNull) | ||
| .collect(Collectors.toList()); | ||
| // 5) CSV 생성 & SFTP 전송 | ||
| File csv = csvGenerator.generateSummary(summaries); | ||
| sftpUploader.upload(csv); | ||
|
|
||
| return RepeatStatus.FINISHED; | ||
| }) | ||
| , transactionManager) | ||
| .build(); | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
src/main/java/com/fisa/pg/batch/job_config/RetryTemplateConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.fisa.pg.batch.job_config; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.retry.backoff.FixedBackOffPolicy; | ||
| import org.springframework.retry.policy.SimpleRetryPolicy; | ||
|
|
||
| @Configuration | ||
| public class RetryTemplateConfig { | ||
|
|
||
| // Tasklet 모델 재시도 전략(Chunk 모델과 달리 프레임워크에서 재시도 기능을 제공해주지 않음) | ||
| @Bean | ||
| public org.springframework.retry.support.RetryTemplate retryTemplate() { | ||
| org.springframework.retry.support.RetryTemplate template = new org.springframework.retry.support.RetryTemplate(); | ||
|
|
||
| SimpleRetryPolicy policy = new SimpleRetryPolicy(); | ||
| policy.setMaxAttempts(3); // 총 3회 시도 | ||
|
|
||
| FixedBackOffPolicy backOff = new FixedBackOffPolicy(); | ||
| backOff.setBackOffPeriod(2000); // 시도 간격 2초 | ||
|
|
||
| template.setRetryPolicy(policy); | ||
| template.setBackOffPolicy(backOff); | ||
| return template; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.fisa.pg.batch.quartz; | ||
|
|
||
| import org.quartz.*; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| public class QuartzConfig { | ||
|
|
||
| @Bean | ||
| public JobDetail settlementJobDetail() { | ||
| return JobBuilder.newJob(QuartzJobLauncher.class) | ||
| .withIdentity("dailySettlementJob") | ||
| .usingJobData("jobName", "GenerateAndSendPurchaseCsvJob") | ||
| .storeDurably() | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public Trigger settlementJobTrigger(JobDetail jobDetail) { | ||
| return TriggerBuilder.newTrigger() | ||
| .forJob(jobDetail) | ||
| .withIdentity("dailySettlementTrigger") | ||
| .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) // 매일 02:00 | ||
| .build(); | ||
| } | ||
| } |
36 changes: 36 additions & 0 deletions
36
src/main/java/com/fisa/pg/batch/quartz/QuartzJobLauncher.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.fisa.pg.batch.quartz; | ||
|
|
||
| import org.quartz.JobExecutionContext; | ||
| import org.quartz.JobExecutionException; | ||
| import org.springframework.batch.core.Job; | ||
| import org.springframework.batch.core.JobParametersBuilder; | ||
| import org.springframework.batch.core.launch.JobLauncher; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.beans.factory.annotation.Qualifier; | ||
| import org.springframework.scheduling.quartz.QuartzJobBean; | ||
|
|
||
| public class QuartzJobLauncher extends QuartzJobBean { | ||
|
|
||
| @Autowired | ||
| private JobLauncher jobLauncher; | ||
|
|
||
| // Bean 이름(메서드명)으로 등록된 Spring Batch Job을 주입 | ||
| @Autowired | ||
| @Qualifier("generateAndSendPurchaseCsvJob") | ||
| private Job generateAndSendPurchaseCsvJob; | ||
|
|
||
| @Override | ||
| protected void executeInternal(JobExecutionContext context) throws JobExecutionException { | ||
| try { | ||
| jobLauncher.run( | ||
| generateAndSendPurchaseCsvJob, | ||
| new JobParametersBuilder() | ||
| .addLong("timestamp", System.currentTimeMillis()) | ||
| .toJobParameters() | ||
| ); | ||
| } catch (Exception ex) { | ||
| // Quartz의 JobExecutionException을 던져야 Quartz가 재시도/오류 로깅을 합니다. | ||
| throw new JobExecutionException(ex); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.fisa.pg.batch.utils; | ||
|
|
||
| import com.fisa.pg.batch.dto.MerchantSummary; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.io.BufferedWriter; | ||
| import java.io.File; | ||
| import java.io.FileWriter; | ||
| import java.io.IOException; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| public class CsvGenerator { | ||
|
|
||
| @Value("${batch.output.directory}") | ||
| private String outputDirectory; | ||
|
|
||
| /** | ||
| * 전체 요약 리스트로 단일 CSV 파일 생성 | ||
| */ | ||
| public File generateSummary(List<MerchantSummary> summaries) throws IOException { | ||
| String timestamp = LocalDateTime.now() | ||
| .format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmm")); | ||
| String fileName = String.format("pg_daily_settlement_summary_to_card_%s.csv", timestamp); | ||
|
|
||
| File dir = new File(outputDirectory); | ||
| if (!dir.exists()) dir.mkdirs(); | ||
| File csvFile = new File(dir, fileName); | ||
|
|
||
| try (BufferedWriter writer = new BufferedWriter(new FileWriter(csvFile))) { | ||
| // 헤더 | ||
| writer.write("merchant_id,merchant_name,credit_card_amount,check_card_amount,total_amount,currency,settlement_date"); | ||
| writer.newLine(); | ||
|
|
||
| // 데이터 | ||
| DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||
| for (MerchantSummary ms : summaries) { | ||
| writer.write(String.join(",", | ||
| ms.merchantId(), | ||
| ms.merchantName(), | ||
| ms.creditAmount().toPlainString(), | ||
| ms.debitAmount().toPlainString(), | ||
| ms.totalAmount().toPlainString(), | ||
| ms.currency(), | ||
| ms.settlementDate().format(dtf) | ||
| )); | ||
| writer.newLine(); | ||
| } | ||
| } | ||
| return csvFile; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package com.fisa.pg.batch.utils; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import net.schmizz.sshj.SSHClient; | ||
| import net.schmizz.sshj.sftp.SFTPClient; | ||
| import net.schmizz.sshj.transport.verification.PromiscuousVerifier; | ||
| import net.schmizz.sshj.xfer.FileSystemFile; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.retry.support.RetryTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.io.File; | ||
| import java.io.IOException; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| public class SftpUploader { | ||
|
|
||
| @Value("${batch.sftp.host}") | ||
| private String sftpHost; | ||
|
|
||
| @Value("${batch.sftp.port:22}") | ||
| private int sftpPort; | ||
|
|
||
| @Value("${batch.sftp.username}") | ||
| private String sftpUsername; | ||
|
|
||
| @Value("${batch.sftp.password}") | ||
| private String sftpPassword; | ||
|
|
||
| @Value("${batch.sftp.remote-dir}") | ||
| private String sftpRemoteDir; | ||
|
|
||
| private final RetryTemplate sftpRetryTemplate; | ||
|
|
||
| public SftpUploader(RetryTemplate sftpRetryTemplate) { | ||
| this.sftpRetryTemplate = sftpRetryTemplate; | ||
| } | ||
|
|
||
| public void upload(File file) { | ||
| if (file == null || !file.exists()) { | ||
| throw new IllegalArgumentException(">>>>>>>>>>>>>[Error]: 전송할 파일이 존재하지 않습니다: " + file); | ||
| } | ||
|
|
||
| try { | ||
| sftpRetryTemplate.execute(context -> { | ||
| // 기존 로직 | ||
| try (SSHClient ssh = new SSHClient()) { | ||
| ssh.addHostKeyVerifier(new PromiscuousVerifier()); | ||
| ssh.connect(sftpHost, sftpPort); | ||
| ssh.authPassword(sftpUsername, sftpPassword); | ||
|
|
||
| try (SFTPClient sftp = ssh.newSFTPClient()) { | ||
| sftp.put(new FileSystemFile(file), sftpRemoteDir + "/"); | ||
| log.info(">>>>>>>>>>>>>>SFTP 전송 성공: {}", file.getName()); | ||
| } | ||
| } catch (IOException e) { | ||
| log.warn(">>>>>>>>>>>>>>SFTP 전송 재시도 중 오류 발생: {}", e.getMessage()); | ||
| throw e; | ||
| } | ||
| return null; | ||
| }); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(">>>>>>>>>>>>>>SFTP 최종 실패: " + file.getName(), e); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Consider separating the null check from the exists() check to provide a clearer error message when file is null.