[FEATURE] 외주 스케줄러 및 이메일 구현#60
Conversation
- 1차 시안 마감 로직을 위한 FirstDraftDeadlineProcessor 구현 - 1차 시안 마감 관련 이벤트 및 이벤트 리스너 작성 - 1차 시안 미제출자 알림 및 환불 요청 이메일 발송 로직 추가 - CommissionScheduler에 1차 시안 마감 스케줄러 설정 - KST 기반 날짜 및 시간 처리 로직 반영 Refs: #54
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough외주 지원, 1차 시안, 최종 마감 처리와 관련된 스케줄러, 상태 전이, 환불·정산 계산, 이벤트 발행, outbox 저장, 이메일 템플릿이 추가되었다. Changes외주 마감 및 이메일 흐름
Sequence Diagram(s)sequenceDiagram
participant CommissionScheduler
participant CommissionDeadlineService
participant ApplicationDeadlineProcessor
participant FirstDraftDeadlineProcessor
participant FinalDeadlineProcessor
CommissionScheduler->>CommissionDeadlineService: processApplicationDeadlines()
CommissionDeadlineService->>ApplicationDeadlineProcessor: process(commissionId, mailScheduledAt)
CommissionScheduler->>CommissionDeadlineService: processFirstDraftDeadlines()
CommissionDeadlineService->>FirstDraftDeadlineProcessor: process(commissionId, mailScheduledAt)
CommissionScheduler->>CommissionDeadlineService: processFinalDeadlines()
CommissionDeadlineService->>FinalDeadlineProcessor: process(commissionId, mailScheduledAt)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 PMD (7.25.0)src/main/java/ditda/backend/domain/payment/entity/Payment.java[ERROR] Cannot load ruleset .coderabbit-pmd-ruleset.xml: Cannot resolve rule/ruleset reference '.coderabbit-pmd-ruleset.xml'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
src/main/java/ditda/backend/domain/commission/core/policy/CommissionPricePolicy.java (1)
28-31: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value정수 나눗셈 절삭 동작을 의도한 것인지 확인 바랍니다.
level3Reward * shortfallCount * 4 / 3는 정수 연산이라level3Reward * shortfallCount가 3의 배수가 아니면 절삭됩니다. 현재 LEVEL_3 기본금(60,000)은 3의 배수라 정확히 떨어지지만, 향후 기본금 정책이 바뀌면 환불 금액에서 미세한 오차가 발생할 수 있습니다. 의도된 절삭이 아니라면 곱셈을 먼저 모두 수행한 뒤(이미 그렇게 동작) 정밀도 손실 여부를 주석으로 명시하거나 반올림 정책을 명확히 해두는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/ditda/backend/domain/commission/core/policy/CommissionPricePolicy.java` around lines 28 - 31, The calculation in CommissionPricePolicy.calculateApplicationShortfallRefund relies on integer arithmetic, so the existing level3Reward * shortfallCount * 4 / 3 expression may truncate if the intermediate value is not divisible by 3. Verify whether this truncation is intended; if not, update the calculation to make the rounding behavior explicit by using a clearer rounding strategy or document the exact expected precision in calculateApplicationShortfallRefund and related reward methods like calculateDraftSubmissionReward.src/main/java/ditda/backend/global/email/NotificationOutboxScheduler.java (1)
55-55: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winTODO: FAILED 상태 시 Discord 웹훅 알림.
발송이 3회 실패해
FAILED로 전이된 레코드에 대한 알림(웹훅) 처리가 아직 구현되지 않았습니다. 운영 가시성 측면에서 중요합니다. 구현 코드를 생성하거나 추적용 이슈를 열어 드릴까요?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/ditda/backend/global/email/NotificationOutboxScheduler.java` at line 55, The FAILED-state Discord webhook notification is still missing in NotificationOutboxScheduler, so add the implementation for records that transition to FAILED after 3 send attempts. Locate the existing status handling in NotificationOutboxScheduler and extend it to trigger the Discord webhook when outbox.getStatus() becomes FAILED, using the scheduler’s current notification flow and any existing webhook/client helper methods if present. Keep the FAILED handling isolated so it’s easy to trace and maintain alongside the current outbox processing logic.src/main/java/ditda/backend/global/converter/MapToJsonConverter.java (1)
34-36: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value공백 문자열(blank) DB 값 처리 누락.
isEmpty()만 검사하므로" "같은 공백 문자열은 파싱 경로로 진입해 역직렬화 실패로 이어집니다.isBlank()로 방어하는 것을 고려해 주세요.♻️ 제안 변경
- if (dbData == null || dbData.isEmpty()) { + if (dbData == null || dbData.isBlank()) { return Map.of(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/ditda/backend/global/converter/MapToJsonConverter.java` around lines 34 - 36, The MapToJsonConverter null/empty guard does not cover blank strings, so whitespace-only DB values still reach the JSON parsing path and can fail. Update the early return in MapToJsonConverter’s conversion logic to treat blank input the same as empty input, using the existing dbData check before deserialization. Keep the fix localized to the converter method that currently checks dbData.isEmpty().src/main/java/ditda/backend/domain/commission/core/service/CommissionDeadlineService.java (1)
33-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value세 메서드의 중복 구조 정리 검토 (선택).
processApplicationDeadlines / processFirstDraftDeadlines / processFinalDeadlines는 today 계산 → mailScheduledAt 계산 → 조회 → for 루프 try/catch 로깅 흐름이 거의 동일합니다. 조회 함수와 프로세서 호출만 파라미터로 받는 공통 헬퍼로 추출하면 중복과// TODO: 디스코드 웹훅처리 누락 위험을 줄일 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/ditda/backend/domain/commission/core/service/CommissionDeadlineService.java` around lines 33 - 103, The three deadline methods in CommissionDeadlineService are nearly identical, so extract the shared today/mailScheduledAt lookup, repository fetch, and try/catch loop into a common helper. Keep the specific parts—CommissionRepository query, CommissionDeadlineProcessor call (applicationDeadlineProcessor, firstDraftDeadlineProcessor, finalDeadlineProcessor), and the log message—passed in as parameters or lambdas, so the duplicated structure and TODO handling live in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/ditda/backend/domain/auth/notification/EmailVerificationMailer.java`:
- Line 20: The authentication email flow in EmailVerificationMailer now uses
EmailSender.sendAsync, which hides delivery failures from the caller. Change the
verification-code path to use the synchronous send method (or route it through a
retry-safe outbox) so exceptions propagate back to the code that issues the
verification code; keep the fix localized to EmailVerificationMailer and the
emailSender invocation used for 인증 코드 발급.
In
`@src/main/java/ditda/backend/domain/commission/core/processor/FinalDeadlineProcessor.java`:
- Around line 46-67: `FinalDeadlineProcessor.process()` already publishes
`publishFinalDeadlineClosedEvent(...)` after `applyFinalDeadline(...)`, so the
CASE1 path in `applyFinalDeadline(...)` is duplicating the same event. Remove
the extra `publishFinalDeadlineClosedEvent(...)` call from the `DRAFT_SELECTING`
branch and keep event emission centralized in `process()` so
`FinalDeadlineClosedEvent` is sent exactly once for both `applyFinalDeadline`
paths.
In
`@src/main/java/ditda/backend/domain/commission/core/processor/FirstDraftDeadlineProcessor.java`:
- Around line 92-111: The partial refund flow in handlePartialSubmission should
not run when there are no missed submissions. Add a guard before calculating
missedLevels and calling paymentService.requestPartialRefund so the refund path
is skipped when missed is empty, while still keeping
commission.startDraftSelecting() and
applicationService.markAllDraftMissed(missed) behavior intact. Use the existing
handlePartialSubmission, missed, and paymentService.requestPartialRefund symbols
to locate and adjust the logic.
In `@src/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.java`:
- Around line 68-72: The timestamp handling in DraftFacade is inconsistent
because applyDesignerSelection uses a system-default LocalDateTime now while
mail scheduling uses ZONE_KST. Update the now value in the selectDesigner flow
to use the same KST-based clock as mailScheduledAt, and keep the selection time,
response timestamp, and event scheduling aligned through the same timezone
reference.
In `@src/main/java/ditda/backend/domain/payment/entity/Payment.java`:
- Around line 83-93: The partial refund flow in
Payment.markPartialRefundRequested is not idempotent and also lacks validation
for non-positive refund amounts. Update this method to reject refundAmount <= 0,
and add a status transition or other guard so repeated invocations for the same
payment cannot subtract the amount twice. Also avoid mutating the original
amount field directly; instead, use a separate field for accumulated refunded
amount (or equivalent) so Payment keeps the original total for tracking and
audit purposes.
In `@src/main/java/ditda/backend/global/email/NotificationOutboxScheduler.java`:
- Around line 40-58: The outbox processing in
NotificationOutboxScheduler.processPendingAlerts can send the same email more
than once because findPendingScheduled returns PENDING rows without any
claim/lock, and a successful emailSender.send followed by a failed
outboxRepository.save will cause retries to resend. Update the flow to
atomically claim each NotificationOutbox before sending, such as transitioning
PENDING to SENDING with a conditional update or adding a pessimistic lock on the
pending query, and only markSent()/persist the final state after the claim is
secured.
---
Nitpick comments:
In
`@src/main/java/ditda/backend/domain/commission/core/policy/CommissionPricePolicy.java`:
- Around line 28-31: The calculation in
CommissionPricePolicy.calculateApplicationShortfallRefund relies on integer
arithmetic, so the existing level3Reward * shortfallCount * 4 / 3 expression may
truncate if the intermediate value is not divisible by 3. Verify whether this
truncation is intended; if not, update the calculation to make the rounding
behavior explicit by using a clearer rounding strategy or document the exact
expected precision in calculateApplicationShortfallRefund and related reward
methods like calculateDraftSubmissionReward.
In
`@src/main/java/ditda/backend/domain/commission/core/service/CommissionDeadlineService.java`:
- Around line 33-103: The three deadline methods in CommissionDeadlineService
are nearly identical, so extract the shared today/mailScheduledAt lookup,
repository fetch, and try/catch loop into a common helper. Keep the specific
parts—CommissionRepository query, CommissionDeadlineProcessor call
(applicationDeadlineProcessor, firstDraftDeadlineProcessor,
finalDeadlineProcessor), and the log message—passed in as parameters or lambdas,
so the duplicated structure and TODO handling live in one place.
In `@src/main/java/ditda/backend/global/converter/MapToJsonConverter.java`:
- Around line 34-36: The MapToJsonConverter null/empty guard does not cover
blank strings, so whitespace-only DB values still reach the JSON parsing path
and can fail. Update the early return in MapToJsonConverter’s conversion logic
to treat blank input the same as empty input, using the existing dbData check
before deserialization. Keep the fix localized to the converter method that
currently checks dbData.isEmpty().
In `@src/main/java/ditda/backend/global/email/NotificationOutboxScheduler.java`:
- Line 55: The FAILED-state Discord webhook notification is still missing in
NotificationOutboxScheduler, so add the implementation for records that
transition to FAILED after 3 send attempts. Locate the existing status handling
in NotificationOutboxScheduler and extend it to trigger the Discord webhook when
outbox.getStatus() becomes FAILED, using the scheduler’s current notification
flow and any existing webhook/client helper methods if present. Keep the FAILED
handling isolated so it’s easy to trace and maintain alongside the current
outbox processing logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: e4a0897f-0694-4afe-81ae-c87ef3c9e231
📒 Files selected for processing (55)
src/main/java/ditda/backend/domain/auth/notification/DesignerSignupMailer.javasrc/main/java/ditda/backend/domain/auth/notification/EmailVerificationMailer.javasrc/main/java/ditda/backend/domain/commission/application/entity/CommissionApplication.javasrc/main/java/ditda/backend/domain/commission/application/entity/enums/ApplicationStatus.javasrc/main/java/ditda/backend/domain/commission/application/exception/ApplicationErrorCode.javasrc/main/java/ditda/backend/domain/commission/application/repository/CommissionApplicationRepository.javasrc/main/java/ditda/backend/domain/commission/application/service/ApplicationService.javasrc/main/java/ditda/backend/domain/commission/core/entity/Commission.javasrc/main/java/ditda/backend/domain/commission/core/event/ApplicationDeadlineClosedEvent.javasrc/main/java/ditda/backend/domain/commission/core/event/CommissionCompletedEvent.javasrc/main/java/ditda/backend/domain/commission/core/event/DraftSelectedEvent.javasrc/main/java/ditda/backend/domain/commission/core/event/FinalDeadlineClosedEvent.javasrc/main/java/ditda/backend/domain/commission/core/event/FirstDraftDeadlineClosedEvent.javasrc/main/java/ditda/backend/domain/commission/core/event/PayoutRequestedEvent.javasrc/main/java/ditda/backend/domain/commission/core/notification/ApplicationDeadlineClosedNotifier.javasrc/main/java/ditda/backend/domain/commission/core/notification/CommissionCompletedNotifier.javasrc/main/java/ditda/backend/domain/commission/core/notification/DraftSelectedNotifier.javasrc/main/java/ditda/backend/domain/commission/core/notification/FinalDeadlineClosedNotifier.javasrc/main/java/ditda/backend/domain/commission/core/notification/FirstDraftDeadlineClosedNotifier.javasrc/main/java/ditda/backend/domain/commission/core/notification/PayoutRequestedNotifier.javasrc/main/java/ditda/backend/domain/commission/core/policy/CommissionPricePolicy.javasrc/main/java/ditda/backend/domain/commission/core/processor/ApplicationDeadlineProcessor.javasrc/main/java/ditda/backend/domain/commission/core/processor/FinalDeadlineProcessor.javasrc/main/java/ditda/backend/domain/commission/core/processor/FirstDraftDeadlineProcessor.javasrc/main/java/ditda/backend/domain/commission/core/repository/CommissionRepository.javasrc/main/java/ditda/backend/domain/commission/core/scheduler/CommissionScheduler.javasrc/main/java/ditda/backend/domain/commission/core/service/CommissionDeadlineService.javasrc/main/java/ditda/backend/domain/commission/draft/facade/DraftFacade.javasrc/main/java/ditda/backend/domain/designer/entity/Designer.javasrc/main/java/ditda/backend/domain/designer/entity/enums/ExpReward.javasrc/main/java/ditda/backend/domain/payment/entity/Payment.javasrc/main/java/ditda/backend/domain/payment/exception/PaymentErrorCode.javasrc/main/java/ditda/backend/domain/payment/notification/DepositMailer.javasrc/main/java/ditda/backend/domain/payment/service/PaymentService.javasrc/main/java/ditda/backend/global/converter/MapToJsonConverter.javasrc/main/java/ditda/backend/global/email/EmailSender.javasrc/main/java/ditda/backend/global/email/NotificationOutbox.javasrc/main/java/ditda/backend/global/email/NotificationOutboxRepository.javasrc/main/java/ditda/backend/global/email/NotificationOutboxScheduler.javasrc/main/java/ditda/backend/global/email/OutboxStatus.javasrc/main/resources/templates/email/admin-payout-request.htmlsrc/main/resources/templates/email/admin-refund-request.htmlsrc/main/resources/templates/email/commission-cancelled.htmlsrc/main/resources/templates/email/commission-finalized-designer.htmlsrc/main/resources/templates/email/commission-finalized-instructor.htmlsrc/main/resources/templates/email/commission-matched-designer.htmlsrc/main/resources/templates/email/commission-matched-instructor.htmlsrc/main/resources/templates/email/commission-shortfall-instructor.htmlsrc/main/resources/templates/email/final-cancelled-designer.htmlsrc/main/resources/templates/email/final-cancelled-instructor.htmlsrc/main/resources/templates/email/first-draft-missed-designer.htmlsrc/main/resources/templates/email/first-draft-rejected-designer.htmlsrc/main/resources/templates/email/first-draft-selected-designer.htmlsrc/main/resources/templates/email/first-draft-shortfall-instructor.htmlsrc/main/resources/templates/email/first-draft-zero-instructor.html
🚀 Related issue
Closes #54
#️⃣ Summary
외주 마감 처리는 자정에 일괄적으로 일어나지만 사용자가 자정에 메일을 받으면 UX적으로 안 좋을 것이다라고 판단해, 메일은 아침 시간대에 발송하도록 분리할 필요가 있어 Outbox 패턴을 도입했습니다.
Message Queue 도입도 고려했으나, 현재 규모에서는 인프라 비용과 운영 복잡도가 과하다고 판단하여 DB 기반 Outbox + 스케줄러 방식을 채택했습니다.
🔧 Changes
NotificationOutbox엔티티 + Map -> JSON 컨버터 추가send) / 비동기(sendAsync) 메서드 분리applicationDeadline자동 처리 스케줄러 (지원자 0/미달/충족 케이스별 취소·환불·매칭)firstDraftDeadline자동 처리 스케줄러 (시안 0/일부 미제출 케이스별 취소·부분 환불)finalDeadline자동 처리 스케줄러 (강사 미선택 취소 / 수정 진행 중 자동 최종 확정)PayoutRequestedEvent도입 (어드민 정산 요청 — 자동/수동 확정, 취소 환불, 미선택 정산 모든 시점 재사용)CommissionCompletedEvent도입 (외주 확정 시 강사/디자이너 알림)DraftSelectedEvent도입 (강사 시안 선택 시 선택/미선택 디자이너 알림)CommissionPricePolicy로 캡슐화 (기본금 / 인센티브 / 미달 환불 / 정산 금액)Payment환불 요청 도메인 메서드 추가 (markFullRefundRequested,markPartialRefundRequested)gainDraftSelectedReward,gainCommissionCompletedReward)ExpReward.REVISION->COMMISSION_COMPLETED이름 변경 (외주 완료 보상 의미 명확화)📸 Test Evidence
💬 Reviewer Notes
Payment에서 구현을 간단히 하기 위해 부분 환불시amount를 차감하는 식으로 구현했습니다.Summary by CodeRabbit
Summary by CodeRabbit