Skip to content

[feat] 급여 지급일 전날 근무 기록 확인 알림 도입#125

Merged
geunyong16 merged 3 commits into
mainfrom
feature/90-payment-day-reminder-notification
Feb 22, 2026
Merged

[feat] 급여 지급일 전날 근무 기록 확인 알림 도입#125
geunyong16 merged 3 commits into
mainfrom
feature/90-payment-day-reminder-notification

Conversation

@geunyong16
Copy link
Copy Markdown
Collaborator

@geunyong16 geunyong16 commented Feb 22, 2026

관련 이슈

Closes #90

작업 내용

급여 지급일 하루 전날 오전 9시에 근로자에게 해당 월의 근무 기록 확인 알림을 발송하는 기능을 구현합니다.

변경 사항

수정된 파일

  • NotificationType: WORK_RECORD_CONFIRMATION 신규 타입 추가
  • UserSettingsService: 신규 타입을 paymentAlertEnabled 설정 그룹으로 매핑
  • WorkerContractRepository: 내일이 급여 지급일인 활성 계약 조회 쿼리 2개 추가
    • findActiveContractsByExactPaymentDay — 일반 날짜용 (paymentDay == 내일)
    • findActiveContractsByPaymentDayOnLastDay — 월말용 (paymentDay >= 내일, 예: paymentDay=31인 계약이 2월 28일에도 포함)

신규 파일

  • PaymentDayReminderService: 핵심 비즈니스 로직
    • 급여 기간 계산 (SalaryService와 동일한 adjustDayOfMonth 로직)
    • COMPLETED 상태 근무 기록 집계 (근무 일수, 총 근무 시간)
    • 개별 계약별 예외 격리 (한 계약 실패가 다른 계약에 영향 없음)
    • 알림 이벤트 발행 (eventPublisher.publishEvent)
  • PaymentDayReminderScheduler: 매일 오전 9시 스케줄러 (@Scheduled(cron = "0 0 9 * * *"))
  • PaymentDayReminderServiceTest: 단위 테스트 7건

알림 명세

항목 내용
타입 WORK_RECORD_CONFIRMATION
설정 paymentAlertEnabled
액션 VIEW_SALARY (급여 상세 페이지 이동)
actionData {"contractId": 123, "year": 2025, "month": 3}
제목 예시 내일은 OO 카페 급여일이에요. 2025년 3월 근무 기록을 확인해 보세요! (근무 15일, 총 120.0시간)

월말 처리 검증

paymentDay 내일 포함 여부
21 3/21
31 2/28 (월말)
31 3/28 ❌ (정상 제외)
28 2/28 (월말)

Test plan

  • paymentDay가 내일 날짜와 일치하는 활성 계약의 근로자에게 알림이 발송되는지 확인
  • paymentDay=31인 계약이 2월 말일 전날에 올바른 급여 기간(1/31~2/27)으로 알림을 발송하는지 확인
  • 월말(2/28)에 findActiveContractsByPaymentDayOnLastDay 쿼리가 사용되는지 확인
  • 근무 기록이 0건인 경우에도 알림이 발송되는지 확인 (0일, 0.0시간으로 표시)
  • 대상 계약 없으면 이벤트 미발행 확인
  • 특정 계약에서 예외 발생 시 나머지 계약의 알림은 정상 발송되는지 확인
  • actionDatacontractId, year, month가 포함되는지 확인
  • ./gradlew test --tests "PaymentDayReminderServiceTest" 7건 전체 통과

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 급여 지급일 전날 자동으로 근무 기록 확인 알림을 전송하는 시스템 추가
    • 매일 오전 9시에 실행되는 자동 알림 스케줄러 구현
    • 월말 급여 지급일(예: 31일) 처리 최적화
  • 테스트

    • 급여 알림 서비스에 대한 포괄적인 단위 테스트 추가

- NotificationType에 WORK_RECORD_CONFIRMATION 추가
- UserSettingsService switch에 신규 타입을 paymentAlertEnabled로 매핑
- WorkerContractRepository에 내일이 급여 지급일인 활성 계약 조회 쿼리 추가
  (일반 날짜용 / 월말 처리용으로 분리)
- PaymentDayReminderService: 급여 기간 계산, 근무 기록 요약 집계, 알림 이벤트 발행
- PaymentDayReminderScheduler: 매일 오전 9시 실행 스케줄러
- 일반 날짜에 paymentDay 일치 계약 알림 발송 검증
- 월말(2월 28일)에 findActiveContractsByPaymentDayOnLastDay 쿼리 사용 검증
- paymentDay=31인 계약의 2월 급여 기간 계산 정확성 검증
- 근무 기록 0건에도 알림 발송되는지 검증
- 대상 계약 없으면 이벤트 미발행 검증
- 특정 계약 예외 발생 시 다른 계약 알림 정상 발송 검증
- 알림 actionData에 contractId/year/month 포함 검증
- sendPaymentDayRemindersForDate(LocalDate) 패키지 프라이빗 메서드 추가로 날짜 주입 가능
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 22, 2026

Warning

Rate limit exceeded

@geunyong16 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 4 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

개요

급여 지급일 하루 전에 근로자에게 근무 기록 확인 알림을 발송하는 기능을 도입합니다. 스케줄러가 매일 오전 9시에 실행되어 활성 계약의 결제 일정을 확인하고, 해당 월의 근무 기록을 조회한 후 알림 이벤트를 발행합니다.

변경 사항

Cohort / File(s) 설명
알림 타입 및 설정
src/main/java/com/example/paycheck/domain/notification/enums/NotificationType.java, src/main/java/com/example/paycheck/domain/settings/service/UserSettingsService.java
새로운 알림 타입 WORK_RECORD_CONFIRMATION을 정의하고, 이를 결제 관련 알림 설정에 추가합니다.
저장소 계층
src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java
내일이 정확한 결제일인 계약과 월말 케이스(결제일 31일)를 조회하는 두 개의 쿼리 메서드를 추가합니다. 각각 JOIN FETCH를 통해 N+1 쿼리 문제를 방지합니다.
서비스 계층
src/main/java/com/example/paycheck/domain/salary/service/PaymentDayReminderService.java
활성 계약을 조회하고, 해당 월의 근무 기록을 집계하여 총 근무시간을 계산한 후, WORK_RECORD_CONFIRMATION 타입의 알림 이벤트를 발행합니다. 월말 날짜 처리 및 개별 계약 에러 처리를 포함합니다.
스케줄러
src/main/java/com/example/paycheck/domain/salary/scheduler/PaymentDayReminderScheduler.java
@scheduled 애너테이션으로 매일 오전 9시에 실행되는 Spring 컴포넌트입니다. PaymentDayReminderService를 호출하고 로깅 및 에러 처리를 수행합니다.
테스트
src/test/java/com/example/paycheck/domain/salary/service/PaymentDayReminderServiceTest.java
일반적인 날짜 흐름, 월말 처리, 2월 31일 엣지 케이스, 근무 기록 부재, 계약 부재, 부분 실패 처리 등을 검증하는 포괄적인 단위 테스트입니다.

시퀀스 다이어그램

sequenceDiagram
    participant Scheduler as PaymentDayReminderScheduler
    participant Service as PaymentDayReminderService
    participant ContractRepo as WorkerContractRepository
    participant WorkRecordRepo as WorkRecordRepository
    participant EventPublisher as ApplicationEventPublisher
    participant NotificationSystem as 알림 시스템

    Scheduler->>Service: sendPaymentDayReminders()
    activate Service
    Service->>ContractRepo: findActiveContractsByExactPaymentDay(내일)
    ContractRepo-->>Service: 계약 목록
    
    loop 각 계약별
        Service->>WorkRecordRepo: 해당 월 근무 기록 조회
        WorkRecordRepo-->>Service: 근무 기록 목록
        Service->>Service: 총 근무시간 계산
        Service->>EventPublisher: NotificationEvent 발행<br/>(WORK_RECORD_CONFIRMATION, VIEW_SALARY)
        EventPublisher->>NotificationSystem: 알림 이벤트 처리
    end
    
    deactivate Service
    Scheduler->>Scheduler: 로깅
Loading

예상 코드 리뷰 노력

🎯 3 (Moderate) | ⏱️ ~25 분

관련 PR

  • [feat] 알림 이벤트 추가 #94: 이벤트 기반 알림 발행 기능을 추가하며(ApplicationEventPublisher 및 NotificationType 확장), 유사한 아키텍처 패턴을 사용합니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 주요 변경사항인 '급여 지급일 전날 근무 기록 확인 알림 기능 도입'을 명확하게 요약하고 있으며, 형식도 템플릿을 준수합니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(관련 이슈, 작업 내용, 변경 사항, 알림 명세, 월말 처리 검증, 테스트 플랜)을 포함하고 있으며 충분히 상세합니다.
Linked Issues check ✅ Passed PR의 모든 구현(스케줄러, 서비스, 저장소 쿼리, 알림 타입, 테스트)이 이슈 #90의 요구사항을 충족하고 있습니다.
Out of Scope Changes check ✅ Passed 모든 코드 변경이 이슈 #90의 급여 지급일 알림 기능 도입이라는 범위 내에 있으며, 관련 없는 변경이 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/90-payment-day-reminder-notification

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.

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (4)
src/main/java/com/example/paycheck/domain/salary/service/PaymentDayReminderService.java (3)

131-142: buildActionData 실패 시 nullactionData로 설정됩니다.

ObjectMapper 직렬화 실패 시 null을 반환하여 알림이 actionData 없이 발행됩니다. 알림 소비자 측에서 actionDatanull인 경우를 처리할 수 있는지 확인이 필요합니다. ObjectMapper 직렬화가 HashMap에 대해 실패하는 경우는 거의 없지만, 방어적으로 빈 JSON 객체("{}")를 fallback으로 반환하는 것을 고려해 보세요.

💡 fallback 제안
         } catch (JsonProcessingException e) {
             log.warn("알림 액션 데이터 생성 실패: contractId={}", contractId, e);
-            return null;
+            return "{}";
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/example/paycheck/domain/salary/service/PaymentDayReminderService.java`
around lines 131 - 142, The buildActionData method currently returns null on
JsonProcessingException which causes actionData to be missing; change the catch
block in buildActionData to return a defensive empty JSON string ("{}") instead
of null, while keeping the warning log (log.warn("알림 액션 데이터 생성 실패:
contractId={}", contractId, e)); ensure the method signature and callers that
expect a String continue to work with "{}" as a safe fallback.

43-46: 읽기 전용 작업에 불필요한 writable @Transactional 사용.

이 서비스는 DB 쓰기 작업 없이 조회와 이벤트 발행만 수행합니다. 클래스 레벨의 @Transactional(readOnly = true)로 충분하므로, 메서드 레벨의 @Transactional@Transactional(readOnly = true)로 변경하거나 제거하는 것을 고려하세요. 읽기 전용 트랜잭션은 DB 최적화(예: MySQL의 경우 snapshot 읽기)를 활용할 수 있습니다.

Also applies to: 52-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/example/paycheck/domain/salary/service/PaymentDayReminderService.java`
around lines 43 - 46, The methods in PaymentDayReminderService (notably
sendPaymentDayReminders and the other method at lines 52-53, e.g.,
sendPaymentDayRemindersForDate) are marked with a writable `@Transactional` but
only perform reads and event publication; change the transactional annotations
to read-only by replacing method-level `@Transactional` with
`@Transactional`(readOnly = true) or remove the method-level annotation and put
`@Transactional`(readOnly = true) at the class level (on
PaymentDayReminderService) so reads use a read-only transaction; update both
sendPaymentDayReminders and the other method mentioned to use readOnly
transactions consistently.

144-151: adjustDayOfMonth 메서드가 SalaryService에서 중복되고 있습니다.

주석에도 명시된 대로 동일한 로직이 두 곳에 존재합니다. 급여 기간 계산 로직이 변경될 경우 양쪽을 동시에 수정해야 하므로, 공통 유틸리티 클래스로 추출하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/example/paycheck/domain/salary/service/PaymentDayReminderService.java`
around lines 144 - 151, The adjustDayOfMonth method in PaymentDayReminderService
duplicates SalaryService.adjustDayOfMonth; extract this logic into a shared
utility (e.g., DateUtils.adjustDayOfMonth or PayDateUtils.adjustDayOfMonth) that
takes (LocalDate baseDate, int desiredDay), move the existing implementation
there, and update both PaymentDayReminderService.adjustDayOfMonth and
SalaryService to call the new shared method; ensure the new utility is placed in
a common package and add unit tests or reuse existing tests to verify behavior
remains identical.
src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java (1)

55-60: 월말 쿼리의 >= 조건은 정확하지만, 인덱스 추가를 고려하세요.

paymentDay >= :tomorrowDay 조건은 월말 처리(예: paymentDay=31인 계약이 2월 28일에 알림 대상이 되는 케이스)를 올바르게 처리합니다.

이 두 쿼리 모두 isActivepaymentDay 컬럼을 조건으로 사용하므로, 계약 수가 증가하면 (is_active, payment_day) 복합 인덱스 추가를 검토하시기 바랍니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java`
around lines 55 - 60, Add a composite index on the isActive and paymentDay
columns to optimize queries like
WorkerContractRepository.findActiveContractsByPaymentDayOnLastDay that filter by
isActive and paymentDay; update the WorkerContract entity (or DB migration) to
create an index covering (is_active, payment_day) so the JPA query using WHERE
c.isActive and c.paymentDay >= :tomorrowDay can use the index for faster
lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/test/java/com/example/paycheck/domain/salary/service/PaymentDayReminderServiceTest.java`:
- Around line 231-234: The test stubs an exception on
failingContract.getWorker() but sendReminderForContract invokes
getWorkplace().getName() first, so update the mock to throw from the actual call
site: change the mock setup for failingContract to throw (or stub) on
getWorkplace() / getWorkplace().getName() rather than getWorker() so
sendReminderForContract will hit the intended RuntimeException; reference
failingContract, sendReminderForContract, getWorkplace, getName and getWorker
when locating and updating the stub.

---

Nitpick comments:
In
`@src/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.java`:
- Around line 55-60: Add a composite index on the isActive and paymentDay
columns to optimize queries like
WorkerContractRepository.findActiveContractsByPaymentDayOnLastDay that filter by
isActive and paymentDay; update the WorkerContract entity (or DB migration) to
create an index covering (is_active, payment_day) so the JPA query using WHERE
c.isActive and c.paymentDay >= :tomorrowDay can use the index for faster
lookups.

In
`@src/main/java/com/example/paycheck/domain/salary/service/PaymentDayReminderService.java`:
- Around line 131-142: The buildActionData method currently returns null on
JsonProcessingException which causes actionData to be missing; change the catch
block in buildActionData to return a defensive empty JSON string ("{}") instead
of null, while keeping the warning log (log.warn("알림 액션 데이터 생성 실패:
contractId={}", contractId, e)); ensure the method signature and callers that
expect a String continue to work with "{}" as a safe fallback.
- Around line 43-46: The methods in PaymentDayReminderService (notably
sendPaymentDayReminders and the other method at lines 52-53, e.g.,
sendPaymentDayRemindersForDate) are marked with a writable `@Transactional` but
only perform reads and event publication; change the transactional annotations
to read-only by replacing method-level `@Transactional` with
`@Transactional`(readOnly = true) or remove the method-level annotation and put
`@Transactional`(readOnly = true) at the class level (on
PaymentDayReminderService) so reads use a read-only transaction; update both
sendPaymentDayReminders and the other method mentioned to use readOnly
transactions consistently.
- Around line 144-151: The adjustDayOfMonth method in PaymentDayReminderService
duplicates SalaryService.adjustDayOfMonth; extract this logic into a shared
utility (e.g., DateUtils.adjustDayOfMonth or PayDateUtils.adjustDayOfMonth) that
takes (LocalDate baseDate, int desiredDay), move the existing implementation
there, and update both PaymentDayReminderService.adjustDayOfMonth and
SalaryService to call the new shared method; ensure the new utility is placed in
a common package and add unit tests or reuse existing tests to verify behavior
remains identical.

sendReminderForContract 실행 순서상 getWorkplace().getName()이
getWorker()보다 먼저 호출되므로, 예외 발생 지점을
getWorker() → getWorkplace()로 수정하여 실제 로직 흐름과 일치시킴
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] 급여 지급일 전날 근무 기록 확인 알림 도입

1 participant