Skip to content

Comments

[Feat] 스마트 컨트랙트 환불 메소드 호출하는 서버 스케줄러 작성#75

Open
imscow11253 wants to merge 15 commits intomainfrom
feat/#65
Open

[Feat] 스마트 컨트랙트 환불 메소드 호출하는 서버 스케줄러 작성#75
imscow11253 wants to merge 15 commits intomainfrom
feat/#65

Conversation

@imscow11253
Copy link
Member

@imscow11253 imscow11253 commented Nov 24, 2025

📌 PR 설명

기한이 만료 되었으나 모금이 완료되지 않은 기부에 대해서 환불을 진행하는 스케줄러를 작성 했습니다.
스마트 컨트랙트는 GAS 비용으로 인해 스케줄링을 돌리기가 힘듭니다. 스마트 컨트랙트를 활성화하기 위한 서버의 스케줄러 로직을 작성했습니다.

✅ 작업 내용

  • 스마트 컨트랙트 환불 메소드 호출하는 서버 스케줄러 작성

🔍 리뷰 요청 사항

현재 스마트 컨트랙트가 배포되지 않아 환경변수에 아무런 값을 넣어두었습니다. 그래서 서버를 실행할 때 오류가 발생할 수 있습니다.
컨트랙트 배포 후 테스트 후 머지하도록 하겠습니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 매일 자정(Asia/Seoul) 자동 환불 스케줄러 추가
    • 환불용 서비스 및 스마트 계약 연동을 통한 일괄 환불 처리 기능 도입
    • 스마트 계약(Java) 바인딩 추가로 이벤트 조회 및 계약 호출 지원
    • 환불 대상 조회 쿼리 및 포트(인터페이스) 추가
  • Chores

    • web3j 라이브러리 버전 업그레이드 및 관련 의존성 추가
  • Tests

    • 테스트용 Web3j 모킹 구성 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@imscow11253 imscow11253 self-assigned this Nov 24, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

Walkthrough

모금 기한이 지났으나 목표 미달인 기부를 스마트컨트랙트로 일괄 환불하는 스케줄러·유즈케이스·퍼시스턴스 쿼리, Web3j 연동 어댑터 및 web3j-generated Solidity Java 래퍼와 관련 설정·테스트 구성이 추가되었습니다.

Changes

Cohort / File(s) 변경 요약
의존성 업데이트
build.gradle
org.web3j:core 버전 4.9.44.12.0 업그레이드, org.web3j:contracts:4.12.0 추가
스케줄러
src/main/java/com/donet/donet/donation/adapter/in/scheduler/RefundScheduler.java
@Component 스케줄러 추가 — @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")로 매일 자정에 refundDonation() 호출
유즈케이스 서비스
src/main/java/com/donet/donet/donation/application/RefundDonationService.java
RefundDonationUsecase 구현체 추가 — 환불 대상 조회 후 SmartContractPort.refundDonations(...) 호출 및 성공/실패 로깅
입/출력 포트 변경
src/main/java/com/donet/donet/donation/application/port/in/RefundDonationUsecase.java,
src/main/java/com/donet/donet/donation/application/port/out/FindDonationPort.java,
src/main/java/com/donet/donet/donation/application/port/out/SmartContractPort.java
입력 포트에 refundDonation() 추가, 출력 포트에 findRefundableDonation()refundDonations(List<Long>) 시그니처 추가
퍼시스턴스 레이어
src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationPersistenceAdapter.java,
src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationRepository.java
JPQL 메서드 findRefundableDonations(LocalDate, BaseStatus) 추가 및 어댑터에서 findRefundableDonation() 구현(만료·목표미달·ACTIVE 필터)
스마트컨트랙트 래퍼
src/main/java/com/donet/donet/global/smartContract/Campaign.java,
src/main/java/com/donet/donet/global/smartContract/CampaignFactory.java
web3j로 생성된 Solidity Java 래퍼 추가 — 함수 호출, 이벤트 바인딩, 배포/로딩/이벤트 파싱 유틸 포함
스마트컨트랙트 어댑터 & 설정
src/main/java/com/donet/donet/global/smartContract/SmartContractAdapter.java,
src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java,
src/main/resources/application.yml,
src/test/java/com/donet/donet/global/config/Web3jTestConfig.java
SmartContractPort 구현체 추가(캠페인 주소 조회 → Contract.load(...).refund() 실행 및 tx 상태 검증), Web3j/Credentials/ContractGasProvider/CampaignFactory 빈 구성(프로덕션) 및 테스트용 Mockito 빈 구성, application.yml에 web3/contract 설정 추가

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as RefundScheduler
    participant UseCase as RefundDonationService
    participant Repo as DonationRepository/Adapter
    participant SmartPort as SmartContractAdapter
    participant Factory as CampaignFactory
    participant Contract as Campaign

    Scheduler->>UseCase: refundDonation()
    UseCase->>Repo: findRefundableDonation()
    Repo-->>UseCase: List<Donation> (IDs)
    alt donations exist
        UseCase->>SmartPort: refundDonations(List<Long>)
        SmartPort->>Factory: getCampaignAddress(donationId)
        Factory-->>SmartPort: campaignAddress
        SmartPort->>Contract: Campaign.load(campaignAddress).refund()
        Contract-->>SmartPort: TransactionReceipt (status)
        SmartPort-->>UseCase: boolean (all success?)
    else no donations
        UseCase-->>Scheduler: 종료 (대상 없음)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • 추가 검토가 권장되는 파일:
    • Campaign.java, CampaignFactory.java — web3j 생성 코드(이벤트·시그니처·배포/로딩 메서드) 검증
    • SmartContractAdapter.java — 트랜잭션 실행·예외 처리·TransactionReceipt 상태 확인 로직
    • DonationRepository.java JPQL 쿼리 — 만료·목표 미달·BaseStatus 필터링 정확성
    • Web3j 버전 업으로 인한 API 호환성(특히 Credentials/ContractGasProvider 사용)

Possibly related PRs

Poem

🐰 달빛 아래 코드를 톡톡,
자정에 돌아오는 환불의 속삭임,
체인 위 작은 트랜잭션마다,
잃어버린 동전들을 찾아 돌려주네,
깡총깡총 — 토끼가 축하해요 ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 변경사항의 주요 내용인 스마트 컨트랙트 환불 메서드 호출 스케줄러 작성을 명확하게 반영하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 있으며, 목적, 작업 내용, 리뷰 요청사항이 포함되어 있습니다.
Linked Issues check ✅ Passed 이슈 #65의 요구사항인 '모금 완료 안된 기부 환불 스케줄러 작성'이 충분히 구현되었습니다. RefundScheduler, RefundDonationService, FindDonationPort, SmartContractPort 등을 통해 스케줄링 기반의 환불 로직이 완성되었습니다.
Out of Scope Changes check ✅ Passed Web3j 의존성 업그레이드, Campaign/CampaignFactory 계약 래퍼, Web3j 설정, 환경변수 추가 등 모두 환불 스케줄러 기능 구현에 필요한 범위 내 변경사항입니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#65

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Nov 24, 2025

Test Results

12 tests   12 ✅  4s ⏱️
 8 suites   0 💤
 8 files     0 ❌

Results for commit 17cce40.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
src/main/java/com/donet/donet/donation/application/port/out/FindDonationPort.java (1)

22-22: 메소드에 JavaDoc 추가를 권장합니다.

"환불 가능한" 기부의 조건(마감일 경과, 목표 미달성, ACTIVE 상태)을 메소드 수준에서 문서화하면 인터페이스 사용자가 더 명확하게 이해할 수 있습니다.

다음과 같이 JavaDoc을 추가할 수 있습니다:

+    /**
+     * 환불 가능한 기부 목록을 조회합니다.
+     * 조건: 마감일이 지났고, 목표 금액에 미달하며, 상태가 ACTIVE인 기부
+     * 
+     * @return 환불 가능한 기부 목록
+     */
     List<Donation> findRefundableDonation();
src/main/java/com/donet/donet/donation/application/port/out/SmartContractPort.java (1)

5-7: 스마트 컨트랙트 포트 설계가 적절합니다.

배치 환불 작업을 위한 명확한 추상화를 제공합니다.

부분 실패 시나리오(일부 기부만 환불 성공)에 대한 처리 전략을 고려하세요. 현재 boolean 반환값으로는 어느 기부가 실패했는지 추적하기 어려울 수 있습니다. 로깅과 모니터링으로 보완하거나, 향후 더 상세한 결과 객체 반환을 고려할 수 있습니다.

src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationPersistenceAdapter.java (1)

129-135: 구현이 기존 패턴을 잘 따르고 있습니다.

donationMapper를 통한 변환과 스트림 처리가 어댑터 내 다른 메소드들과 일관성 있게 구현되었습니다.

테스트 용이성을 위해 LocalDate.now()Clock 빈 주입으로 대체하는 것을 고려할 수 있습니다. 이렇게 하면 단위 테스트에서 시간을 제어할 수 있습니다.

다음과 같이 개선할 수 있습니다:

@Component
@RequiredArgsConstructor
public class DonationPersistenceAdapter implements FindDonationPort, UpdateDonationPort, CreateDonationPort {
    private final DonationRepository donationRepository;
    // ... 기존 필드들
    private final Clock clock; // 추가

    @Override
    public List<Donation> findRefundableDonation() {
        return donationRepository.findRefundableDonations(LocalDate.now(clock))
                .stream()
                .map(donationMapper::mapToDomainEntity)
                .toList();
    }
}
src/main/java/com/donet/donet/global/smartContract/SmartContractAdapter.java (1)

52-61: 예외 처리·로깅 및 부분 실패 전략 개선 제안

현재는 어떤 donation 하나라도 실패하면 즉시 예외를 던지고 false를 반환하며, e.printStackTrace()만 남기기 때문에 운영 환경에서 원인 추적과 모니터링이 어렵습니다. Logger를 사용해 donationIds / 실패한 donationId / tx hash 등을 구조적으로 남기고, 필요하다면 “실패 건만 따로 기록하면서 나머지는 계속 시도”하는 부분 성공 전략도 고려해 보시면 좋겠습니다.

예시:

-        } catch (Exception e) {
-            e.printStackTrace();
-            return false;
-        }
+        } catch (Exception e) {
+            // TODO: 롤백/재시도 전략에 맞게 에러 처리 정책 확정
+            log.error("Smart contract refund batch failed. donationIds={}", donationIds, e);
+            return false;
+        }

(위 예시는 Lombok @Slf4j 추가가 필요합니다.)

src/main/java/com/donet/donet/donation/adapter/in/scheduler/RefundScheduler.java (1)

13-16: 스케줄 주기 하드코딩 대신 설정값으로 외부화 + 최소 로그 권장

매일 0시(Asia/Seoul)로 하드코딩된 크론식은 운영 중 주기를 조정할 때 매번 빌드가 필요해 유지보수성이 떨어집니다. application.yml에서 주입받도록 하면 환경별로 쉽게 조정할 수 있고, 배치성 작업 특성상 시작/종료 로그를 남기면 장애 시 추적이 훨씬 수월합니다.

-    @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
+    @Scheduled(cron = "${refund.scheduler.cron:0 0 0 * * *}", zone = "Asia/Seoul")
     public void runRefundJob() {
-        refundDonationUsecase.refundDonation();
+        log.info("Refund scheduler started");
+        refundDonationUsecase.refundDonation();
+        log.info("Refund scheduler finished");
     }

(예시처럼 사용할 경우 클래스에 Lombok @Slf4j 또는 Logger 주입이 필요합니다.)

src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (2)

15-34: Web3jConfig에서 @value 필드 주입과 @requiredargsconstructor 혼용

이 클래스는 모든 필드를 @Value로 필드 주입하고 있어, @RequiredArgsConstructor는 실제로 생성되는 생성자가 없습니다(필드가 final도 아니고 @nonnull도 아님). 동작에는 문제 없지만 주입 방식이 혼재되어 가독성이 떨어질 수 있으니,

  • 옵션 1: @RequiredArgsConstructor 제거하고 현재처럼 필드 주입만 사용
  • 옵션 2: @ConfigurationProperties 또는 생성자 주입으로 일원화

중 하나로 통일하는 것을 권장합니다.


35-57: web3 관련 설정값 유효성 검증 및 프로파일 분리 고려

rpcUrl, privateKey, gasPrice, gasLimit, campaignFactoryAddress가 비어 있거나 잘못된 경우에도 빈 생성 시점까지는 조용히 진행되다가, 실제 호출 시점(Credentials.create, web3 호출, Factory.load 등)에서 런타임 예외가 발생할 수 있습니다. PR 설명대로 아직 배포 전이라면, 최소한:

  • 특정 프로파일(예: web3, prod)에서만 이 설정 클래스를 활성화하거나,
  • 애플리케이션 시작 시점에 위 값들의 null/blank 여부를 검사해 더 친절한 오류 메시지로 fail-fast 하도록

정책을 잡아두면 이후 환경별 설정 누락 시 디버깅 비용을 줄일 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bac4217 and 3aa54f0.

📒 Files selected for processing (13)
  • build.gradle (1 hunks)
  • src/main/java/com/donet/donet/donation/adapter/in/scheduler/RefundScheduler.java (1 hunks)
  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationPersistenceAdapter.java (2 hunks)
  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationRepository.java (2 hunks)
  • src/main/java/com/donet/donet/donation/application/RefundDonationService.java (1 hunks)
  • src/main/java/com/donet/donet/donation/application/port/in/RefundDonationUsecase.java (1 hunks)
  • src/main/java/com/donet/donet/donation/application/port/out/FindDonationPort.java (1 hunks)
  • src/main/java/com/donet/donet/donation/application/port/out/SmartContractPort.java (1 hunks)
  • src/main/java/com/donet/donet/global/smartContract/Campaign.java (1 hunks)
  • src/main/java/com/donet/donet/global/smartContract/CampaignFactory.java (1 hunks)
  • src/main/java/com/donet/donet/global/smartContract/SmartContractAdapter.java (1 hunks)
  • src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (1 hunks)
  • src/main/resources/application.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-27T03:22:43.846Z
Learnt from: JangIkhwan
Repo: Konkuk-25-FinalProject/Donet-BE PR: 31
File: src/main/java/com/donet/donet/donation/adapter/out/persistence/donationRecord/DonationRecordMapper.java:24-35
Timestamp: 2025-10-27T03:22:43.846Z
Learning: In the Java/Spring codebase for Donet-BE project, Mapper classes (e.g., DonationRecordMapper) should not have repository dependencies. Mapper classes should only perform data transformation. Entity resolution and database lookups should be performed in the Adapter layer before calling the mapper method.

Applied to files:

  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationPersistenceAdapter.java
🧬 Code graph analysis (3)
src/main/java/com/donet/donet/donation/application/port/in/RefundDonationUsecase.java (7)
src/main/java/com/donet/donet/donation/application/port/in/AddDonationRecordUsecase.java (2)
  • AddDonationRecordUsecase (5-7)
  • addDonationRecord (6-6)
src/main/java/com/donet/donet/donation/application/port/out/UpdateDonationPort.java (1)
  • UpdateDonationPort (5-8)
src/main/java/com/donet/donet/donation/application/port/out/CreateDonationPort.java (2)
  • CreateDonationPort (5-7)
  • createDonation (6-6)
src/main/java/com/donet/donet/donation/application/port/in/GetDonationDetailUsecase.java (1)
  • GetDonationDetailUsecase (5-7)
src/main/java/com/donet/donet/review/application/port/in/CreateDonationReviewUsecase.java (1)
  • CreateDonationReviewUsecase (6-8)
src/main/java/com/donet/donet/donation/application/port/in/CreateDonationUsecase.java (2)
  • CreateDonationUsecase (5-7)
  • createDonation (6-6)
src/main/java/com/donet/donet/review/application/port/out/SaveDonationReviewPort.java (1)
  • SaveDonationReviewPort (5-7)
src/main/java/com/donet/donet/global/smartContract/Campaign.java (1)
src/main/java/com/donet/donet/global/smartContract/CampaignFactory.java (1)
  • InitializedEventResponse (844-846)
src/main/java/com/donet/donet/global/smartContract/CampaignFactory.java (1)
src/main/java/com/donet/donet/global/smartContract/Campaign.java (1)
  • InitializedEventResponse (642-644)
🔇 Additional comments (4)
src/main/java/com/donet/donet/donation/application/port/in/RefundDonationUsecase.java (1)

3-5: 잘 설계된 유스케이스 인터페이스입니다.

헥사고날 아키텍처 패턴을 준수하며, 환불 프로세스에 대한 명확한 진입점을 제공합니다.

build.gradle (1)

77-78: 더 정확한 검증을 위해 중간 버전들의 변경사항도 확인하겠습니다.

web3j 라이브러리 버전 업그레이드는 안전합니다.

web3j 4.12.0 업그레이드에서 명시적인 breaking changes가 없습니다. 변경사항은 Gradle 업데이트, 패키지 내 오타 수정, DynamicBytes 인코딩 개선 등 소규모 수정사항으로 제한됩니다. 4.9.4에서 4.12.0으로의 마이너 버전 업그레이드는 호환성 문제 없이 진행될 수 있습니다.

org.web3j:contracts:4.12.0 의존성 추가는 생성된 스마트 컨트랙트 래퍼(Campaign, CampaignFactory)를 지원하기 위한 의도된 변경사항입니다.

src/main/java/com/donet/donet/global/smartContract/Campaign.java (1)

32-671: 자동 생성된 Campaign 래퍼는 직접 수정 대신 재생성으로 관리하는 것이 좋습니다

전체 구조(이벤트, 함수 시그니처, refund/refundAll 메소드 등)를 보면 web3j 코드젠 결과 그대로로 보이고, 주석 처리된 CustomError 블록 뒤의 세미콜론도 컴파일상 문제는 없습니다. 이 파일은 수동으로 손대기보다, Solidity ABI/Binary가 변경될 때마다 web3j 코드젠으로 재생성하는 방식을 유지하시면 SmartContractAdapter에서 사용하는 메소드(refund, refundAll, 이벤트 파싱 등)와 실제 배포된 컨트랙트 사이의 불일치 위험을 줄일 수 있습니다.

src/main/java/com/donet/donet/global/smartContract/CampaignFactory.java (1)

47-875: CampaignFactory 래퍼는 코드젠 산출물로 유지하고 Adapter 사용 방식만 주의하면 될 것 같습니다

이 클래스도 web3j 코드젠 결과로 보이며, 각 이벤트/함수(getCampaignAddress, getCampaignDetails, 토큰 허용 설정 등)의 시그니처가 Solidity 컨트랙트 ABI와만 일치한다면 자체 구현 문제는 없어 보입니다. 다만 SmartContractAdapter에서 getCampaignAddress를 사용할 때는 이 메소드가 RemoteFunctionCall<String>을 반환하므로, 반드시 .send()로 실행해 실제 주소를 가져오는 방식(및 campaignId 인자 의미)과 일관되도록 관리해 주시면 됩니다.

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

smartContractPort.refundDonations(refundTargetDonationIds);
}
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

스마트 컨트랙트 호출 결과를 처리하세요.

Line 26에서 smartContractPort.refundDonations()의 반환값을 무시하고 있습니다. 환불이 실패해도 감지할 수 없어 데이터 불일치가 발생할 수 있습니다.

다음을 추가하세요:

  • 반환값 확인 및 실패 시 로깅
  • 부분 실패 시나리오 처리
  • 메트릭 또는 알림을 통한 모니터링

다음과 같이 개선할 수 있습니다:

+import lombok.extern.slf4j.Slf4j;
+
+@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();
 
-        smartContractPort.refundDonations(refundTargetDonationIds);
+        if (refundTargetDonationIds.isEmpty()) {
+            log.info("환불 대상 기부가 없습니다.");
+            return;
+        }
+
+        log.info("환불 처리 시작: {} 건", refundTargetDonationIds.size());
+        boolean success = smartContractPort.refundDonations(refundTargetDonationIds);
+        
+        if (success) {
+            log.info("환불 처리 완료: {} 건", refundTargetDonationIds.size());
+        } else {
+            log.error("환불 처리 실패: donation IDs = {}", refundTargetDonationIds);
+            // TODO: 알림 또는 재시도 로직 추가 고려
+        }
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/donation/application/RefundDonationService.java
around lines 19 to 27, the result of smartContractPort.refundDonations(...) is
currently ignored which can hide failed refunds and cause data drift; update the
method to capture the return value, verify which donation IDs succeeded and
which failed, log failures with sufficient context, handle partial failures by
retrying or marking failed donations in the DB (or scheduling compensating
actions), and emit a metric/alert for non-zero failures so ops can monitor and
respond.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/main/java/com/donet/donet/donation/application/RefundDonationService.java (1)

21-41: 멱등성과 재시도 로직을 추가하는 것을 권장합니다.

현재 구현은 다음 문제가 있을 수 있습니다:

  • 스케줄러가 중복 실행되면 동일한 기부에 대해 환불을 여러 번 시도할 수 있습니다
  • 환불 실패 시 자동 재시도가 없어 수동 개입이 필요합니다

다음을 고려하세요:

  • Donation 엔티티에 환불 상태 필드 추가 (예: PENDING_REFUND, REFUNDED, REFUND_FAILED)
  • 환불 성공 시 상태를 업데이트하여 중복 처리 방지
  • 실패한 기부에 대한 재시도 로직 추가 (exponential backoff 권장)
  • findRefundableDonation()에서 REFUNDED 상태인 기부는 제외하도록 필터링
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa54f0 and a4203be.

📒 Files selected for processing (4)
  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationPersistenceAdapter.java (3 hunks)
  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationRepository.java (3 hunks)
  • src/main/java/com/donet/donet/donation/application/RefundDonationService.java (1 hunks)
  • src/main/java/com/donet/donet/global/smartContract/SmartContractAdapter.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/com/donet/donet/global/smartContract/SmartContractAdapter.java
  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationRepository.java
  • src/main/java/com/donet/donet/donation/adapter/out/persistence/donation/DonationPersistenceAdapter.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
src/main/java/com/donet/donet/donation/application/RefundDonationService.java (2)

13-16: LGTM!

클래스 선언과 어노테이션이 적절합니다. Lombok을 통한 생성자 주입과 Spring Service 등록이 올바르게 구성되었습니다.


18-19: LGTM!

포트 인터페이스를 통한 의존성 주입이 헥사고날 아키텍처 원칙에 맞게 잘 구현되었습니다.

Comment on lines +23 to +26
List<Long> refundTargetDonationIds = findDonationPort.findRefundableDonation()
.stream()
.map(Donation::getId)
.toList();
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.

Comment on lines +34 to +40
boolean success = smartContractPort.refundDonations(refundTargetDonationIds);

if (success) {
log.info("환불 처리 완료: {} 건", refundTargetDonationIds.size());
}else{
log.error("환불 처리 실패: donation IDs = {}", refundTargetDonationIds);
}
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 인터페이스 수정 및 어댑터 구현 코드 생성을 도와드릴 수 있습니다.


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.

- test에서는 web3j관련 빈을 모킹하도록 수정
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (3)

18-18: 불필요한 @requiredargsconstructor 애노테이션

모든 필드가 @Value로 주입되므로 @RequiredArgsConstructor가 생성하는 생성자는 비어있습니다. 이 애노테이션은 제거해도 무방합니다.

 @Profile("!test")
 @Configuration
-@RequiredArgsConstructor
 public class Web3jConfig {

38-40: Web3j 연결 검증 고려

현재 HttpService는 URL 유효성이나 연결 가능 여부를 검증하지 않습니다. 프로덕션 환경에서는 연결 타임아웃이나 재시도 정책을 설정하는 것을 권장합니다.

예시:

@Bean
public Web3j web3j() {
    HttpService httpService = new HttpService(rpcUrl);
    // 연결 타임아웃 설정 (선택사항)
    // httpService.addHeader("timeout", "10000");
    return Web3j.build(httpService);
}

48-50: 정적 가스 제공자 사용 확인

StaticGasProvider는 고정된 gas price와 gas limit을 사용합니다. 프로덕션 환경에서는 네트워크 상황에 따라 가스 가격이 변동할 수 있으므로, 필요시 DefaultGasProvider나 동적 가스 가격 전략을 고려하세요.

현재 구현은 테스트넷이나 초기 개발 단계에서는 적절합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4203be and 17cce40.

📒 Files selected for processing (2)
  • src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (1 hunks)
  • src/test/java/com/donet/donet/global/config/Web3jTestConfig.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (1)
src/test/java/com/donet/donet/global/config/Web3jTestConfig.java (1)
  • Profile (13-39)
src/test/java/com/donet/donet/global/config/Web3jTestConfig.java (1)
src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (1)
  • Profile (16-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
src/test/java/com/donet/donet/global/config/Web3jTestConfig.java (2)

13-15: 테스트 설정 분리가 잘 되어 있습니다

@Profile("test")를 사용하여 프로덕션 환경의 Web3jConfig와 충돌 없이 테스트 환경에서만 모킹된 빈을 제공하도록 잘 구성되어 있습니다.


17-38: 모킹 구성이 적절합니다

Web3j 관련 모든 빈을 Mockito로 모킹하여 단위 테스트에서 외부 의존성 없이 테스트할 수 있도록 잘 구성되어 있습니다.

campaignFactory 메서드의 파라미터들이 사용되지 않는 것은 모킹 컨텍스트에서 정상입니다. 메서드 시그니처가 프로덕션 설정과 일치하여 일관성이 유지됩니다.

src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (1)

53-59: Let me check if there's any validation code in the SmartContractAdapter that uses CampaignFactory:Based on my verification of the review comment and the search results, I can now provide the rewritten review:

배포 후 CampaignFactory 검증 필요

web3j의 Contract.load() 메서드는 로드 시점에 컨트랙트 주소를 자동으로 검증하지 않으며, 배포된 바이트코드가 실제로 일치하는지 확인하려면 isValid() 메서드 호출이 필요합니다. 스마트 컨트랙트 배포 후 다음을 확인하세요:

  • campaignFactoryAddress 설정값이 유효한 이더리움 주소 형식(0x + 40개 16진수 문자)인지 검증
  • 해당 주소에 컨트랙트가 실제로 배포되어 있는지 확인
  • Web3jConfig.java 라인 58의 CampaignFactory.load() 호출 시 반환된 인스턴스에 대해 isValid() 메서드로 검증

Comment on lines +21 to +35
@Value("${web3.rpcUrl}")
private String rpcUrl;

@Value("${web3.privateKey}")
private String privateKey;

@Value("${web3.gasPrice}")
private BigInteger gasPrice;

@Value("${web3.gasLimit}")
private BigInteger gasLimit;

// 이미 배포되어 있는 CampaignFactory의 on-chain 주소
@Value("${contracts.campaignFactoryAddress}")
private String campaignFactoryAddress;
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

🧩 Analysis chain

설정 값 검증 및 에러 처리 필요

PR 설명에서 언급한 대로 현재 환경 변수가 비어있어 런타임 에러가 발생할 수 있습니다. 빈 설정 또는 잘못된 형식의 값이 주입될 경우를 대비한 검증 로직을 추가하는 것을 권장합니다.

특히 다음 사항을 확인하세요:

  • privateKey가 유효한 형식인지 검증 (예: hex 문자열 길이 확인)
  • rpcUrl이 유효한 URL 형식인지 검증
  • campaignFactoryAddress가 유효한 이더리움 주소 형식인지 검증
  • 민감한 정보인 privateKey가 로그에 노출되지 않도록 주의

다음 스크립트로 애플리케이션이 시작될 때 설정 검증이 제대로 이루어지는지 확인하세요:


🏁 Script executed:

#!/bin/bash
# 설정 검증 로직이 있는지 확인
rg -n "privateKey|rpcUrl|campaignFactoryAddress" --type java -A 5 -B 5 -g '!**/test/**'

Length of output: 4046



---

🏁 Script executed:

```shell
#!/bin/bash
# View complete Web3jConfig.java file
cat src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java

Length of output: 1624


설정 값 검증 및 에러 처리 필수

현재 Web3jConfig.java의 설정 값들이 검증 없이 직접 사용되고 있습니다. 빈 또는 잘못된 형식의 설정이 주입될 경우 애플리케이션 시작 시점에 실패합니다.

다음 위치에서 검증 로직 추가가 필요합니다:

  • Line 39 (web3j() 메서드): rpcUrl을 HttpService에 전달하기 전에 유효한 URL 형식 확인
  • Line 44 (credentials() 메서드): privateKey를 Credentials.create에 전달하기 전에 유효한 hex 문자열 형증 (길이 130자 또는 128자)
  • Line 58 (campaignFactory() 메서드): campaignFactoryAddress를 CampaignFactory.load에 전달하기 전에 유효한 이더리움 주소 형식 확인 (0x로 시작하는 40자 hex)

추가로 @Value 필드에 @NotBlank, @NotEmpty 등의 검증 애노테이션을 추가하고, 민감한 정보인 privateKey가 에러 로그에 노출되지 않도록 주의하세요.

🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (fields
around lines 21–35, methods referenced at ~39, 44, 58): add defensive validation
and safe error handling for injected properties — annotate rpcUrl, privateKey,
campaignFactoryAddress with @NotBlank/@NotEmpty, then in web3j() validate rpcUrl
is a well-formed HTTP(S) URL before passing to HttpService and throw a clear
IllegalArgumentException if invalid; in credentials() validate privateKey is a
hex string of 128 or 130 chars (allow optional 0x), do NOT log the key, and
throw a descriptive exception if invalid; in campaignFactory() validate
campaignFactoryAddress is a 0x-prefixed 40-byte hex address (or normalize by
adding 0x) before calling CampaignFactory.load and throw if invalid; ensure all
exceptions include non-sensitive details only and update method signatures/bean
creation to fail fast with clear messages.

Comment on lines +43 to +45
public Credentials credentials() {
return Credentials.create(privateKey);
}
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

🧩 Analysis chain

Private key 보안 및 유효성 검증 필요

Credentials.create()는 유효하지 않은 private key 형식에 대해 예외를 발생시킵니다. Private key가 올바른 hex 형식인지 사전 검증하는 것을 권장합니다.

보안 주의사항:

  • Private key가 로그에 노출되지 않도록 주의하세요
  • 애플리케이션 설정 파일에 평문으로 저장하지 말고 환경 변수나 보안 볼트를 사용하세요
  • Private key가 버전 관리 시스템에 커밋되지 않도록 확인하세요

다음 스크립트로 private key가 로그에 노출되는지 확인하세요:


🏁 Script executed:

#!/bin/bash
# Private key 로깅 여부 확인
rg -n "privateKey.*log|log.*privateKey|System\.out.*privateKey" --type java -g '!**/test/**'

Length of output: 0


더 정확한 검증을 위해 추가 정보가 필요합니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# Web3jConfig.java 전체 내용 확인
find . -name "Web3jConfig.java" -type f ! -path "*/test/*" -exec cat {} \;

Length of output: 1624


🏁 Script executed:

#!/bin/bash
# privateKey 필드 및 초기화 방식 확인
rg -n "privateKey" --type java -g '!**/test/**' -B5 -A5 | head -100

Length of output: 2167


🏁 Script executed:

#!/bin/bash
# 로깅 관련 설정 파일 확인
find . -name "logback*.xml" -o -name "log4j*.xml" -o -name "log4j*.properties" | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 예외 처리 패턴 및 스택 트레이스 로깅 확인
rg -n "catch.*Exception|printStackTrace|logger.*Exception" --type java -g '!**/test/**' -C2 | head -50

Length of output: 5283


🏁 Script executed:

#!/bin/bash
# application.yml/properties 파일에서 privateKey 설정 방식 확인
find . -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/null

Length of output: 2685


🏁 Script executed:

#!/bin/bash
# pom.xml에서 web3j 버전 확인
find . -name "pom.xml" -exec cat {} \; | grep -A5 "web3j"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# web3j Credentials.create() 동작 확인을 위한 웹 검색 준비
echo "web3j Credentials.create validation"

Length of output: 35


추가 검증이 필요합니다.


🏁 Script executed:

#!/bin/bash
# pom.xml 재확인 - 전체 내용
cat pom.xml 2>/dev/null | head -200

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Credentials 관련 예외 처리 확인
rg -n "Credentials\." --type java -g '!**/test/**' -B3 -A3

Length of output: 610


🏁 Script executed:

#!/bin/bash
# 빈 초기화 관련 에러 로깅 확인
rg -n "InitializationException|BeanInstantiation|BeanCreation" --type java -g '!**/test/**'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# SmartContractAdapter 전체 내용 확인
find . -name "SmartContractAdapter.java" -type f ! -path "*/test/*" -exec cat {} \;

Length of output: 2243


🌐 Web query:

web3j Credentials.create() validation error handling private key

💡 Result:

Short answer: Credentials.create expects a 32-byte private key encoded as 64 hex characters (0x prefix optional). Common failures come from non-hex chars, wrong length (e.g. 63 chars) or passing a decimal string — web3j will throw NumberFormatException / IllegalArgumentException when it can't parse the key. See docs. [1]

How to handle it

  • Normalize: strip "0x" if present.
  • Ensure hex and length = 64; if shorter, pad left with '0' (leading zeros are valid private keys). If longer or non-hex, reject.
  • Wrap Credentials.create in try/catch and throw a clear error message.

Example (concise):

String normalizePrivateKey(String pk) {
  if (pk == null) throw new IllegalArgumentException("privateKey is null");
  pk = pk.startsWith("0x") ? pk.substring(2) : pk;
  if (!pk.matches("[0-9a-fA-F]+")) throw new IllegalArgumentException("privateKey must be hex");
  if (pk.length() > 64) throw new IllegalArgumentException("privateKey too long");
  if (pk.length() < 64) pk = String.format("%64s", pk).replace(' ', '0'); // pad left
  return pk;
}

try {
  String key = normalizePrivateKey(inputPrivateKey);
  Credentials creds = Credentials.create(key);
} catch (NumberFormatException | IllegalArgumentException e) {
  // handle invalid key: inform user / log / fail-safe
}

Notes:

  • Padding shorter keys to 64 hex chars is an accepted workaround (some key sources omit leading zeros). [2]
  • Always avoid logging or exposing private keys in error messages or logs.

Sources: web3j Credentials docs and community discussion about 63/64-char keys and padding. [1][2]

References
[1] web3j Credentials docs.
[2] Discussion (Web3Auth / StackOverflow echo) about 63-char private keys and padding.


Private key 유효성 검증 및 예외 처리 필수

Credentials.create(privateKey) 호출 시 유효하지 않은 형식에 대한 예외 처리와 사전 검증이 없습니다.

문제점:

  • 유효하지 않은 private key 형식(non-hex, 길이 부족 등)이 전달되면 NumberFormatException 또는 IllegalArgumentException 발생
  • 예외 처리 없이 bean 초기화 실패 시 스택 트레이스에 private key 값이 노출될 수 있음

권장사항:

  1. Private key 형식 검증 추가 (hex 여부, 길이 64자, "0x" prefix 제거)
  2. Credentials.create() 호출을 try-catch로 감싸고 명확한 에러 메시지 반환
  3. 에러 메시지에 private key 값을 포함하지 않기
  4. 환경 변수 설정 누락 시 애플리케이션 시작 시점에 실패하는 것이 바람직 (현재는 이미 적용 중)
private String normalizePrivateKey(String pk) {
    if (pk == null || pk.isEmpty()) {
        throw new IllegalArgumentException("privateKey must not be empty");
    }
    pk = pk.startsWith("0x") ? pk.substring(2) : pk;
    if (!pk.matches("[0-9a-fA-F]+")) {
        throw new IllegalArgumentException("privateKey must be hexadecimal");
    }
    if (pk.length() != 64) {
        throw new IllegalArgumentException("privateKey must be 64 hex characters");
    }
    return pk;
}

@Bean
public Credentials credentials() {
    try {
        return Credentials.create(normalizePrivateKey(privateKey));
    } catch (IllegalArgumentException e) {
        throw new RuntimeException("Invalid privateKey configuration", e);
    }
}
🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java around
lines 43-45, the credentials() bean directly calls
Credentials.create(privateKey) without validating the key or handling
exceptions; add a normalize step that checks for null/empty, strips a leading
"0x", verifies the string is hexadecimal and exactly 64 characters, then call
Credentials.create with the normalized key inside a try-catch that wraps
IllegalArgumentException/NumberFormatException into a RuntimeException with a
clear message that does NOT include the private key value; this ensures
invalid/missing env config fails fast during startup and prevents leaking the
private key in stack traces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] : 모금 완료 안된 기부 환불 스케줄러 작성하기

2 participants