Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ dependencies {
implementation 'software.amazon.awssdk:s3'

// Smart Contract
implementation 'org.web3j:core:4.9.4'
implementation 'org.web3j:core:4.12.0'
implementation 'org.web3j:contracts:4.12.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.donet.donet.donation.adapter.in.scheduler;

import com.donet.donet.donation.application.port.in.RefundDonationUsecase;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RefundScheduler {
private final RefundDonationUsecase refundDonationUsecase;

@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
public void runRefundJob() {
refundDonationUsecase.refundDonation();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.donet.donet.global.exception.CustomException;
import com.donet.donet.global.exception.DonationException;
import com.donet.donet.global.exception.UserException;
import com.donet.donet.global.persistence.BaseStatus;
import com.donet.donet.review.adapter.out.persistence.DonationReviewRepository;
import com.donet.donet.user.adapter.out.persistence.UserJpaEntity;
import com.donet.donet.user.adapter.out.persistence.UserRepository;
Expand All @@ -26,6 +27,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -125,6 +127,14 @@ public List<RegisteredDonation> findRegisteredDonations(User user, int size) {
return registeredDonations;
}

@Override
public List<Donation> findRefundableDonation() {
return donationRepository.findRefundableDonations(LocalDate.now(), BaseStatus.ACTIVE)
.stream()
.map(donationMapper::mapToDomainEntity)
.toList();
}

@Override
public Donation increaseDonationView(Long donationId) {
DonationJpaEntity donationJpaEntity = donationRepository.findDonationById(donationId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.donet.donet.donation.adapter.out.persistence.donation;

import com.donet.donet.global.persistence.BaseStatus;
import com.donet.donet.user.adapter.out.persistence.UserJpaEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -8,6 +9,7 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -90,4 +92,12 @@ SELECT COUNT(*)
Page<JoinedDonationProjection> findJoinedDonations(@Param("userId") Long userId, Pageable pageable);

List<DonationJpaEntity> findAllByUserJpaEntityOrderByIdDesc(UserJpaEntity userJpaEntity, Pageable pageable);

@Query("""
SELECT d FROM DonationJpaEntity d
WHERE d.endDate < :today
AND d.currentAmount < d.targetAmount
AND d.status = :status
""")
List<DonationJpaEntity> findRefundableDonations(LocalDate today, BaseStatus status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.donet.donet.donation.application;

import com.donet.donet.donation.application.port.in.RefundDonationUsecase;
import com.donet.donet.donation.application.port.out.FindDonationPort;
import com.donet.donet.donation.application.port.out.SmartContractPort;
import com.donet.donet.donation.domain.Donation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class RefundDonationService implements RefundDonationUsecase {

private final FindDonationPort findDonationPort;
private final SmartContractPort smartContractPort;

@Override
public void refundDonation() {
List<Long> refundTargetDonationIds = findDonationPort.findRefundableDonation()
.stream()
.map(Donation::getId)
.toList();
Comment on lines +23 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

예외 처리 전략을 검증하세요.

findDonationPort.findRefundableDonation() 호출 시 발생할 수 있는 예외(DB 연결 실패, 타임아웃 등)가 처리되지 않고 있습니다. 스케줄러가 예외로 인해 중단될 수 있으며, 이후 실행에도 영향을 줄 수 있습니다.

다음을 확인하세요:

  • 스케줄러 레벨에서 예외를 처리하는지 검증
  • 또는 이 메소드에 try-catch 추가 고려

다음 스크립트로 스케줄러의 예외 처리 구현을 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: 스케줄러에서 예외 처리가 구현되어 있는지 확인

# RefundScheduler 파일 찾기 및 예외 처리 로직 검색
fd -e java RefundScheduler --exec cat {} \; | grep -A 10 -B 5 "refundDonation"

Length of output: 327


스케줄러 레벨의 예외 처리를 더 자세히 검증하기 위해 RefundDonationService 구현을 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# RefundDonationService의 refundDonation() 메소드 구현 확인
fd -e java RefundDonationService --exec cat {} \;

Length of output: 1390


🏁 Script executed:

#!/bin/bash
# RefundDonationUsecase 인터페이스 확인
fd -e java RefundDonationUsecase --exec cat {} \;

Length of output: 124


🏁 Script executed:

#!/bin/bash
# 전체 프로젝트에서 refundDonation 메소드 구현 찾기
rg -n "refundDonation\s*\(" --type java -A 15

Length of output: 2399


🏁 Script executed:

#!/bin/bash
# Spring @Scheduled에 대한 에러 핸들링 설정 확인
rg -n "TaskScheduler\|SchedulingConfigurer\|@EnableScheduling" --type java -B 2 -A 5

Length of output: 0


추가로 프로젝트 레벨의 글로벌 에러 핸들링 설정을 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# 글로벌 예외 처리 설정 확인 (AOP, Aspect, ErrorHandler 등)
rg -n "@Aspect\|@ControllerAdvice\|ErrorHandler\|ExceptionHandler" --type java -B 2 -A 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 프로젝트 루트의 application 설정 파일 확인
fd -e yml -e yaml -e properties | head -20 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Length of output: 3726


🏁 Script executed:

#!/bin/bash
# FindDonationPort 포트의 구현체 확인 - findRefundableDonation 메소드
rg -n "findRefundableDonation" --type java -B 5 -A 10

Length of output: 6038


findDonationPort.findRefundableDonation() 호출에 예외 처리 추가 필요합니다.

검증 결과, RefundDonationService의 23-26번 줄에서 데이터베이스 조회 메소드를 호출할 때 예외 처리가 없습니다. DB 연결 실패, 타임아웃 등의 예외가 발생하면 스케줄러로 전파되어 실행 흐름이 중단됩니다. Spring @scheduled는 예외를 로깅하고 다음 스케줄에 계속 실행되지만, 애플리케이션 레벨의 명시적인 예외 처리 전략이 부재합니다.

다음과 같이 예외 처리를 추가하세요:

  • try-catch 블록으로 findRefundableDonation() 호출 감싸기
  • 또는 전체 refundDonation() 메소드에 예외 처리 추가
  • 예외 발생 시 적절한 로깅 및 알림 메커니즘 구현
🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/donation/application/RefundDonationService.java
around lines 23-26, the call to findDonationPort.findRefundableDonation() lacks
exception handling and can propagate DB/connectivity/timeouts to the scheduler;
wrap the call (or the whole refundDonation() method) in a try-catch that catches
relevant exceptions (e.g., DataAccessException, RuntimeException), log the error
with context (including exception message and stacktrace) and return/exit
gracefully so the exception does not propagate to the scheduler, and optionally
trigger your existing alerting/notification mechanism when a failure occurs.


if (refundTargetDonationIds.isEmpty()) {
log.info("환불 대상 기부가 없습니다.");
return;
}

log.info("환불 처리 시작: {} 건", refundTargetDonationIds.size());
boolean success = smartContractPort.refundDonations(refundTargetDonationIds);

if (success) {
log.info("환불 처리 완료: {} 건", refundTargetDonationIds.size());
}else{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

코드 포맷팅을 수정하세요.

else 키워드 앞뒤에 공백이 누락되었습니다.

다음과 같이 수정하세요:

-        }else{
+        } else {
🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/donation/application/RefundDonationService.java
around line 38, the else keyword is incorrectly formatted as "}else{"; replace
it with the properly spaced form "} else {" so there is a space before and after
else to match project code style.

log.error("환불 처리 실패: donation IDs = {}", refundTargetDonationIds);
}
Comment on lines +34 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

부분 실패 시나리오를 처리하세요.

smartContractPort.refundDonations()가 boolean을 반환하는 설계는 all-or-nothing 결과만 표현할 수 있습니다. 만약 100건 중 50건만 성공하고 50건이 실패한다면, 어떤 기부가 환불되었고 어떤 기부가 실패했는지 추적할 수 없습니다.

다음을 고려하세요:

  • SmartContractPort.refundDonations()의 반환 타입을 List<Long> (성공한 ID 목록) 또는 Map<Long, RefundResult> 형태로 변경
  • 실패한 기부 ID를 별도로 기록하여 재시도 가능하도록 구현
  • 부분 성공 시에도 성공한 건수와 실패한 건수를 모두 로깅

이 설계 변경이 필요하다면, SmartContractPort 인터페이스 수정 및 어댑터 구현 코드 생성을 도와드릴 수 있습니다.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.donet.donet.donation.application.port.in;

public interface RefundDonationUsecase {
void refundDonation();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public interface FindDonationPort {

List<JoinedDonation> findJoinedDonations(User user, int size);
List<RegisteredDonation> findRegisteredDonations(User user, int size);

List<Donation> findRefundableDonation();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.donet.donet.donation.application.port.out;

import java.util.List;

public interface SmartContractPort {
boolean refundDonations(List<Long> donationIds);
}
Loading