Skip to content

[feat] 근로자 퇴사 시 예정된 근무 기록 삭제 처리#118

Merged
geunyong16 merged 3 commits into
mainfrom
feature/112-delete-future-work-records-on-termination
Feb 22, 2026
Merged

[feat] 근로자 퇴사 시 예정된 근무 기록 삭제 처리#118
geunyong16 merged 3 commits into
mainfrom
feature/112-delete-future-work-records-on-termination

Conversation

@geunyong16
Copy link
Copy Markdown
Collaborator

@geunyong16 geunyong16 commented Feb 19, 2026

🔖 관련 GitHub Issue

📝 변경 사항

주요 변경 내용

  • WorkRecordCommandServicedeleteFutureWorkRecords 메서드 추가
  • ContractService.terminateContract에서 미래 근무 기록 삭제 호출
  • 삭제 시 관련 CorrectionRequest 함께 정리
  • buildActionData에서 null 반환 대신 RuntimeException throw로 변경 (리뷰 반영)
  • Clock 주입으로 테스트 용이성 개선
  • WorkRecordCalculationService에 일괄 계산 메서드 추가

상세 설명

근로 계약 해지(퇴사) 시, 해당 근로자의 미래 예정된 SCHEDULED 상태 근무 기록을 일괄 삭제 처리합니다.

  • ContractService.terminateContract() 실행 시 WorkRecordCommandService.deleteFutureWorkRecords()를 호출하여 미래 근무 기록을 soft-delete합니다.
  • 삭제 대상 근무 기록에 연결된 CorrectionRequest도 함께 정리하여 데이터 정합성을 유지합니다.
  • LocalDate.now() 대신 Clock을 주입받아 테스트에서 시간을 제어할 수 있도록 개선했습니다.
  • 코드 리뷰 반영: buildActionData에서 JSON 직렬화 실패 시 null 반환 대신 RuntimeException을 throw하여 데이터 무결성을 보장합니다.

✅ 체크리스트

  • PR 제목이 형식에 맞는가? (유형: 작업 요약)
  • 코드가 정상적으로 빌드되는가?
  • 새로운 기능에 대한 테스트 코드를 작성했는가?
  • 코드 스타일 가이드를 따랐는가?

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 계약 종료 시 미래 근무 기록이 자동으로 삭제됩니다.
    • 배치 처리 방식으로 근무 기록 계산 성능이 개선되었습니다.
  • Bug Fixes

    • 액션 데이터 생성 실패 시 오류가 기록되고 안정적으로 처리됩니다.
  • Chores

    • 시스템 로깅과 시간 소스 구성이 추가되어 운영·디버깅이 쉬워졌습니다.
  • Tests

    • 고정된 시간 기반 테스트가 추가되어 미래 기록 삭제 동작을 검증합니다.

…tion

- WorkRecordCommandService에 deleteFutureWorkRecords 메서드 추가
- ContractService.terminateContract에서 미래 근무 기록 삭제 호출
- 삭제 시 관련 CorrectionRequest 함께 정리
- buildActionData에서 null 반환 대신 RuntimeException throw로 변경
- Clock 주입으로 테스트 용이성 개선
- WorkRecordCalculationService에 일괄 계산 메서드 추가
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 19, 2026

Walkthrough

계약 종료 시 미래 예정된 SCHEDULED 근무 기록 및 관련 CorrectionRequest를 정리하도록 삭제 호출을 추가했고, WorkRecord 계산에 배치 처리 메서드를 도입했으며, Clock 빈 주입과 SLF4J 로깅·오류 로그 추가로 시간 결정론화 및 로깅을 강화했습니다.

Changes

Cohort / File(s) Summary
계약 서비스
src/main/java/com/example/paycheck/domain/contract/service/ContractService.java
클래스에 @Slf4j 추가 및 계약 종료 흐름에서 workRecordCommandService.deleteFutureWorkRecords(contractId) 호출; action 데이터 생성 실패 시 오류 로그 추가.
WorkRecord 커맨드 처리(삭제·Clock·로깅)
src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java
@Slf4j 추가, Clock 주입으로 LocalDate.now(clock) 사용(시간 결정론화), deleteFutureWorkRecords(Long) 공개 메서드 추가(미래 SCHEDULED 근무 및 관련 CorrectionRequest 정리), 배치 계산 호출로 전환, action 데이터 빌드 실패 시 에러 로깅 및 기본 "{}" 반환 처리.
WorkRecord 계산 — 배치 처리 추가
src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCalculationService.java
여러 WorkRecord 대상의 연도별 공휴일을 일괄 조회해 N+1 쿼리 완화하고 레코드별 계산을 수행하는 calculateWorkRecordDetailsBatch(List<WorkRecord>) 메서드 추가.
시간 인프라 설정
src/main/java/com/example/paycheck/global/config/ClockConfig.java
Spring Clock 빈(Clock.systemDefaultZone())을 제공하는 설정 클래스 추가.
테스트 보완(고정 Clock)
src/test/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandServiceTest.java
고정 Clock(Clock.fixed) 스파이 추가 및 deleteFutureWorkRecords 동작과 삭제 순서 보장을 검증하는 신규 테스트 2건 추가.

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant ContractService as ContractService
    participant WorkRecordCommandService as WorkRecordCommandService
    participant WorkRecordDB as WorkRecordDB
    participant CorrectionDB as CorrectionRequestDB

    rect rgba(200,200,255,0.5)
    User->>ContractService: terminateContract(contractId)
    end

    rect rgba(200,255,200,0.5)
    ContractService->>WorkRecordCommandService: deleteFutureWorkRecords(contractId)
    end

    rect rgba(255,230,200,0.5)
    WorkRecordCommandService->>WorkRecordDB: find SCHEDULED records after (today-1)
    WorkRecordDB-->>WorkRecordCommandService: future WorkRecords
    WorkRecordCommandService->>CorrectionDB: delete CorrectionRequest by workRecord ids / contractId
    CorrectionDB-->>WorkRecordCommandService: deletion confirmation
    WorkRecordCommandService->>WorkRecordDB: mark WorkRecords as DELETED (soft-delete)
    WorkRecordDB-->>WorkRecordCommandService: update confirmation
    end

    ContractService-->>User: termination complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While primary changes align with issue #112, several secondary changes (batch calculation method, Clock injection, logging additions) are introduced without explicit mention in linked issues. Clarify whether batch calculation, Clock injection, and logging enhancements are supporting changes for testability or represent separate scope that should be split into different PRs.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the correct format '[feat]' and clearly summarizes the main feature: deleting scheduled work records when a worker's contract is terminated.
Description check ✅ Passed The PR description covers all required template sections with linked issues (#112), detailed change summary, implementation details, and completed checklist items.
Linked Issues check ✅ Passed All coding requirements from issue #112 are met: deleteFutureWorkRecords method added to WorkRecordCommandService, contract termination integration implemented, and CorrectionRequest cleanup included.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/112-delete-future-work-records-on-termination

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: 4

🧹 Nitpick comments (3)
src/main/java/com/example/paycheck/global/config/ClockConfig.java (1)

12-13: 타임존을 명시적으로 설정하는 것을 권장합니다.

Clock.systemDefaultZone()은 JVM 기본 타임존을 사용합니다. Docker/클라우드 환경에서는 기본 타임존이 UTC가 되는 경우가 많아 날짜 경계(자정 전후)가 LocalDate.now(clock) 결과에 영향을 줄 수 있습니다. 한국 서비스이므로 Asia/Seoul을 명시하는 것이 안전합니다.

♻️ 명시적 타임존 설정 제안
+import java.time.ZoneId;

 `@Bean`
 public Clock clock() {
-    return Clock.systemDefaultZone();
+    return Clock.system(ZoneId.of("Asia/Seoul"));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/example/paycheck/global/config/ClockConfig.java` around
lines 12 - 13, The ClockConfig.clock() currently returns
Clock.systemDefaultZone(), which relies on the JVM default TZ; change it to
return a Clock explicitly using the Seoul zone (e.g.,
Clock.system(ZoneId.of("Asia/Seoul"))) so all consumers (like
LocalDate.now(clock)) get a consistent Korea timezone regardless of
container/JVM defaults; update imports/usages to include ZoneId and ensure the
bean method name ClockConfig.clock() is returning the new zone-specific Clock.
src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java (1)

432-441: buildActionData에서 매 호출마다 new ObjectMapper()를 생성하고 있습니다.

ObjectMapper는 스레드 세이프하며 무거운 객체이므로 싱글턴으로 재사용해야 합니다. ContractServiceObjectMapper를 DI로 주입받아 사용합니다. WorkRecordCommandService도 동일하게 주입받아 사용해야 합니다.

♻️ ObjectMapper 주입 방식으로 수정
+import com.fasterxml.jackson.databind.ObjectMapper;

 // 필드 선언부에 추가
+private final ObjectMapper objectMapper;

 private String buildActionData(Long workRecordId) {
     try {
-        ObjectMapper mapper = new ObjectMapper();
         Map<String, Object> data = new HashMap<>();
         data.put("workRecordId", workRecordId);
-        return mapper.writeValueAsString(data);
+        return objectMapper.writeValueAsString(data);
     } catch (Exception e) {
         throw new RuntimeException("알림 액션 데이터 생성 실패: workRecordId=" + workRecordId, e);
     }
 }
🤖 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/workrecord/service/WorkRecordCommandService.java`
around lines 432 - 441, buildActionData currently creates a new ObjectMapper on
every call which is heavy; change WorkRecordCommandService to accept and reuse a
singleton ObjectMapper (via constructor or field injection like ContractService
does) and replace the local "new ObjectMapper()" usage in buildActionData with
the injected ObjectMapper instance so the service reuses the thread-safe mapper
across calls and avoid per-call allocations.
src/main/java/com/example/paycheck/domain/contract/service/ContractService.java (1)

203-213: buildActionData 오류 처리 방식이 WorkRecordCommandService와 일치하지 않습니다.

ContractService.buildActionDataJsonProcessingException 발생 시 null을 반환(알림이 actionData=null로 발행됨)하지만, WorkRecordCommandService.buildActionDataRuntimeException을 throw(트랜잭션 롤백 가능)합니다. 두 방식 중 하나로 통일하거나, 각각의 선택 이유를 주석으로 명시하는 것이 좋습니다.

🤖 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/service/ContractService.java`
around lines 203 - 213, The error handling in ContractService.buildActionData is
inconsistent with WorkRecordCommandService.buildActionData; instead of returning
null on JsonProcessingException, change ContractService.buildActionData to
rethrow a RuntimeException (including a clear message and the caught exception
as the cause) so callers get consistent failure behavior and transactions can
roll back, or alternatively add a brief comment in buildActionData documenting
why this method returns null if you choose to keep it—refer to buildActionData
in ContractService and WorkRecordCommandService.buildActionData to apply the
same approach.
🤖 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/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 391-399: Capture LocalDate once to avoid midnight-race
inconsistencies and make the deletion semantics consistent with
regenerateFutureWorkRecords: in deleteFutureWorkRecords(), assign LocalDate
today = LocalDate.now(clock) once and use that variable for both repository
calls
(correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus and
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus); remove the
minusDays(1) so the "after" comparisons are today-exclusive, and update the
comment to reflect "오늘 이후 (오늘 제외)".
- Around line 386-399: The current deleteFutureWorkRecords method physically
deletes rows; change it to perform a soft delete by updating WorkRecord.status
to WorkRecordStatus.DELETED instead of deleting rows: add/implement
workRecordRepository.softDeleteByContractIdAndWorkDateAfterAndStatus(...) (an
`@Modifying` UPDATE JPQL that sets w.status = :deletedStatus where contract.id =
:contractId AND workDate > :date AND status = :scheduledStatus) and call it from
deleteFutureWorkRecords; keep
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(...)
as-is for removing associated requests (or adjust its query to target WorkRecord
IDs if needed). Also extract LocalDate today = LocalDate.now(clock) once and use
that variable in both repository calls.

In
`@src/test/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandServiceTest.java`:
- Around line 67-68: The test suite is missing unit tests for
deleteFutureWorkRecords(Long) and the `@Spy` Clock isn't used to fix "today"; add
a test (e.g., deleteFutureWorkRecords_Success) that stubs clock.instant() and
clock.getZone() with doReturn(...) so "today" is deterministic, call
workRecordCommandService.deleteFutureWorkRecords(contractId), and verify
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(...)
and workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(...) are
invoked with the expected contractId, date boundary (today.minusDays(1)) and
WorkRecordStatus.SCHEDULED to assert correct parameters and date range handling.
- Line 27: Remove the unused import of ZoneId from the test class; in
WorkRecordCommandServiceTest delete the line importing java.time.ZoneId since
ZoneId is not referenced anywhere in that class.

---

Nitpick comments:
In
`@src/main/java/com/example/paycheck/domain/contract/service/ContractService.java`:
- Around line 203-213: The error handling in ContractService.buildActionData is
inconsistent with WorkRecordCommandService.buildActionData; instead of returning
null on JsonProcessingException, change ContractService.buildActionData to
rethrow a RuntimeException (including a clear message and the caught exception
as the cause) so callers get consistent failure behavior and transactions can
roll back, or alternatively add a brief comment in buildActionData documenting
why this method returns null if you choose to keep it—refer to buildActionData
in ContractService and WorkRecordCommandService.buildActionData to apply the
same approach.

In
`@src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 432-441: buildActionData currently creates a new ObjectMapper on
every call which is heavy; change WorkRecordCommandService to accept and reuse a
singleton ObjectMapper (via constructor or field injection like ContractService
does) and replace the local "new ObjectMapper()" usage in buildActionData with
the injected ObjectMapper instance so the service reuses the thread-safe mapper
across calls and avoid per-call allocations.

In `@src/main/java/com/example/paycheck/global/config/ClockConfig.java`:
- Around line 12-13: The ClockConfig.clock() currently returns
Clock.systemDefaultZone(), which relies on the JVM default TZ; change it to
return a Clock explicitly using the Seoul zone (e.g.,
Clock.system(ZoneId.of("Asia/Seoul"))) so all consumers (like
LocalDate.now(clock)) get a consistent Korea timezone regardless of
container/JVM defaults; update imports/usages to include ZoneId and ensure the
bean method name ClockConfig.clock() is returning the new zone-specific Clock.

Comment on lines +391 to +399
public void deleteFutureWorkRecords(Long contractId) {
// 삭제할 WorkRecord를 참조하는 CorrectionRequest 삭제
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(
contractId, LocalDate.now(clock).minusDays(1), WorkRecordStatus.SCHEDULED);

// 오늘 이후의 SCHEDULED 상태 WorkRecord 삭제
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(
contractId, LocalDate.now(clock).minusDays(1), WorkRecordStatus.SCHEDULED);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

LocalDate.now(clock)을 두 번 호출하여 자정 경계에서 불일치가 발생할 수 있으며, 주석과 구현의 날짜 범위가 다릅니다.

LocalDate.now(clock)이 lines 394, 398에서 각각 호출되고 있어 자정 바로 직전에 메서드가 시작되면 두 호출이 서로 다른 날짜를 반환할 수 있습니다. 로컬 변수로 한 번만 캡처해야 합니다.

또한, 주석은 "오늘 이후" (today exclusive)라고 되어 있지만, minusDays(1)을 사용하므로 실제로는 오늘 포함 이후(today inclusive)로 삭제됩니다. 반면 regenerateFutureWorkRecordsLocalDate.now(clock) (today exclusive)를 사용해 날짜 기준이 다릅니다. 의도적 차이라면 주석에 명확히 표기해야 합니다.

🛠️ 수정 제안
 public void deleteFutureWorkRecords(Long contractId) {
+    LocalDate today = LocalDate.now(clock);
+
     // 삭제할 WorkRecord를 참조하는 CorrectionRequest 삭제
     correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(
-            contractId, LocalDate.now(clock).minusDays(1), WorkRecordStatus.SCHEDULED);
+            contractId, today.minusDays(1), WorkRecordStatus.SCHEDULED);

-    // 오늘 이후의 SCHEDULED 상태 WorkRecord 삭제
+    // 오늘 포함 이후(today inclusive)의 SCHEDULED 상태 WorkRecord 삭제
     workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(
-            contractId, LocalDate.now(clock).minusDays(1), WorkRecordStatus.SCHEDULED);
+            contractId, today.minusDays(1), WorkRecordStatus.SCHEDULED);
 }
🤖 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/workrecord/service/WorkRecordCommandService.java`
around lines 391 - 399, Capture LocalDate once to avoid midnight-race
inconsistencies and make the deletion semantics consistent with
regenerateFutureWorkRecords: in deleteFutureWorkRecords(), assign LocalDate
today = LocalDate.now(clock) once and use that variable for both repository
calls
(correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus and
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus); remove the
minusDays(1) so the "after" comparisons are today-exclusive, and update the
comment to reflect "오늘 이후 (오늘 제외)".

- LocalDate.now(clock) 중복 호출 제거 → 로컬 변수 today로 캡처
- 주석 '오늘 이후' → '오늘 포함 이후'로 수정 (today inclusive 명확화)
- @SPY Clock을 고정 날짜(2026-02-21)로 변경하여 날짜 경계 테스트 안정화
- deleteFutureWorkRecords 단위 테스트 추가 (정상 삭제, 삭제 순서 보장)
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java (1)

343-353: ⚠️ Potential issue | 🟠 Major

배치와 단건 메서드의 공휴일 판정 기준 불일치 → 시급 계산 편차 가능성

calculateWorkRecordDetailsBatchgetHolidayDates(year) (전체 공휴일 테이블 조회)를 사용하고, calculateWorkRecordDetailsisPublicHoliday() (isPublicHoliday=true인 항목만 필터)를 사용합니다. 그 결과 기념일(예: 식목일, isPublicHoliday=false)처럼 isPublicHoliday 플래그가 거짓인 항목에 대해 배치는 휴일 수당을 적용하고 단건은 적용하지 않을 수 있습니다.

두 메서드가 동일한 로직으로 공휴일을 판정하도록 통일하세요. 예를 들어 배치도 isPublicHoliday() 메서드를 사용하거나, getHolidayDates() 조회 시 isPublicHoliday=true 필터를 추가하세요.

🤖 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/workrecord/service/WorkRecordCommandService.java`
around lines 343 - 353, The batch vs single calculation use different holiday
criteria causing pay discrepancies: update calculateWorkRecordDetailsBatch
and/or the holiday retrieval so both use the same public-holiday predicate as
calculateWorkRecordDetails; specifically either make
calculateWorkRecordDetailsBatch call the same isPublicHoliday() filtering logic
(reuse the same helper) when deciding holiday pay, or change
getHolidayDates(year) usage in the batch path to return only entries with
isPublicHoliday=true so its results match calculateWorkRecordDetails; ensure
both methods reference the same helper/function to determine holiday status
(isPublicHoliday) to guarantee identical holiday-pay decisions.
🧹 Nitpick comments (1)
src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java (1)

413-421: regenerateFutureWorkRecords에서 LocalDate.now(clock) 세 번 중복 호출

Lines 414, 418, 421에서 각각 LocalDate.now(clock)을 호출하고 있어 자정 경계 직전 실행 시 일관성 없는 날짜가 사용될 수 있습니다. deleteFutureWorkRecords에서 today 로컬 변수로 한 번만 캡처한 패턴과 통일성이 없습니다.

♻️ 제안 수정
 public void regenerateFutureWorkRecords(Long contractId) {
     WorkerContract contract = workerContractRepository.findById(contractId)
             .orElseThrow(() -> new NotFoundException(ErrorCode.CONTRACT_NOT_FOUND, "계약을 찾을 수 없습니다."));

+    LocalDate today = LocalDate.now(clock);
+
     // 삭제할 WorkRecord를 참조하는 CorrectionRequest 삭제
     correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(
-            contractId, LocalDate.now(clock), WorkRecordStatus.SCHEDULED);
+            contractId, today, WorkRecordStatus.SCHEDULED);

     // 오늘 이후의 SCHEDULED 상태 WorkRecord 삭제
     workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(
-            contractId, LocalDate.now(clock), WorkRecordStatus.SCHEDULED);
+            contractId, today, WorkRecordStatus.SCHEDULED);

     // 새로운 WorkRecord 생성 (오늘+1 ~ 2개월 뒤)
-    LocalDate startDate = LocalDate.now(clock).plusDays(1);
+    LocalDate startDate = today.plusDays(1);
     LocalDate endDate = startDate.plusMonths(2);
     workRecordGenerationService.generateWorkRecordsForPeriod(contract, startDate, endDate);
 }
🤖 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/workrecord/service/WorkRecordCommandService.java`
around lines 413 - 421, The method calls LocalDate.now(clock) three times
causing potential inconsistent dates; capture LocalDate.now(clock) once into a
local variable (e.g., today) at the start of regenerateFutureWorkRecords and
reuse that variable in the three locations:
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(contractId,
today, ...),
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(contractId,
today, ...), and compute startDate as today.plusDays(1) so all operations use
the same consistent date snapshot.
🤖 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/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 441-442: buildActionData currently throws a RuntimeException which
will bubble up and cause the surrounding `@Transactional` methods
(createWorkRecordByEmployer, updateWorkRecord in WorkRecordCommandService) to
rollback; change buildActionData to catch serialization/processing exceptions,
log the error with context (include workRecordId and exception), and return null
(or an Optional) instead of rethrowing so the business transaction isn't rolled
back, or alternatively extract the notification creation/publishing into a
separate method annotated with `@Transactional`(propagation = REQUIRES_NEW) (e.g.,
publishNotification or createNotificationInNewTx) and call that from the main
flow so notification failures don't affect the main work record transaction.

---

Outside diff comments:
In
`@src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 343-353: The batch vs single calculation use different holiday
criteria causing pay discrepancies: update calculateWorkRecordDetailsBatch
and/or the holiday retrieval so both use the same public-holiday predicate as
calculateWorkRecordDetails; specifically either make
calculateWorkRecordDetailsBatch call the same isPublicHoliday() filtering logic
(reuse the same helper) when deciding holiday pay, or change
getHolidayDates(year) usage in the batch path to return only entries with
isPublicHoliday=true so its results match calculateWorkRecordDetails; ensure
both methods reference the same helper/function to determine holiday status
(isPublicHoliday) to guarantee identical holiday-pay decisions.

---

Duplicate comments:
In
`@src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 386-401: deleteFutureWorkRecords currently performs a physical
delete via workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus
which violates the soft-delete policy; change the repository call to perform a
status update to WorkRecordStatus.DELETED (e.g., add a `@Modifying` JPQL method
like setStatusDeletedByContractIdAndWorkDateAfterAndStatus or
updateStatusByContractIdAndWorkDateAfterAndStatus and call that from
deleteFutureWorkRecords), keep or confirm
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus
behavior as needed, ensure the update is executed within the existing
transaction context, and update the method JavaDoc to reflect soft-delete
semantics (set status to DELETED) instead of “물리 삭제”.

---

Nitpick comments:
In
`@src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 413-421: The method calls LocalDate.now(clock) three times causing
potential inconsistent dates; capture LocalDate.now(clock) once into a local
variable (e.g., today) at the start of regenerateFutureWorkRecords and reuse
that variable in the three locations:
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(contractId,
today, ...),
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(contractId,
today, ...), and compute startDate as today.plusDays(1) so all operations use
the same consistent date snapshot.

…failure

RuntimeException throw 시 @transactional 롤백으로 근무 기록 저장까지
실패하는 문제 수정. 알림 데이터 생성 실패는 비즈니스 트랜잭션과 분리하여
예외 로깅 후 빈 JSON 객체 반환으로 처리.
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.

🧹 Nitpick comments (2)
src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java (2)

413-421: LocalDate.now(clock)을 세 번 호출하여 자정 경계에서 불일치가 발생할 수 있습니다.

deleteFutureWorkRecords에서 today 변수를 추출하여 해결한 것과 달리, regenerateFutureWorkRecords에서는 LocalDate.now(clock)을 lines 414, 418, 421에서 각각 호출하고 있습니다. 자정 직전에 메서드가 시작되면 세 호출이 서로 다른 날짜를 반환할 수 있어 삭제 범위와 생성 시작일이 어긋날 수 있습니다.

♻️ 수정 제안
 public void regenerateFutureWorkRecords(Long contractId) {
     WorkerContract contract = workerContractRepository.findById(contractId)
             .orElseThrow(() -> new NotFoundException(ErrorCode.CONTRACT_NOT_FOUND, "계약을 찾을 수 없습니다."));

+    LocalDate today = LocalDate.now(clock);
+
     // 삭제할 WorkRecord를 참조하는 CorrectionRequest 삭제
     correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus(
-            contractId, LocalDate.now(clock), WorkRecordStatus.SCHEDULED);
+            contractId, today, WorkRecordStatus.SCHEDULED);

     // 오늘 이후의 SCHEDULED 상태 WorkRecord 삭제
     workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus(
-            contractId, LocalDate.now(clock), WorkRecordStatus.SCHEDULED);
+            contractId, today, WorkRecordStatus.SCHEDULED);

     // 새로운 WorkRecord 생성 (오늘+1 ~ 2개월 뒤)
-    LocalDate startDate = LocalDate.now(clock).plusDays(1);
+    LocalDate startDate = today.plusDays(1);
     LocalDate endDate = startDate.plusMonths(2);
     workRecordGenerationService.generateWorkRecordsForPeriod(contract, startDate, endDate);
 }
🤖 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/workrecord/service/WorkRecordCommandService.java`
around lines 413 - 421, In regenerateFutureWorkRecords, avoid calling
LocalDate.now(clock) multiple times; capture a single LocalDate today =
LocalDate.now(clock) at the start of the method and use that variable for all
subsequent date calculations (replace the calls used in
deleteByWorkRecordContractAndDateAfterAndStatus,
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus, and when
computing startDate = today.plusDays(1)), ensuring contractId and any other
repository calls in this method reference the same today value so deletion and
new WorkRecord creation use a consistent boundary.

343-353: calculateWorkRecordDetailsBatch 호출에 빈 리스트 가드가 없어 saveAll 호출과 비대칭입니다.

completedRecords가 비어 있으면 calculateWorkRecordDetailsBatch(line 348)는 불필요하게 호출되는 반면, workRecordRepository.saveAll(line 351)은 빈 리스트 검사 후 호출됩니다. 두 호출 모두 동일한 조건 블록 안에 묶는 것이 일관성 면에서 낫습니다.

♻️ 수정 제안
-    calculationService.calculateWorkRecordDetailsBatch(completedRecords);
-
-    // 7. 계산된 WorkRecord 일괄 업데이트
-    if (!completedRecords.isEmpty()) {
+    // 6-7. COMPLETED 레코드 상세 계산 및 일괄 업데이트
+    if (!completedRecords.isEmpty()) {
+        calculationService.calculateWorkRecordDetailsBatch(completedRecords);
         workRecordRepository.saveAll(completedRecords);
     }
🤖 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/workrecord/service/WorkRecordCommandService.java`
around lines 343 - 353, completedRecords is checked before calling
workRecordRepository.saveAll but not before calling
calculationService.calculateWorkRecordDetailsBatch, causing an unnecessary call
when the list is empty; wrap the calculateWorkRecordDetailsBatch and
workRecordRepository.saveAll calls in the same non-empty guard (e.g., if
(!completedRecords.isEmpty()) {
calculationService.calculateWorkRecordDetailsBatch(completedRecords);
workRecordRepository.saveAll(completedRecords); }) so both operations execute
only when completedRecords has elements, referencing
calculateWorkRecordDetailsBatch and workRecordRepository.saveAll in
WorkRecordCommandService.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 386-401: The method deleteFutureWorkRecords currently calls
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus which
physically removes rows; change this to perform a soft delete by adding a
`@Modifying` update method softDeleteByContractIdAndWorkDateAfterAndStatus on
WorkRecordRepository (update w.status = :deletedStatus where w.contract.id =
:contractId and w.workDate > :date and w.status = :scheduledStatus) and call
that from deleteFutureWorkRecords with WorkRecordStatus.DELETED (and also update
the JavaDoc/comment that currently says "물리 삭제" to reflect soft delete); keep
the existing
correctionRequestRepository.deleteByWorkRecordContractAndDateAfterAndStatus call
as-is.

---

Nitpick comments:
In
`@src/main/java/com/example/paycheck/domain/workrecord/service/WorkRecordCommandService.java`:
- Around line 413-421: In regenerateFutureWorkRecords, avoid calling
LocalDate.now(clock) multiple times; capture a single LocalDate today =
LocalDate.now(clock) at the start of the method and use that variable for all
subsequent date calculations (replace the calls used in
deleteByWorkRecordContractAndDateAfterAndStatus,
workRecordRepository.deleteByContractIdAndWorkDateAfterAndStatus, and when
computing startDate = today.plusDays(1)), ensuring contractId and any other
repository calls in this method reference the same today value so deletion and
new WorkRecord creation use a consistent boundary.
- Around line 343-353: completedRecords is checked before calling
workRecordRepository.saveAll but not before calling
calculationService.calculateWorkRecordDetailsBatch, causing an unnecessary call
when the list is empty; wrap the calculateWorkRecordDetailsBatch and
workRecordRepository.saveAll calls in the same non-empty guard (e.g., if
(!completedRecords.isEmpty()) {
calculationService.calculateWorkRecordDetailsBatch(completedRecords);
workRecordRepository.saveAll(completedRecords); }) so both operations execute
only when completedRecords has elements, referencing
calculateWorkRecordDetailsBatch and workRecordRepository.saveAll in
WorkRecordCommandService.

@geunyong16 geunyong16 merged commit 84c9e6d into main Feb 22, 2026
1 check passed
@geunyong16 geunyong16 deleted the feature/112-delete-future-work-records-on-termination branch February 22, 2026 04:02
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