Skip to content

일정 알림 기능 개발#167

Open
kang20 wants to merge 4 commits intodevelopfrom
feature/162-schedule-reminder
Open

일정 알림 기능 개발#167
kang20 wants to merge 4 commits intodevelopfrom
feature/162-schedule-reminder

Conversation

@kang20
Copy link
Copy Markdown
Contributor

@kang20 kang20 commented Feb 23, 2026

✨ 작업 내용

1️⃣ 일정 알림 데이터 계층

  • ScheduleAlram enum에 minutes 필드 추가
  • ScheduleReminderData, ScheduleReminderJob 엔티티 및 Repository 구현

2️⃣ Quartz 기반 알림 스케줄러

  • ScheduleReminderQuartzJob + ScheduleReminderSchedulerService 구현
  • 일반 일정: 알람 시간만큼 이전 알림 / 종일 일정: 하루 전 10시 알림
  • 반복 일정 요일 필터링, 중복 방지 로직 포함

3️⃣ 이벤트 기반 UseCase 연동

  • ScheduleCreated/Updated/DeletedEvent + ScheduleReminderEventListener 구현
  • ScheduleModifyUseCase에서 일정 CRUD 시 이벤트 발행
  • ScheduleReminderBatchSchedulerUseCase 일일 배치 스케줄러 구현

4️⃣ 테스트 (35+ 케이스)

  • 스케줄러, 이벤트 리스너, 도메인 이벤트, 알림 이벤트, 알림 데이터 등 6개 테스트 클래스 작성

✅ 리뷰 요구사항(선택)

  • ScheduleReminderSchedulerService에서 날짜 범위 순회 방식의 성능 적절성
  • 종일 일정 알림 시간(하루 전 10:00)의 UX 적절성

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 일정 생성, 수정, 삭제 시 자동으로 알림을 예약하고 관리하는 기능 추가
    • 매일 정해진 시간에 예정된 일정의 알림을 자동으로 처리하는 배치 처리 추가
    • 하루 종일 일정은 전날 10:00에, 정시 일정은 지정된 시간에서 알림 시간을 뺀 시간에 알림 발송
  • 테스트

    • 일정 알림 기능에 대한 포괄적인 단위 및 통합 테스트 추가

- ScheduleAlram enum에 minutes 필드 추가 (TEN_MINUTE=10, THIRTY_MINUTE=30, ONE_DAY=1440)

- ScheduleReminderData 완성 (scheduleId, scheduleTitle, reminderType, isAllDay)

- ScheduleReminderJob 엔티티 생성 (중복 방지 유니크 제약 포함)

- ScheduleReminderJobRepository + QueryDSL Custom 구현
- ScheduleReminderNotificationEvent 생성 (종일/일반 메시지 분기)

- ScheduleReminderQuartzJob Quartz Job 구현

- ScheduleReminderSchedulerService Quartz 스케줄링 서비스 구현
- ScheduleCreated/Updated/DeletedEvent 도메인 이벤트 생성

- ScheduleReminderEventListener 이벤트 리스너 구현

- ScheduleModifyUseCase에 이벤트 발행 추가 (생성/수정/삭제)

- ScheduleReminderBatchSchedulerUseCase 일일 배치 스케줄러 구현

- ScheduleReadService에 활성 일정 조회 메서드 추가

- ScheduleRepositoryCustom에 findActiveSchedulesWithAlarmForDates 추가
- ScheduleAlramTest: enum 변환 단위 테스트

- ScheduleReminderSchedulerServiceTest: 스케줄링 로직 테스트 (8개)

- ScheduleReminderEventListenerTest: 이벤트 리스너 테스트 (6개)

- ScheduleEventTest: 도메인 이벤트 테스트 (9개)

- ScheduleReminderNotificationEventTest: 알림 이벤트 테스트 (6개)

- ScheduleReminderDataTest: 알림 데이터 테스트 (3개)

- LayeredArchitectureTest에 schedule event 패키지 제외 추가
@kang20 kang20 requested a review from Seol-JY February 23, 2026 10:07
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 23, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'auto_resolve_threads', 'spring_specific'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

스케줄 알림 기능을 구현하기 위해 도메인 이벤트, 알림 데이터 모델, Quartz 기반 스케줄러 서비스, JPA 엔티티, 리포지토리, 일괄 스케줄러 및 이벤트 리스너를 추가합니다.

Changes

코호트 / 파일(들) 요약
알림 도메인 모델
src/main/.../notification/domain/data/ScheduleReminderData.java, src/main/.../notification/domain/event/ScheduleReminderNotificationEvent.java
알림 데이터 구조와 알림 이벤트 클래스를 추가하여 스케줄 알림 정보를 표현하고, 플랫폼별(앱 내, 푸시) 메시지 생성 로직을 구현합니다.
스케줄 도메인 이벤트
src/main/.../schedule/domain/event/ScheduleCreatedEvent.java, src/main/.../schedule/domain/event/ScheduleUpdatedEvent.java, src/main/.../schedule/domain/event/ScheduleDeletedEvent.java
스케줄 생명주기 이벤트(생성, 수정, 삭제)를 정의하여 이벤트 기반 아키텍처를 지원합니다.
스케줄 이벤트 리스너 및 배치 스케줄러
src/main/.../schedule/domain/event/ScheduleReminderEventListener.java, src/main/.../schedule/domain/usecase/ScheduleReminderBatchSchedulerUseCase.java
스케줄 이벤트를 감지하여 알림을 스케줄링하고, 매일 배치 작업으로 예정된 알림을 생성합니다.
스케줄 알림 스케줄러 서비스
src/main/.../schedule/domain/service/ScheduleReminderSchedulerService.java
Quartz를 사용하여 알림 작업을 스케줄링하고, 중복 방지 및 취소 기능을 관리합니다.
스케줄 조회 및 수정 서비스
src/main/.../schedule/domain/service/ScheduleReadService.java, src/main/.../schedule/domain/usecase/ScheduleModifyUseCase.java
알림 조회 메서드를 추가하고, 스케줄 생성/수정/삭제 시 이벤트를 발행합니다.
스케줄 알림 엔티티 및 리포지토리
src/main/.../schedule/persistence/entity/ScheduleReminderJob.java, src/main/.../schedule/persistence/repository/ScheduleReminderJobRepository.java, src/main/.../schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustom(Impl).java
알림 작업 기록을 저장하는 JPA 엔티티와 QueryDSL 기반 커스텀 리포지토리를 구현합니다.
Quartz 작업
src/main/.../schedule/infrastructure/scheduler/job/ScheduleReminderQuartzJob.java
Quartz 작업 실행 시 알림 이벤트를 발행합니다.
스케줄 저장소 및 Enum 개선
src/main/.../schedule/persistence/repository/querydsl/ScheduleRepositoryCustom(Impl).java, src/main/.../schedule/persistence/vo/ScheduleAlram.java
알림과 함께 활성 스케줄을 조회하는 메서드를 추가하고, ScheduleAlram enum에 분 단위 값을 추가합니다.
아키텍처 테스트 업데이트
src/test/.../architecture/main/domain/LayeredArchitectureTest.java
스케줄 이벤트 패키지 예외 사항을 추가합니다.
단위 및 통합 테스트
src/test/.../notification/domain/.../ScheduleReminderDataTest.java, src/test/.../notification/domain/.../ScheduleReminderNotificationEventTest.java, src/test/.../schedule/domain/event/ScheduleEventTest.java, src/test/.../schedule/domain/event/ScheduleReminderEventListenerTest.java, src/test/.../schedule/domain/service/ScheduleReminderSchedulerServiceTest.java, src/test/.../schedule/persistence/vo/ScheduleAlramTest.java
알림 데이터, 이벤트, 이벤트 리스너, 스케줄러 서비스 및 Enum에 대한 포괄적인 테스트 커버리지를 추가합니다.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant API as 스케줄 API
    participant UseCase as ScheduleModifyUseCase
    participant Publisher as EventPublisher
    participant Listener as ScheduleReminderEventListener
    participant Scheduler as ScheduleReminderSchedulerService
    participant Quartz as Quartz Scheduler
    participant Job as ScheduleReminderQuartzJob
    participant Notification as NotificationMessagePublisher

    User->>API: 스케줄 생성
    API->>UseCase: createSchedule()
    UseCase->>UseCase: 스케줄 저장
    UseCase->>Publisher: ScheduleCreatedEvent 발행
    Publisher->>Listener: 이벤트 수신
    Listener->>Listener: 스케줄 로드
    Listener->>Scheduler: scheduleScheduleReminders()
    Scheduler->>Scheduler: 알림 시간 계산<br/>(모든 날씨 / 시간별)
    Scheduler->>Quartz: JobDetail 및 Trigger 생성
    Quartz->>Quartz: 알림 작업 등록
    Scheduler->>Scheduler: ScheduleReminderJob 저장

    Note over Quartz: 예정된 시간에 트리거 실행
    Quartz->>Job: 알림 작업 실행
    Job->>Job: 작업 데이터 추출
    Job->>Notification: ScheduleReminderNotificationEvent 발행
    Notification->>Notification: 알림 전송
    Job-->>Quartz: 완료
Loading
sequenceDiagram
    participant Batch as ScheduleReminderBatchSchedulerUseCase
    participant Scheduler as APScheduler (매일 03:58)
    participant ReadService as ScheduleReadService
    participant DB as Database
    participant ReminderScheduler as ScheduleReminderSchedulerService
    participant QuartzScheduler as Quartz Scheduler

    Scheduler->>Batch: 배치 작업 시작
    Batch->>Batch: ShedLock 획득
    Batch->>ReadService: 오늘과 내일의<br/>활성 스케줄 조회
    ReadService->>DB: findActiveSchedulesWithAlarmForDates()
    DB-->>ReadService: 스케줄 목록 반환
    ReadService-->>Batch: 스케줄 목록

    loop 각 스케줄마다
        Batch->>ReminderScheduler: scheduleScheduleReminders()
        ReminderScheduler->>ReminderScheduler: 알림 시간 계산
        ReminderScheduler->>QuartzScheduler: 알림 작업 등록
        ReminderScheduler->>DB: ScheduleReminderJob 저장
    end

    Batch-->>Scheduler: 배치 완료
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested labels

✨ Feature

Suggested reviewers

  • Junad-Park
  • Seol-JY
  • wafla

Poem

🐰 알림의 시간이 왔네요,
Quartz 시계가 똑딱대며,
이벤트 흐름을 따라 춤을 추고,
배치는 새벽 세시에 소근거리며,
스케줄과 알림이 손을 맞잡습니다! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 일정 알림 기능 개발을 정확하게 요약하고 있으며, 간결하고 명확합니다.
Description check ✅ Passed PR 설명은 템플릿 구조를 따르고 있으며, 4가지 주요 작업 내용(데이터 계층, Quartz 스케줄러, 이벤트 연동, 테스트)과 리뷰 요청사항을 포함하고 있습니다.

✏️ 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/162-schedule-reminder

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@kang20 kang20 self-assigned this Feb 23, 2026
@github-actions
Copy link
Copy Markdown

🧪 Test Results

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit 6f135b4. ± Comparison against base commit 995d7bb.

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

🧹 Nitpick comments (8)
src/main/java/im/toduck/domain/schedule/domain/event/ScheduleUpdatedEvent.java (1)

16-33: 생성자 파라미터가 많아 호출 시 실수가 발생하기 쉽습니다.

8개의 파라미터 중 6개가 동일한 boolean 타입이라 순서를 잘못 전달해도 컴파일 타임에 감지되지 않습니다. @Builder 패턴을 사용하면 가독성과 안전성이 향상됩니다.

♻️ Builder 패턴 적용 제안
+import lombok.Builder;
+
 `@Getter`
+@Builder
 public class ScheduleUpdatedEvent {
     private final Long scheduleId;
     private final Long userId;
     private final boolean isAlarmChanged;
     private final boolean isTimeChanged;
     private final boolean isAllDayChanged;
     private final boolean isTitleChanged;
     private final boolean isDateChanged;
     private final boolean isDaysOfWeekChanged;
-
-    public ScheduleUpdatedEvent(
-            final Long scheduleId,
-            final Long userId,
-            final boolean isAlarmChanged,
-            final boolean isTimeChanged,
-            final boolean isAllDayChanged,
-            final boolean isTitleChanged,
-            final boolean isDateChanged,
-            final boolean isDaysOfWeekChanged) {
-        this.scheduleId = scheduleId;
-        this.userId = userId;
-        this.isAlarmChanged = isAlarmChanged;
-        this.isTimeChanged = isTimeChanged;
-        this.isAllDayChanged = isAllDayChanged;
-        this.isTitleChanged = isTitleChanged;
-        this.isDateChanged = isDateChanged;
-        this.isDaysOfWeekChanged = isDaysOfWeekChanged;
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/schedule/domain/event/ScheduleUpdatedEvent.java`
around lines 16 - 33, The ScheduleUpdatedEvent constructor has many boolean
parameters which is error-prone; replace the multi-boolean constructor with a
builder to improve readability and safety: annotate the ScheduleUpdatedEvent
class (or create a static inner Builder) using Lombok's `@Builder` (or implement a
manual builder) and remove or deprecate the long positional constructor; ensure
fields (scheduleId, userId, isAlarmChanged, isTimeChanged, isAllDayChanged,
isTitleChanged, isDateChanged, isDaysOfWeekChanged) are accessible to the
builder and update all call sites to use
ScheduleUpdatedEvent.builder()....build() so callers set named properties
instead of relying on positional booleans.
src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java (1)

68-78: 배치 쿼리에 대한 인덱스 추가를 고려하세요.

findActiveSchedulesWithAlarmForDates 쿼리는 전체 사용자의 스케줄을 대상으로 alarm IS NOT NULL, deleted_at IS NULL, 날짜 범위 조건으로 조회합니다. 데이터 증가 시 성능 저하가 우려됩니다.

schedule 테이블에 (deleted_at, start_date, end_date) 등의 복합 인덱스 추가를 고려해 주세요.

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

In
`@src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java`
around lines 68 - 78, The query in findActiveSchedulesWithAlarmForDates (uses
schedule.scheduleTime.alarm, schedule.deletedAt,
schedule.scheduleDate.startDate, schedule.scheduleDate.endDate) will scan many
rows as data grows; add a DB-level composite index to support these predicates
(e.g., composite index on deleted_at, start_date, end_date — optionally include
alarm if selective) by creating a migration that adds the index and/or annotate
the Schedule entity with a matching `@Table`(indexes=...) entry so the database
can use the index for this query.
src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustomImpl.java (1)

42-58: deleteByScheduleIddeleteByScheduleIdAndReminderDateAfter에서 QueryDSL 벌크 삭제 사용 시 영속성 컨텍스트 불일치 가능성

cancelAllScheduleReminderscancelFutureScheduleReminders에서 먼저 엔티티를 fetch한 후 동일 트랜잭션 내에서 벌크 delete를 실행합니다. QueryDSL 벌크 삭제는 JPA 영속성 컨텍스트를 우회하므로, 삭제 후에도 1차 캐시에 해당 엔티티가 남아 있을 수 있습니다.

현재 코드에서는 삭제 후 해당 엔티티를 다시 조회하지 않으므로 실질적인 문제는 없지만, 향후 유지보수 시 주의가 필요합니다. 필요 시 entityManager.flush()entityManager.clear() 호출을 고려하세요.

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

In
`@src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustomImpl.java`
around lines 42 - 58, deleteByScheduleId and
deleteByScheduleIdAndReminderDateAfter perform QueryDSL bulk deletes which
bypass the JPA persistence context and can leave stale entities fetched earlier
by methods like cancelAllScheduleReminders and cancelFutureScheduleReminders in
the first-level cache; after executing the bulk delete in
ScheduleReminderJobRepositoryCustomImpl, call entityManager.flush() and
entityManager.clear() (or alternatively avoid bulk delete and remove entities
via the EntityManager per-entity) to ensure the persistence context is
synchronized and no stale entities remain.
src/test/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerServiceTest.java (1)

96-118: 핵심 비즈니스 로직(알림 시간 계산)에 대한 검증 부족

테스트 이름은 "10분 전에 알림을 스케줄링한다"지만, 실제로는 any(Trigger.class)로만 검증하여 트리거의 실행 시간이 정확히 13:50인지 확인하지 않습니다. THIRTY_MINUTE, ONE_DAY 테스트도 동일한 문제가 있습니다.

ArgumentCaptor를 사용하여 실제 트리거 시간을 검증하면 리마인더 시간 계산 로직의 정확성을 보장할 수 있습니다.

♻️ ArgumentCaptor를 사용한 트리거 시간 검증 예시
import org.mockito.ArgumentCaptor;

// then
ArgumentCaptor<Trigger> triggerCaptor = ArgumentCaptor.forClass(Trigger.class);
then(scheduler).should(atLeastOnce()).scheduleJob(any(JobDetail.class), triggerCaptor.capture());

Trigger capturedTrigger = triggerCaptor.getValue();
Date expectedTime = Date.from(
    LocalDateTime.of(2026, 2, 24, 13, 50)  // 14:00 - 10분
        .atZone(ZoneId.systemDefault()).toInstant());
assertThat(capturedTrigger.getStartTime()).isEqualTo(expectedTime);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerServiceTest.java`
around lines 96 - 118, The test currently verifies only that scheduleJob was
called with any Trigger, so add an ArgumentCaptor<Trigger> in
ScheduleReminderSchedulerServiceTest to capture the actual Trigger passed into
scheduler.scheduleJob when calling schedulerService.scheduleScheduleReminders;
extract the captured Trigger's start time and assert it matches the expected
reminder time (for the TEN_MINUTE case LocalDateTime.of(2026,2,24,13,50)
converted to Date/Instant using the system ZoneId). Use the same pattern to
adjust/augment the THIRTY_MINUTE and ONE_DAY tests to capture the Trigger and
assert startTime equals the computed reminder time for those alarms, referencing
scheduler.scheduleJob,
ScheduleReminderSchedulerService#scheduleScheduleReminders, and
ScheduleReminderJob repository save assertions to keep existing behavior checks.
src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java (1)

46-50: 메서드 이름 scheduleScheduleReminders 가독성 개선

schedule이 동사와 명사로 중복되어 읽기 어렵습니다. registerScheduleReminders 또는 createScheduleReminders 같은 대안을 고려해 주세요.

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

In
`@src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java`
around lines 46 - 50, Rename the confusingly named method
scheduleScheduleReminders in ScheduleReminderSchedulerService to a clearer name
(e.g. registerScheduleReminders or createScheduleReminders) and update its
declaration (keeping `@Transactional` and the same parameters: Schedule schedule,
LocalDateTime currentDateTime, boolean isBatchScheduling), then update all
usages, imports, tests, and JavaDocs to reference the new method name so
compilation and behavior remain unchanged.
src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java (1)

34-34: LocalDateTime.now()가 여러 곳에서 개별 호출되어 자정 경계에서 불일치가 발생할 수 있습니다.

handleScheduleCreated의 Line 34와 handleScheduleUpdated의 Line 50(LocalDate.now()), Line 54(LocalDateTime.now())에서 각각 현재 시간을 별도로 조회합니다. 자정 전후에 이벤트가 처리되면 날짜가 달라질 수 있어 알림 스케줄링에 불일치가 발생할 수 있습니다.

핸들러 시작 시 LocalDateTime.now()를 한 번만 호출하여 일관되게 사용하는 것을 권장합니다.

♻️ 예시: handleScheduleUpdated에서 시간 통일
     public void handleScheduleUpdated(final ScheduleUpdatedEvent event) {
         log.info("일정 수정 이벤트 처리 시작 - ScheduleId: {}", event.getScheduleId());
+        LocalDateTime currentDateTime = LocalDateTime.now();
 
         try {
             if (event.isReminderRelatedChanged()) {
                 scheduleReminderSchedulerService.cancelFutureScheduleReminders(
-                        event.getScheduleId(), LocalDate.now());
+                        event.getScheduleId(), currentDateTime.toLocalDate());
 
                 scheduleReadService.getScheduleById(event.getScheduleId())
                         .ifPresent(schedule -> {
-                            LocalDateTime currentDateTime = LocalDateTime.now();
                             scheduleReminderSchedulerService.scheduleScheduleReminders(
                                     schedule, currentDateTime, false);
                         });
             }

Also applies to: 54-54

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

In
`@src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java`
at line 34, In ScheduleReminderEventListener, avoid multiple calls to
LocalDateTime.now() by capturing a single LocalDateTime at the start of each
handler and reusing it for all date/time calculations; update
handleScheduleCreated and handleScheduleUpdated to declare a single
LocalDateTime (e.g., currentDateTime) once at method start and derive LocalDate
or any other time values from that variable instead of calling LocalDate.now()
or LocalDateTime.now() again.
src/main/java/im/toduck/domain/schedule/infrastructure/scheduler/job/ScheduleReminderQuartzJob.java (1)

23-46: 데이터 추출 및 enum 파싱이 try-catch 블록 밖에 있습니다.

Line 26-32의 JobDataMap 값 추출과 ScheduleAlram.valueOf() 호출이 try-catch 밖에 위치하여, 잘못된 키나 유효하지 않은 enum 이름으로 인한 예외가 JobExecutionException으로 래핑되지 않고 그대로 전파됩니다. Quartz의 misfire 처리와 일관된 에러 핸들링을 위해 전체 로직을 try-catch 안으로 이동하는 것을 권장합니다.

♻️ 제안: 전체 로직을 try-catch 안으로 이동
     `@Override`
     protected void executeInternal(final JobExecutionContext context) throws JobExecutionException {
-        JobDataMap jobDataMap = context.getMergedJobDataMap();
-
-        Long userId = jobDataMap.getLong("userId");
-        Long scheduleId = jobDataMap.getLong("scheduleId");
-        String scheduleTitle = jobDataMap.getString("scheduleTitle");
-        String reminderTypeName = jobDataMap.getString("reminderType");
-        boolean isAllDay = jobDataMap.getBoolean("isAllDay");
-
-        ScheduleAlram reminderType = ScheduleAlram.valueOf(reminderTypeName);
-
-        log.info("일정 알림 발송 - UserId: {}, ScheduleId: {}, Title: {}", userId, scheduleId, scheduleTitle);
-
         try {
+            JobDataMap jobDataMap = context.getMergedJobDataMap();
+
+            Long userId = jobDataMap.getLong("userId");
+            Long scheduleId = jobDataMap.getLong("scheduleId");
+            String scheduleTitle = jobDataMap.getString("scheduleTitle");
+            String reminderTypeName = jobDataMap.getString("reminderType");
+            boolean isAllDay = jobDataMap.getBoolean("isAllDay");
+
+            ScheduleAlram reminderType = ScheduleAlram.valueOf(reminderTypeName);
+
+            log.info("일정 알림 발송 - UserId: {}, ScheduleId: {}, Title: {}", userId, scheduleId, scheduleTitle);
+
             ScheduleReminderNotificationEvent event = ScheduleReminderNotificationEvent.of(
                     userId, scheduleId, scheduleTitle, reminderType, isAllDay);
 
             notificationMessagePublisher.publishNotificationEvent(event);
 
             log.info("일정 알림 이벤트 발행 완료 - ScheduleId: {}", scheduleId);
         } catch (Exception e) {
-            log.error("일정 알림 이벤트 발행 실패 - ScheduleId: {}", scheduleId, e);
+            log.error("일정 알림 처리 실패", e);
             throw new JobExecutionException("일정 알림 이벤트 발행 실패", e);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/schedule/infrastructure/scheduler/job/ScheduleReminderQuartzJob.java`
around lines 23 - 46, The data extraction from JobDataMap and enum parsing are
outside the try-catch in executeInternal, so malformed keys or invalid enum
names escape the JobExecutionException handling; move the JobDataMap reads
(userId, scheduleId, scheduleTitle, reminderTypeName, isAllDay) and the
ScheduleAlram.valueOf(reminderTypeName) call inside the existing try block (in
method executeInternal) so any Exception is caught, logged via log.error("일정 알림
이벤트 발행 실패 - ScheduleId: {}", scheduleId, e) and rethrown as a
JobExecutionException; ensure variables used after the try (e.g., when
constructing ScheduleReminderNotificationEvent and calling
notificationMessagePublisher.publishNotificationEvent) are declared/initialized
appropriately within the try so the control flow and error wrapping are
consistent.
src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleModifyUseCase.java (1)

137-140: ScheduleUpdatedEvent의 모든 변경 플래그가 true로 설정되어 있습니다.

Line 140에서 isAlarmChanged, isTimeChanged, isAllDayChanged, isTitleChanged, isDateChanged, isDaysOfWeekChanged 모두 true로 하드코딩되어 있습니다. 실제로 변경된 필드만 반영하는 것이 이벤트의 의미론적 정확성을 높이고, 리스너의 불필요한 재스케줄링을 방지할 수 있습니다.

현재 리스너는 isReminderRelatedChanged()만 체크하므로 동작에는 문제가 없지만, 향후 이벤트 소비자가 개별 플래그에 의존할 경우 잘못된 판단을 내릴 수 있습니다.

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

In
`@src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleModifyUseCase.java`
around lines 137 - 140, ScheduleUpdatedEvent is being created with all change
flags hardcoded true; instead compute each flag from the actual difference
between the original schedule and the modified result inside
ScheduleModifyUseCase and pass those booleans into new ScheduleUpdatedEvent
(e.g., compute isAlarmChanged = !Objects.equals(original.getAlarm(),
result.getAlarm()), isTimeChanged = !Objects.equals(original.getTime(),
result.getTime()) or check start/end timestamps, isAllDayChanged =
original.isAllDay() != result.isAllDay(), isTitleChanged =
!Objects.equals(original.getTitle(), result.getTitle()), isDateChanged =
!Objects.equals(original.getDate(), result.getDate()), isDaysOfWeekChanged =
!Objects.equals(original.getDaysOfWeek(), result.getDaysOfWeek())); then call
eventPublisher.publishEvent(new ScheduleUpdatedEvent(result.scheduleId(),
userId, isAlarmChanged, isTimeChanged, isAllDayChanged, isTitleChanged,
isDateChanged, isDaysOfWeekChanged)).
🤖 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/im/toduck/domain/notification/domain/event/ScheduleReminderNotificationEvent.java`:
- Around line 44-52: In ScheduleReminderNotificationEvent.getPushBody(), guard
against the case where getData().getReminderType() == ScheduleAlram.ONE_DAY
while getData().isAllDay() is false so you don't render "1440분 전"; update the
method to detect that combination and return a natural string (e.g., use "일정 하루
전! ..." or another short phrase) instead of String.format with minutes, or add a
defensive comment explaining the invariants if you prefer to fail fast;
reference getPushBody(), getData().isAllDay(), getData().getReminderType(), and
ScheduleAlram.ONE_DAY when making the change.

In
`@src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java`:
- Around line 47-57: The listener currently cancels existing reminders before
verifying the schedule exists, causing potential loss if getScheduleById returns
empty or throws; change ScheduleReminderEventListener so that when
event.isReminderRelatedChanged() you first call
scheduleReadService.getScheduleById(event.getScheduleId()), and only if the
Optional isPresent (and retrieval succeeded) then call
scheduleReminderSchedulerService.cancelFutureScheduleReminders(event.getScheduleId(),
LocalDate.now()) followed by
scheduleReminderSchedulerService.scheduleScheduleReminders(schedule,
LocalDateTime.now(), false); also ensure any exceptions from getScheduleById are
handled/logged and do not trigger cancellation.

In
`@src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java`:
- Around line 135-160: The scheduleReminderForDate method currently calls
scheduler.scheduleJob then saveReminderJobRecord which can lead to Quartz/DB
inconsistency and silently swallows SchedulerExceptions; change the flow to
ensure atomicity/compensation: perform saveReminderJobRecord first inside a
`@Transactional` boundary (or confirm record persisted), then call
scheduler.scheduleJob and if scheduling fails throw/propagate a runtime
exception (or implement retry) so the transaction can roll back; alternatively,
if you must schedule first, implement compensation by catching failures from
saveReminderJobRecord and unscheduling the already-registered job using
scheduler.deleteJob(jobKey) (use createJobKey to locate it), and for
SchedulerException ensure you surface the error (rethrow or trigger retry/alert)
instead of only logging; keep references to scheduleReminderForDate,
scheduler.scheduleJob, saveReminderJobRecord, createJobKey and ensure exception
handling covers both paths to maintain consistency.
- Around line 189-202: The createTrigger method in
ScheduleReminderSchedulerService uses ZoneId.systemDefault(), causing timezone
mismatch with the cron in ScheduleReminderBatchSchedulerUseCase (which uses
Asia/Seoul); change createTrigger to convert reminderDateTime using the explicit
ZoneId.of("Asia/Seoul") (or accept a ZoneId parameter propagated from where the
cron is defined) so TriggerBuilder.startAt(...) uses the correct Asia/Seoul
instant; update references to createTrigger and any callers to pass or rely on
the explicit zone and ensure TRIGGER_GROUP identity remains unchanged.
- Around line 89-106: The loop in scheduleRemindersInDateRange iterates from
startDate to endDate unconditionally which is wasteful; cap the traversal window
by computing an effectiveStart = max(startDate, currentDateTime.toLocalDate())
and an effectiveEnd = min(endDate, scheduleUntil.toLocalDate()) and iterate only
between those dates so shouldScheduleReminder checks are only evaluated for
dates that can actually produce reminderTimes before scheduleUntil; update
scheduleRemindersInDateRange to use effectiveStart/effectiveEnd and keep
existing calls to shouldScheduleForDate, calculateReminderTime, and
shouldScheduleReminder unchanged.

In
`@src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleModifyUseCase.java`:
- Around line 127-142: The current flow in ScheduleModifyUseCase publishes
ScheduleUpdatedEvent only for the newly created schedule ID
(result.scheduleId()), so alarms tied to the original schedule
(request.scheduleId()) aren’t cleaned up when updateSingleDate or
updateAfterDate create a new entity; update the method to detect when a new
schedule ID differs from request.scheduleId() (or when
updateSingleDate/updateAfterDate semantics indicate the original was
soft-deleted) and publish an additional ScheduleUpdatedEvent (or a dedicated
cleanup event) for the original request.scheduleId() with the appropriate flags
to cancel/reschedule its notifications; reference
scheduleModifyService.updateSingleDate, scheduleModifyService.updateAfterDate,
ScheduleUpdatedEvent and request.scheduleId() to locate where to add the extra
event emission.

In
`@src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleReminderBatchSchedulerUseCase.java`:
- Around line 27-43: The current `@Transactional` scheduleDailyScheduleReminders
method can roll back all processed schedules if
scheduleReminderSchedulerService.scheduleScheduleReminders throws; to fix,
remove or avoid a single enclosing transaction and isolate failures per
schedule: wrap each call to
ScheduleReminderSchedulerService.scheduleScheduleReminders(schedule,
currentDateTime, true) in a try-catch that logs the schedule identifier and
exception and continues, or change transaction semantics so each schedule is
processed in its own transaction (e.g., move transactional boundary to
ScheduleReminderSchedulerService.scheduleScheduleReminders or annotate it with
PROPAGATION_REQUIRES_NEW) to prevent one failure from rolling back others; also
add unit/integration tests for full-success, partial-failure, and empty-result
scenarios for
ScheduleReminderBatchSchedulerUseCase.scheduleDailyScheduleReminders.

In
`@src/main/java/im/toduck/domain/schedule/persistence/entity/ScheduleReminderJob.java`:
- Around line 31-35: The ScheduleReminderJob entity currently has no FK
constraint for scheduleId/userId which risks orphaned rows if the async event
handler ScheduleReminderEventListener.handleScheduleDeleted() (which calls
cancelAllScheduleReminders()) fails or is delayed; add proper foreign key
constraints for schedule_id and user_id on the ScheduleReminderJob table (or map
them with `@ManyToOne` relationships) and/or implement a retry/backoff for the
asynchronous cancelAllScheduleReminders() invocation (or convert it to a
synchronous transactional delete) so deletions are guaranteed; update the
ScheduleReminderJob mapping and the
ScheduleReminderEventListener.cancelAllScheduleReminders()/handleScheduleDeleted()
flow accordingly to ensure referential integrity or reliable retries.

In
`@src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustom.java`:
- Line 18: 메서드 이름과 구현이 불일치합니다: ScheduleReminderJobRepositoryCustom의
deleteByScheduleIdAndReminderDateAfter는
구현(ScheduleReminderJobRepositoryCustomImpl에서 goe(date) 사용)상 날짜를 포함해 삭제하는데, 이름의
"After"는 보통 제외(>)를 의미합니다; 수정하려면 둘 중 하나를 선택하세요 — 메서드명을
deleteByScheduleIdAndReminderDateGreaterThanEqual로 변경해 네이밍을 구현과 일치시키거나, 구현을 엄격
비교로 바꿔 ScheduleReminderJobRepositoryCustomImpl의 해당 조건을 .gt(date)로 변경해 이름과 동작을
일치시키십시오 (참조: deleteByScheduleIdAndReminderDateAfter,
ScheduleReminderJobRepositoryCustomImpl,
findByScheduleIdAndReminderDateGreaterThanEqual).

---

Nitpick comments:
In
`@src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java`:
- Line 34: In ScheduleReminderEventListener, avoid multiple calls to
LocalDateTime.now() by capturing a single LocalDateTime at the start of each
handler and reusing it for all date/time calculations; update
handleScheduleCreated and handleScheduleUpdated to declare a single
LocalDateTime (e.g., currentDateTime) once at method start and derive LocalDate
or any other time values from that variable instead of calling LocalDate.now()
or LocalDateTime.now() again.

In
`@src/main/java/im/toduck/domain/schedule/domain/event/ScheduleUpdatedEvent.java`:
- Around line 16-33: The ScheduleUpdatedEvent constructor has many boolean
parameters which is error-prone; replace the multi-boolean constructor with a
builder to improve readability and safety: annotate the ScheduleUpdatedEvent
class (or create a static inner Builder) using Lombok's `@Builder` (or implement a
manual builder) and remove or deprecate the long positional constructor; ensure
fields (scheduleId, userId, isAlarmChanged, isTimeChanged, isAllDayChanged,
isTitleChanged, isDateChanged, isDaysOfWeekChanged) are accessible to the
builder and update all call sites to use
ScheduleUpdatedEvent.builder()....build() so callers set named properties
instead of relying on positional booleans.

In
`@src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java`:
- Around line 46-50: Rename the confusingly named method
scheduleScheduleReminders in ScheduleReminderSchedulerService to a clearer name
(e.g. registerScheduleReminders or createScheduleReminders) and update its
declaration (keeping `@Transactional` and the same parameters: Schedule schedule,
LocalDateTime currentDateTime, boolean isBatchScheduling), then update all
usages, imports, tests, and JavaDocs to reference the new method name so
compilation and behavior remain unchanged.

In
`@src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleModifyUseCase.java`:
- Around line 137-140: ScheduleUpdatedEvent is being created with all change
flags hardcoded true; instead compute each flag from the actual difference
between the original schedule and the modified result inside
ScheduleModifyUseCase and pass those booleans into new ScheduleUpdatedEvent
(e.g., compute isAlarmChanged = !Objects.equals(original.getAlarm(),
result.getAlarm()), isTimeChanged = !Objects.equals(original.getTime(),
result.getTime()) or check start/end timestamps, isAllDayChanged =
original.isAllDay() != result.isAllDay(), isTitleChanged =
!Objects.equals(original.getTitle(), result.getTitle()), isDateChanged =
!Objects.equals(original.getDate(), result.getDate()), isDaysOfWeekChanged =
!Objects.equals(original.getDaysOfWeek(), result.getDaysOfWeek())); then call
eventPublisher.publishEvent(new ScheduleUpdatedEvent(result.scheduleId(),
userId, isAlarmChanged, isTimeChanged, isAllDayChanged, isTitleChanged,
isDateChanged, isDaysOfWeekChanged)).

In
`@src/main/java/im/toduck/domain/schedule/infrastructure/scheduler/job/ScheduleReminderQuartzJob.java`:
- Around line 23-46: The data extraction from JobDataMap and enum parsing are
outside the try-catch in executeInternal, so malformed keys or invalid enum
names escape the JobExecutionException handling; move the JobDataMap reads
(userId, scheduleId, scheduleTitle, reminderTypeName, isAllDay) and the
ScheduleAlram.valueOf(reminderTypeName) call inside the existing try block (in
method executeInternal) so any Exception is caught, logged via log.error("일정 알림
이벤트 발행 실패 - ScheduleId: {}", scheduleId, e) and rethrown as a
JobExecutionException; ensure variables used after the try (e.g., when
constructing ScheduleReminderNotificationEvent and calling
notificationMessagePublisher.publishNotificationEvent) are declared/initialized
appropriately within the try so the control flow and error wrapping are
consistent.

In
`@src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustomImpl.java`:
- Around line 42-58: deleteByScheduleId and
deleteByScheduleIdAndReminderDateAfter perform QueryDSL bulk deletes which
bypass the JPA persistence context and can leave stale entities fetched earlier
by methods like cancelAllScheduleReminders and cancelFutureScheduleReminders in
the first-level cache; after executing the bulk delete in
ScheduleReminderJobRepositoryCustomImpl, call entityManager.flush() and
entityManager.clear() (or alternatively avoid bulk delete and remove entities
via the EntityManager per-entity) to ensure the persistence context is
synchronized and no stale entities remain.

In
`@src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java`:
- Around line 68-78: The query in findActiveSchedulesWithAlarmForDates (uses
schedule.scheduleTime.alarm, schedule.deletedAt,
schedule.scheduleDate.startDate, schedule.scheduleDate.endDate) will scan many
rows as data grows; add a DB-level composite index to support these predicates
(e.g., composite index on deleted_at, start_date, end_date — optionally include
alarm if selective) by creating a migration that adds the index and/or annotate
the Schedule entity with a matching `@Table`(indexes=...) entry so the database
can use the index for this query.

In
`@src/test/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerServiceTest.java`:
- Around line 96-118: The test currently verifies only that scheduleJob was
called with any Trigger, so add an ArgumentCaptor<Trigger> in
ScheduleReminderSchedulerServiceTest to capture the actual Trigger passed into
scheduler.scheduleJob when calling schedulerService.scheduleScheduleReminders;
extract the captured Trigger's start time and assert it matches the expected
reminder time (for the TEN_MINUTE case LocalDateTime.of(2026,2,24,13,50)
converted to Date/Instant using the system ZoneId). Use the same pattern to
adjust/augment the THIRTY_MINUTE and ONE_DAY tests to capture the Trigger and
assert startTime equals the computed reminder time for those alarms, referencing
scheduler.scheduleJob,
ScheduleReminderSchedulerService#scheduleScheduleReminders, and
ScheduleReminderJob repository save assertions to keep existing behavior checks.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 995d7bb and 6f135b4.

📒 Files selected for processing (25)
  • src/main/java/im/toduck/domain/notification/domain/data/ScheduleReminderData.java
  • src/main/java/im/toduck/domain/notification/domain/event/ScheduleReminderNotificationEvent.java
  • src/main/java/im/toduck/domain/schedule/domain/event/ScheduleCreatedEvent.java
  • src/main/java/im/toduck/domain/schedule/domain/event/ScheduleDeletedEvent.java
  • src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java
  • src/main/java/im/toduck/domain/schedule/domain/event/ScheduleUpdatedEvent.java
  • src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReadService.java
  • src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java
  • src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleModifyUseCase.java
  • src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleReminderBatchSchedulerUseCase.java
  • src/main/java/im/toduck/domain/schedule/infrastructure/scheduler/job/ScheduleReminderQuartzJob.java
  • src/main/java/im/toduck/domain/schedule/persistence/entity/ScheduleReminderJob.java
  • src/main/java/im/toduck/domain/schedule/persistence/repository/ScheduleReminderJobRepository.java
  • src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustom.java
  • src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustomImpl.java
  • src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustom.java
  • src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java
  • src/main/java/im/toduck/domain/schedule/persistence/vo/ScheduleAlram.java
  • src/test/java/im/toduck/architecture/main/domain/LayeredArchitectureTest.java
  • src/test/java/im/toduck/domain/notification/domain/data/ScheduleReminderDataTest.java
  • src/test/java/im/toduck/domain/notification/domain/event/ScheduleReminderNotificationEventTest.java
  • src/test/java/im/toduck/domain/schedule/domain/event/ScheduleEventTest.java
  • src/test/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListenerTest.java
  • src/test/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerServiceTest.java
  • src/test/java/im/toduck/domain/schedule/persistence/vo/ScheduleAlramTest.java

Comment on lines +44 to +52
@Override
public String getPushBody() {
if (getData().isAllDay()) {
return "일정 하루 전! 준비된 하루를 시작해볼까요? 📅";
}

ScheduleAlram reminderType = getData().getReminderType();
return String.format("일정 %d분 전! 준비된 하루를 시작해볼까요? 📅", reminderType.getMinutes());
}
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

ONE_DAY enum이 isAllDay=false와 조합될 경우 "1440분 전" 메시지가 표시됩니다.

현재 getPushBodyisAllDay 플래그만으로 분기하므로, ScheduleAlram.ONE_DAY가 비-종일 일정에 사용되면 "일정 1440분 전!" 이라는 부자연스러운 메시지가 생성됩니다. 현재 비즈니스 로직상 이 조합이 발생하지 않는다면 방어 코드나 주석을 추가하는 것을 권장합니다.

🛡️ 방어 로직 예시
     `@Override`
     public String getPushBody() {
         if (getData().isAllDay()) {
             return "일정 하루 전! 준비된 하루를 시작해볼까요? 📅";
         }
 
         ScheduleAlram reminderType = getData().getReminderType();
+        if (reminderType.getMinutes() >= 1440) {
+            return "일정 하루 전! 준비된 하루를 시작해볼까요? 📅";
+        }
         return String.format("일정 %d분 전! 준비된 하루를 시작해볼까요? 📅", reminderType.getMinutes());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String getPushBody() {
if (getData().isAllDay()) {
return "일정 하루 전! 준비된 하루를 시작해볼까요? 📅";
}
ScheduleAlram reminderType = getData().getReminderType();
return String.format("일정 %d분 전! 준비된 하루를 시작해볼까요? 📅", reminderType.getMinutes());
}
`@Override`
public String getPushBody() {
if (getData().isAllDay()) {
return "일정 하루 전! 준비된 하루를 시작해볼까요? 📅";
}
ScheduleAlram reminderType = getData().getReminderType();
if (reminderType.getMinutes() >= 1440) {
return "일정 하루 전! 준비된 하루를 시작해볼까요? 📅";
}
return String.format("일정 %d분 전! 준비된 하루를 시작해볼까요? 📅", reminderType.getMinutes());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/notification/domain/event/ScheduleReminderNotificationEvent.java`
around lines 44 - 52, In ScheduleReminderNotificationEvent.getPushBody(), guard
against the case where getData().getReminderType() == ScheduleAlram.ONE_DAY
while getData().isAllDay() is false so you don't render "1440분 전"; update the
method to detect that combination and return a natural string (e.g., use "일정 하루
전! ..." or another short phrase) instead of String.format with minutes, or add a
defensive comment explaining the invariants if you prefer to fail fast;
reference getPushBody(), getData().isAllDay(), getData().getReminderType(), and
ScheduleAlram.ONE_DAY when making the change.

Comment on lines +47 to +57
try {
if (event.isReminderRelatedChanged()) {
scheduleReminderSchedulerService.cancelFutureScheduleReminders(
event.getScheduleId(), LocalDate.now());

scheduleReadService.getScheduleById(event.getScheduleId())
.ifPresent(schedule -> {
LocalDateTime currentDateTime = LocalDateTime.now();
scheduleReminderSchedulerService.scheduleScheduleReminders(
schedule, currentDateTime, false);
});
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 | 🟠 Major

취소 후 조회 실패 시 알림이 유실될 수 있습니다.

Line 49-50에서 기존 알림을 먼저 취소한 후, Line 52에서 스케줄을 다시 조회합니다. 만약 getScheduleByIdOptional.empty()를 반환하거나 예외가 발생하면, 기존 알림은 이미 취소되었지만 새 알림은 스케줄링되지 않아 알림이 유실됩니다.

스케줄 조회를 먼저 수행하고, 성공한 경우에만 취소 + 재스케줄링을 진행하는 것을 권장합니다.

🐛 제안: 조회 후 취소 순서로 변경
         try {
             if (event.isReminderRelatedChanged()) {
-                scheduleReminderSchedulerService.cancelFutureScheduleReminders(
-                        event.getScheduleId(), LocalDate.now());
-
                 scheduleReadService.getScheduleById(event.getScheduleId())
                         .ifPresent(schedule -> {
+                            scheduleReminderSchedulerService.cancelFutureScheduleReminders(
+                                    event.getScheduleId(), LocalDate.now());
                             LocalDateTime currentDateTime = LocalDateTime.now();
                             scheduleReminderSchedulerService.scheduleScheduleReminders(
                                     schedule, currentDateTime, false);
                         });
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
if (event.isReminderRelatedChanged()) {
scheduleReminderSchedulerService.cancelFutureScheduleReminders(
event.getScheduleId(), LocalDate.now());
scheduleReadService.getScheduleById(event.getScheduleId())
.ifPresent(schedule -> {
LocalDateTime currentDateTime = LocalDateTime.now();
scheduleReminderSchedulerService.scheduleScheduleReminders(
schedule, currentDateTime, false);
});
try {
if (event.isReminderRelatedChanged()) {
scheduleReadService.getScheduleById(event.getScheduleId())
.ifPresent(schedule -> {
scheduleReminderSchedulerService.cancelFutureScheduleReminders(
event.getScheduleId(), LocalDate.now());
LocalDateTime currentDateTime = LocalDateTime.now();
scheduleReminderSchedulerService.scheduleScheduleReminders(
schedule, currentDateTime, false);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java`
around lines 47 - 57, The listener currently cancels existing reminders before
verifying the schedule exists, causing potential loss if getScheduleById returns
empty or throws; change ScheduleReminderEventListener so that when
event.isReminderRelatedChanged() you first call
scheduleReadService.getScheduleById(event.getScheduleId()), and only if the
Optional isPresent (and retrieval succeeded) then call
scheduleReminderSchedulerService.cancelFutureScheduleReminders(event.getScheduleId(),
LocalDate.now()) followed by
scheduleReminderSchedulerService.scheduleScheduleReminders(schedule,
LocalDateTime.now(), false); also ensure any exceptions from getScheduleById are
handled/logged and do not trigger cancellation.

Comment on lines +89 to +106
private void scheduleRemindersInDateRange(
final Schedule schedule,
final LocalDate startDate,
final LocalDate endDate,
final LocalDateTime currentDateTime,
final LocalDateTime scheduleUntil) {
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
if (shouldScheduleForDate(schedule, currentDate)) {
LocalDateTime reminderTime = calculateReminderTime(schedule, currentDate);

if (shouldScheduleReminder(reminderTime, currentDateTime, scheduleUntil)) {
scheduleReminderForDate(schedule, currentDate, reminderTime);
}
}
currentDate = currentDate.plusDays(1);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

날짜 범위 순회 성능 최적화 필요 (리뷰 요청 사항 답변)

effectiveStartDate부터 endDate까지 모든 날짜를 순회하지만, shouldScheduleReminder에서 [currentTime, scheduleUntil) 범위로 필터링하므로 실제 스케줄링 대상은 1~2일치뿐입니다. 장기간(예: 1년) 반복 일정의 경우 불필요하게 최대 365회 반복할 수 있습니다.

순회 시작점을 scheduleUntil 기준으로 제한하면 성능을 크게 개선할 수 있습니다.

♻️ 순회 범위 최적화 제안
     private void scheduleRemindersInDateRange(
             final Schedule schedule,
             final LocalDate startDate,
             final LocalDate endDate,
             final LocalDateTime currentDateTime,
             final LocalDateTime scheduleUntil) {
-        LocalDate currentDate = startDate;
-        while (!currentDate.isAfter(endDate)) {
+        // scheduleUntil 날짜까지만 순회 (알림은 이 범위 안에서만 생성됨)
+        LocalDate effectiveEndDate = endDate.isBefore(scheduleUntil.toLocalDate())
+                ? endDate
+                : scheduleUntil.toLocalDate();
+        // 종일 일정의 경우 하루 전에 알림이므로 +1일 추가 고려
+        if (schedule.getScheduleTime().getIsAllDay()) {
+            effectiveEndDate = effectiveEndDate.plusDays(1).isBefore(endDate)
+                    ? effectiveEndDate.plusDays(1)
+                    : endDate;
+        }
+        LocalDate currentDate = startDate;
+        while (!currentDate.isAfter(effectiveEndDate)) {
             if (shouldScheduleForDate(schedule, currentDate)) {
                 LocalDateTime reminderTime = calculateReminderTime(schedule, currentDate);

                 if (shouldScheduleReminder(reminderTime, currentDateTime, scheduleUntil)) {
                     scheduleReminderForDate(schedule, currentDate, reminderTime);
                 }
             }
             currentDate = currentDate.plusDays(1);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java`
around lines 89 - 106, The loop in scheduleRemindersInDateRange iterates from
startDate to endDate unconditionally which is wasteful; cap the traversal window
by computing an effectiveStart = max(startDate, currentDateTime.toLocalDate())
and an effectiveEnd = min(endDate, scheduleUntil.toLocalDate()) and iterate only
between those dates so shouldScheduleReminder checks are only evaluated for
dates that can actually produce reminderTimes before scheduleUntil; update
scheduleRemindersInDateRange to use effectiveStart/effectiveEnd and keep
existing calls to shouldScheduleForDate, calculateReminderTime, and
shouldScheduleReminder unchanged.

Comment on lines +135 to +160
private void scheduleReminderForDate(
final Schedule schedule,
final LocalDate scheduleDate,
final LocalDateTime reminderDateTime) {
try {
if (isReminderAlreadyScheduled(schedule.getId(), reminderDateTime)) {
log.debug("이미 스케줄링된 알림 스킵 - ScheduleId: {}, DateTime: {}",
schedule.getId(), reminderDateTime);
return;
}

String jobKey = createJobKey(schedule.getId(), scheduleDate, reminderDateTime.toLocalTime());
JobDetail jobDetail = createJobDetail(schedule, jobKey);
Trigger trigger = createTrigger(jobDetail, reminderDateTime, jobKey);

scheduler.scheduleJob(jobDetail, trigger);
saveReminderJobRecord(schedule, reminderDateTime, jobKey);

log.debug("일정 알림 스케줄링 성공 - ScheduleId: {}, ScheduleDate: {}, ReminderDateTime: {}",
schedule.getId(), scheduleDate, reminderDateTime);

} catch (SchedulerException e) {
log.error("일정 알림 스케줄링 실패 - ScheduleId: {}, ScheduleDate: {}",
schedule.getId(), scheduleDate, e);
}
}
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 | 🟠 Major

Quartz Job 등록과 DB 저장 간 정합성 미보장

Line 150에서 scheduler.scheduleJob이 성공한 후 Line 151에서 DB에 저장하는데, 두 가지 불일치 시나리오가 있습니다:

  1. scheduleJob 성공 → saveReminderJobRecord 실패 (또는 트랜잭션 롤백): Quartz에는 Job이 등록되었지만 DB에는 기록이 없어, 취소 시 Quartz Job이 삭제되지 않습니다.
  2. Line 156-159에서 SchedulerException을 catch하여 로깅만 하므로, Quartz 장애 시 알림이 무음으로 유실되며 재시도 메커니즘이 없습니다.

@Transactional 경계 내에서 Quartz 작업은 롤백 대상이 아니므로, 보상 로직(compensation)이나 최소한 실패 알림 전파를 고려해 주세요.

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

In
`@src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java`
around lines 135 - 160, The scheduleReminderForDate method currently calls
scheduler.scheduleJob then saveReminderJobRecord which can lead to Quartz/DB
inconsistency and silently swallows SchedulerExceptions; change the flow to
ensure atomicity/compensation: perform saveReminderJobRecord first inside a
`@Transactional` boundary (or confirm record persisted), then call
scheduler.scheduleJob and if scheduling fails throw/propagate a runtime
exception (or implement retry) so the transaction can roll back; alternatively,
if you must schedule first, implement compensation by catching failures from
saveReminderJobRecord and unscheduling the already-registered job using
scheduler.deleteJob(jobKey) (use createJobKey to locate it), and for
SchedulerException ensure you surface the error (rethrow or trigger retry/alert)
instead of only logging; keep references to scheduleReminderForDate,
scheduler.scheduleJob, saveReminderJobRecord, createJobKey and ensure exception
handling covers both paths to maintain consistency.

Comment on lines +189 to +202
private Trigger createTrigger(
final JobDetail jobDetail,
final LocalDateTime reminderDateTime,
final String jobKey) {
Date triggerTime = Date.from(reminderDateTime.atZone(ZoneId.systemDefault()).toInstant());

return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(new TriggerKey(jobKey, TRIGGER_GROUP))
.startAt(triggerTime)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withMisfireHandlingInstructionFireNow())
.build();
}
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 | 🟠 Major

ZoneId.systemDefault() 사용으로 인한 시간대 불일치 위험

ScheduleReminderBatchSchedulerUseCase의 cron 표현식은 zone = "Asia/Seoul"을 명시적으로 사용하지만, 트리거 시간 변환에서는 ZoneId.systemDefault()를 사용합니다. JVM 시스템 타임존이 Asia/Seoul이 아닌 환경(예: UTC로 설정된 클라우드 서버)에서는 알림이 잘못된 시간에 발송됩니다.

🐛 명시적 타임존 사용 제안
+    private static final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul");
+
     private Trigger createTrigger(
             final JobDetail jobDetail,
             final LocalDateTime reminderDateTime,
             final String jobKey) {
-        Date triggerTime = Date.from(reminderDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        Date triggerTime = Date.from(reminderDateTime.atZone(ZONE_ID).toInstant());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReminderSchedulerService.java`
around lines 189 - 202, The createTrigger method in
ScheduleReminderSchedulerService uses ZoneId.systemDefault(), causing timezone
mismatch with the cron in ScheduleReminderBatchSchedulerUseCase (which uses
Asia/Seoul); change createTrigger to convert reminderDateTime using the explicit
ZoneId.of("Asia/Seoul") (or accept a ZoneId parameter propagated from where the
cron is defined) so TriggerBuilder.startAt(...) uses the correct Asia/Seoul
instant; update references to createTrigger and any callers to pass or rely on
the explicit zone and ensure TRIGGER_GROUP identity remains unchanged.

Comment on lines +127 to +142

ScheduleIdResponse result;
if (request.isOneDayDeleted()) {
log.info("하루의 일정만 수정 : {}", request.scheduleId());
return scheduleModifyService.updateSingleDate(schedule, request);
result = scheduleModifyService.updateSingleDate(schedule, request);
} else {
log.info("특정 날짜 이후 일괄 일정 수정 : {}", request.scheduleId());
result = scheduleModifyService.updateAfterDate(schedule, request);
}
log.info("특정 날짜 이후 일괄 일정 수정 : {}", request.scheduleId());
return scheduleModifyService.updateAfterDate(schedule, request);

// 원본 일정의 알림 재스케줄링을 위한 이벤트 발행
eventPublisher.publishEvent(new ScheduleUpdatedEvent(
result.scheduleId(), userId,
true, true, true, true, true, true));

return result;
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "*.java" -type f 2>/dev/null | head -20

Repository: toduck-App/toduck-backend

Length of output: 51


🏁 Script executed:

find . -name "*ScheduleModifyService*" -type f

Repository: toduck-App/toduck-backend

Length of output: 152


🏁 Script executed:

rg -n "updateSingleDate|updateAfterDate" --type=java -B2 -A10

Repository: toduck-App/toduck-backend

Length of output: 5155


🏁 Script executed:

rg -n "class ScheduleIdResponse|ScheduleIdResponse.of" --type=java -B2 -A5

Repository: toduck-App/toduck-backend

Length of output: 1989


🏁 Script executed:

sed -n '110,130p' src/main/java/im/toduck/domain/schedule/domain/service/ScheduleModifyService.java

Repository: toduck-App/toduck-backend

Length of output: 736


🏁 Script executed:

cat -n src/main/java/im/toduck/domain/schedule/domain/service/ScheduleModifyService.java | sed -n '110,130p'

Repository: toduck-App/toduck-backend

Length of output: 855


updateSingleDateupdateAfterDate 모두 새로 생성된 일정 ID를 반환하므로, 원본 일정의 알림이 갱신되지 않습니다.

updateSingleDate가 반복 일정을 수정할 경우 새 일정을 생성하여 반환하고, updateAfterDate는 항상 새 일정을 생성하여 반환합니다:

  • updateSingleDate (반복 일정): 원본을 soft-delete하고 새 일정을 생성 후 새 ID 반환 (라인 82-85)
  • updateAfterDate: 항상 새 일정을 생성하여 새 ID 반환 (라인 121-124)

ScheduleUpdatedEvent는 새 일정의 ID로만 발행되므로, 원본 일정(request.scheduleId())의 기존 알림은 재스케줄링되거나 취소되지 않습니다. 원본 일정이 soft-delete되거나 수정될 때 해당 알림을 정리하거나, 원본 일정에 대한 별도의 이벤트를 발행해야 합니다.

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

In
`@src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleModifyUseCase.java`
around lines 127 - 142, The current flow in ScheduleModifyUseCase publishes
ScheduleUpdatedEvent only for the newly created schedule ID
(result.scheduleId()), so alarms tied to the original schedule
(request.scheduleId()) aren’t cleaned up when updateSingleDate or
updateAfterDate create a new entity; update the method to detect when a new
schedule ID differs from request.scheduleId() (or when
updateSingleDate/updateAfterDate semantics indicate the original was
soft-deleted) and publish an additional ScheduleUpdatedEvent (or a dedicated
cleanup event) for the original request.scheduleId() with the appropriate flags
to cancel/reschedule its notifications; reference
scheduleModifyService.updateSingleDate, scheduleModifyService.updateAfterDate,
ScheduleUpdatedEvent and request.scheduleId() to locate where to add the extra
event emission.

Comment on lines +27 to +43
@Scheduled(cron = "0 58 3 * * *", zone = "Asia/Seoul")
@SchedulerLock(name = "ScheduleReminderBatchScheduler_scheduleDailyScheduleReminders", lockAtMostFor = "55m", lockAtLeastFor = "1m")
@Transactional
public void scheduleDailyScheduleReminders() {
LocalDateTime currentDateTime = LocalDateTime.now();
LocalDate today = currentDateTime.toLocalDate();
LocalDate tomorrow = today.plusDays(1);

log.info("일일 일정 알림 배치 작업 시작 - 현재시간: {}", currentDateTime);

scheduleReadService.findActiveSchedulesWithAlarmForDates(today, tomorrow)
.forEach(
schedule -> scheduleReminderSchedulerService.scheduleScheduleReminders(
schedule, currentDateTime, true));

log.info("일일 일정 알림 배치 작업 완료");
}
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 | 🟠 Major

개별 일정 실패 시 전체 배치 롤백 위험

scheduleScheduleReminders에서 RuntimeException이 발생하면(ScheduleReminderSchedulerService Line 75) forEach 스트림이 중단되고, @Transactional에 의해 이전에 성공한 모든 알림 스케줄링까지 롤백됩니다.

개별 일정 실패를 격리하여 나머지 일정은 정상 처리되도록 해야 합니다.

🐛 개별 일정 실패 격리 제안
         scheduleReadService.findActiveSchedulesWithAlarmForDates(today, tomorrow)
-                .forEach(
-                        schedule -> scheduleReminderSchedulerService.scheduleScheduleReminders(
-                                schedule, currentDateTime, true));
+                .forEach(schedule -> {
+                    try {
+                        scheduleReminderSchedulerService.scheduleScheduleReminders(
+                                schedule, currentDateTime, true);
+                    } catch (Exception e) {
+                        log.error("일정 알림 배치 스케줄링 실패 - ScheduleId: {}", schedule.getId(), e);
+                    }
+                });

또한, 이 클래스에 대한 테스트 커버리지가 누락되어 있습니다. 배치 스케줄러의 정상 동작, 부분 실패 처리, 빈 결과 처리 등에 대한 테스트 추가를 고려해 주세요.

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

In
`@src/main/java/im/toduck/domain/schedule/domain/usecase/ScheduleReminderBatchSchedulerUseCase.java`
around lines 27 - 43, The current `@Transactional` scheduleDailyScheduleReminders
method can roll back all processed schedules if
scheduleReminderSchedulerService.scheduleScheduleReminders throws; to fix,
remove or avoid a single enclosing transaction and isolate failures per
schedule: wrap each call to
ScheduleReminderSchedulerService.scheduleScheduleReminders(schedule,
currentDateTime, true) in a try-catch that logs the schedule identifier and
exception and continues, or change transaction semantics so each schedule is
processed in its own transaction (e.g., move transactional boundary to
ScheduleReminderSchedulerService.scheduleScheduleReminders or annotate it with
PROPAGATION_REQUIRES_NEW) to prevent one failure from rolling back others; also
add unit/integration tests for full-success, partial-failure, and empty-result
scenarios for
ScheduleReminderBatchSchedulerUseCase.scheduleDailyScheduleReminders.

Comment on lines +31 to +35
@Column(name = "schedule_id", nullable = false)
private Long scheduleId;

@Column(name = "user_id", nullable = false)
private Long userId;
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

🧩 Analysis chain

🏁 Script executed:

rg -n "ScheduleDeletedEvent" --type=java -C5 -g '!**/test/**'

Repository: toduck-App/toduck-backend

Length of output: 5852


🏁 Script executed:

# Check the ScheduleReminderJob entity for full details
cat -n src/main/java/im/toduck/domain/schedule/persistence/entity/ScheduleReminderJob.java

Repository: toduck-App/toduck-backend

Length of output: 2211


🏁 Script executed:

# Search for delete methods related to ScheduleReminderJob
rg -n "ScheduleReminderJob" --type=java -C3 | grep -E "(delete|remove|cascade)" -i

Repository: toduck-App/toduck-backend

Length of output: 1640


🏁 Script executed:

# Look for event handling or listener implementations
rg -n "@EventListener|implements.*Event|on.*Event" --type=java -g '!**/test/**' | head -20

Repository: toduck-App/toduck-backend

Length of output: 3535


🏁 Script executed:

cat -n src/main/java/im/toduck/domain/schedule/domain/event/ScheduleReminderEventListener.java

Repository: toduck-App/toduck-backend

Length of output: 3785


🏁 Script executed:

# Check ScheduleReminderSchedulerService.cancelAllScheduleReminders implementation
rg -n "cancelAllScheduleReminders" --type=java -A20

Repository: toduck-App/toduck-backend

Length of output: 12840


🏁 Script executed:

# Check for any cleanup batch jobs or scheduled tasks
rg -n "@Scheduled|@Async|batch|cleanup" --type=java -i -g '!**/test/**' | head -30

Repository: toduck-App/toduck-backend

Length of output: 4889


FK 제약 부재로 인한 고아 레코드 위험 - 이벤트 핸들러가 정상 작동 중

외래 키 제약이 없는 설계는 확인되었으나, ScheduleReminderEventListener.handleScheduleDeleted()에서 cancelAllScheduleReminders() 메서드를 호출하여 관련 ScheduleReminderJob 레코드를 삭제하고 있습니다.

다만 주의할 점:

  • 이벤트 핸들러는 @Async로 비동기 실행되므로, 스레드 풀 고갈 등으로 작업이 대기 중일 때 고아 레코드가 임시로 발생할 수 있습니다
  • 현재 try-catch로 예외는 처리되지만, 비동기 작업 자체가 실패하면 레코드가 삭제되지 않을 수 있습니다
  • FK 제약을 추가하거나, 비동기 실행 실패 시 재시도 로직 추가를 고려해 주세요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/im/toduck/domain/schedule/persistence/entity/ScheduleReminderJob.java`
around lines 31 - 35, The ScheduleReminderJob entity currently has no FK
constraint for scheduleId/userId which risks orphaned rows if the async event
handler ScheduleReminderEventListener.handleScheduleDeleted() (which calls
cancelAllScheduleReminders()) fails or is delayed; add proper foreign key
constraints for schedule_id and user_id on the ScheduleReminderJob table (or map
them with `@ManyToOne` relationships) and/or implement a retry/backoff for the
asynchronous cancelAllScheduleReminders() invocation (or convert it to a
synchronous transactional delete) so deletions are guaranteed; update the
ScheduleReminderJob mapping and the
ScheduleReminderEventListener.cancelAllScheduleReminders()/handleScheduleDeleted()
flow accordingly to ensure referential integrity or reliable retries.


void deleteByScheduleId(final Long scheduleId);

void deleteByScheduleIdAndReminderDateAfter(final Long scheduleId, final LocalDate date);
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

deleteByScheduleIdAndReminderDateAfter 메서드 이름과 실제 구현의 의미 불일치

ScheduleReminderJobRepositoryCustomImpl의 구현(Line 56)에서 goe(date) (>=)를 사용하고 있어, 해당 날짜를 포함하여 삭제합니다. 그러나 메서드 이름의 After는 일반적으로 해당 날짜를 제외한 이후(strictly greater than)를 의미합니다.

findByScheduleIdAndReminderDateGreaterThanEqual(Line 13)의 네이밍 컨벤션과도 일관성이 없습니다. deleteByScheduleIdAndReminderDateGreaterThanEqual로 변경하거나, 의도적으로 해당 날짜 이후만 삭제하려면 구현에서 .gt(date)를 사용해야 합니다.

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

In
`@src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleReminderJobRepositoryCustom.java`
at line 18, 메서드 이름과 구현이 불일치합니다: ScheduleReminderJobRepositoryCustom의
deleteByScheduleIdAndReminderDateAfter는
구현(ScheduleReminderJobRepositoryCustomImpl에서 goe(date) 사용)상 날짜를 포함해 삭제하는데, 이름의
"After"는 보통 제외(>)를 의미합니다; 수정하려면 둘 중 하나를 선택하세요 — 메서드명을
deleteByScheduleIdAndReminderDateGreaterThanEqual로 변경해 네이밍을 구현과 일치시키거나, 구현을 엄격
비교로 바꿔 ScheduleReminderJobRepositoryCustomImpl의 해당 조건을 .gt(date)로 변경해 이름과 동작을
일치시키십시오 (참조: deleteByScheduleIdAndReminderDateAfter,
ScheduleReminderJobRepositoryCustomImpl,
findByScheduleIdAndReminderDateGreaterThanEqual).

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.

1 participant