Skip to content

feat: 코스 일정 푸시 알림 기능 구현 - #48

Merged
kwonwnsduf merged 2 commits into
developfrom
feature/push-notifications
Aug 6, 2026
Merged

kwonwnsduf merged 2 commits into
developfrom
feature/push-notifications

Conversation

@kwonwnsduf

@kwonwnsduf kwonwnsduf commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

📌 관련 이슈


🔎 What is this PR?

코스 일정에 맞춰 사용자에게 D-1 및 D-Day 푸시 알림을 전송하는 기능을 구현했
습니다.

  • FCM 토큰 등록: POST /api/users/fcm-token
  • FCM 토큰 삭제: DELETE /api/users/fcm-token

✨ Changes

  • 코스 일정 D-1 및 D-Day 알림 생성 스케줄러를 구현했습니다.
  • 기본적으로 Asia/Seoul 기준 매일 오전 9시에 알림이 생성되도록 설정했습니
    다.
  • 사용자의 푸시 알림 설정과 FCM 토큰 등록 여부를 확인한 후 알림을 생성하도
    록 구현했습니다.
  • FCM 토큰 등록 및 삭제 API를 추가했습니다.
  • Firebase Cloud Messaging을 통한 푸시 알림 발송 기능을 구현했습니다.
  • 알림 발송 상태를 PENDING, SENT, FAILED로 관리하도록 개선했습니다.
  • 발송 실패 시 최대 5회까지 재시도하며, 유효하지 않은 FCM 토큰은 자동으로
    삭제하도록 구현했습니다.
  • 동일한 코스 알림이 중복 생성되지 않도록 처리했습니다.
  • Firebase가 비활성화된 환경에서도 서버와 테스트가 정상적으로 실행되도록 구
    성했습니다.
  • 알림 생성, FCM 토큰 관리, 알림 발송 및 재시도 로직에 대한 테스트를 추가했
    습니다.
  • Firebase 프로젝트 및 서비스 계정 관련 정보는 프론트엔드와 상의한 후 추가
    할 예정입니다.

📷 Result

백엔드 푸시 알림 기능으로 별도의 화면 결과는 없습니다.

  • 코스 일정 D-1/D-Day 알림 자동 생성
  • FCM을 통한 푸시 알림 발송
  • 발송 실패 시 재시도 및 상태 기록
  • 만료되거나 유효하지 않은 FCM 토큰 자동 삭제

💬 To. Reviewer

  • 코스 알림 대상자 조회 및 중복 알림 방지 로직을 중점적으로 확인해 주세요.
  • 알림 발송 실패 시 재시도 처리와 유효하지 않은 FCM 토큰 삭제 로직을 확인해
    주세요.
  • Firebase 프로젝트 ID와 서비스 계정 정보는 프론트엔드와 협의한 후 환경변수
    에 반영할 예정입니다.

Summary by CodeRabbit

  • New Features
    • Added push notification support for course reminders, including delivery tracking and automatic retries.
    • Added authenticated endpoints to register and remove device notification tokens.
    • Added support for Android and iOS devices.
    • Course reminders can be scheduled using configurable time zones and delivery settings.
  • Changes
    • Simplified notification preferences to a single push-notification setting.
  • Documentation
    • Added setup guidance for Firebase Cloud Messaging, credentials, environment variables, retries, and reminder schedules.

@kwonwnsduf kwonwnsduf self-assigned this Aug 6, 2026
@kwonwnsduf kwonwnsduf added the ✨ feature 새로운 기능을 추가하는 작업 label Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kwonwnsduf, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b94e46e-952e-42f2-95cf-66c04a0496f3

📥 Commits

Reviewing files that changed from the base of the PR and between e1092f2 and 1dad57c.

📒 Files selected for processing (43)
  • .env.example
  • README.md
  • build.gradle
  • src/main/java/com/daytodo/domain/auth/service/AuthService.java
  • src/main/java/com/daytodo/domain/course/repository/CourseMemberRepository.java
  • src/main/java/com/daytodo/domain/user/controller/UserController.java
  • src/main/java/com/daytodo/domain/user/dto/UserRequest.java
  • src/main/java/com/daytodo/domain/user/dto/UserResponse.java
  • src/main/java/com/daytodo/domain/user/entity/FcmToken.java
  • src/main/java/com/daytodo/domain/user/entity/Notification.java
  • src/main/java/com/daytodo/domain/user/entity/UserNotificationSetting.java
  • src/main/java/com/daytodo/domain/user/enums/DevicePlatform.java
  • src/main/java/com/daytodo/domain/user/enums/NotificationDeliveryStatus.java
  • src/main/java/com/daytodo/domain/user/push/DisabledPushNotificationGateway.java
  • src/main/java/com/daytodo/domain/user/push/FirebasePushNotificationGateway.java
  • src/main/java/com/daytodo/domain/user/push/PushNotificationGateway.java
  • src/main/java/com/daytodo/domain/user/repository/FcmTokenRepository.java
  • src/main/java/com/daytodo/domain/user/repository/NotificationRepository.java
  • src/main/java/com/daytodo/domain/user/repository/UserNotificationSettingRepository.java
  • src/main/java/com/daytodo/domain/user/repository/WithdrawnUserCleanupRepository.java
  • src/main/java/com/daytodo/domain/user/service/CourseNotificationScheduler.java
  • src/main/java/com/daytodo/domain/user/service/CourseReminderService.java
  • src/main/java/com/daytodo/domain/user/service/FcmTokenService.java
  • src/main/java/com/daytodo/domain/user/service/NotificationDeliveryWorker.java
  • src/main/java/com/daytodo/domain/user/service/NotificationDispatchService.java
  • src/main/java/com/daytodo/domain/user/service/NotificationEventCreator.java
  • src/main/java/com/daytodo/domain/user/service/UserNotificationService.java
  • src/main/java/com/daytodo/global/config/FirebaseConfig.java
  • src/main/java/com/daytodo/global/config/SecurityConfig.java
  • src/main/java/com/daytodo/global/config/TimeConfig.java
  • src/main/resources/application.yml
  • src/test/java/com/daytodo/DayTodoApplicationTests.java
  • src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java
  • src/test/java/com/daytodo/domain/user/controller/UserControllerTest.java
  • src/test/java/com/daytodo/domain/user/repository/FcmTokenRepositoryTest.java
  • src/test/java/com/daytodo/domain/user/service/CourseReminderServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/FcmTokenServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/NotificationDeliveryWorkerTest.java
  • src/test/java/com/daytodo/domain/user/service/NotificationDispatchServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/UserNotificationServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/WithdrawnUserDeletionIntegrationTest.java
  • src/test/java/com/daytodo/global/config/EnvironmentExampleTest.java
  • src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java
📝 Walkthrough

Walkthrough

Added Firebase Cloud Messaging integration, FCM token APIs, scheduled D-1 and D-0 course reminders, notification delivery tracking, retry handling, and related tests and configuration.

Changes

Course notification delivery

Layer / File(s) Summary
Notification contracts and configuration
.env.example, README.md, build.gradle, src/main/java/com/daytodo/domain/user/entity/*, src/main/java/com/daytodo/domain/user/push/*, src/main/java/com/daytodo/global/config/*, src/main/resources/application.yml
Added Firebase configuration, notification delivery states, FCM token data, push gateway contracts, and simplified push settings.
Push settings initialization and service updates
src/main/java/com/daytodo/domain/auth/service/AuthService.java, src/main/java/com/daytodo/domain/user/service/UserNotificationService.java
Created default notification settings during local and Naver user registration. Settings now expose and update only pushEnabled.
FCM token registration and cleanup
src/main/java/com/daytodo/domain/user/controller/UserController.java, src/main/java/com/daytodo/domain/user/service/FcmTokenService.java, src/main/java/com/daytodo/domain/user/repository/FcmTokenRepository.java, src/main/java/com/daytodo/global/config/SecurityConfig.java, src/main/java/com/daytodo/domain/user/repository/WithdrawnUserCleanupRepository.java
Added authenticated token registration and deletion endpoints. Added token persistence, user validation, and withdrawn-user token cleanup.
Scheduled course reminder creation
src/main/java/com/daytodo/domain/course/repository/CourseMemberRepository.java, src/main/java/com/daytodo/domain/user/service/CourseReminderService.java, src/main/java/com/daytodo/domain/user/service/NotificationEventCreator.java, src/main/java/com/daytodo/domain/user/service/CourseNotificationScheduler.java, src/main/java/com/daytodo/domain/user/repository/NotificationRepository.java
Selected eligible course members, filtered by active push settings and tokens, created D-1 and D-0 notifications, and scheduled reminder generation.
Notification dispatch and retry delivery
src/main/java/com/daytodo/domain/user/push/*, src/main/java/com/daytodo/domain/user/service/NotificationDispatchService.java, src/main/java/com/daytodo/domain/user/service/NotificationDeliveryWorker.java
Dispatched pending or failed notifications through Firebase or the disabled gateway. Invalid tokens are removed, and failed deliveries receive retry timestamps.
Endpoint, service, and integration validation
src/test/java/com/daytodo/domain/user/*, src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java, src/test/java/com/daytodo/DayTodoApplicationTests.java, src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java
Added tests for token APIs, reminder creation, delivery outcomes, notification settings, withdrawn-user cleanup, and test-context dependencies.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant ReminderService
  participant NotificationRepository
  participant DeliveryWorker
  participant FirebaseGateway
  Scheduler->>ReminderService: Create daily course reminders
  ReminderService->>NotificationRepository: Save unique pending notifications
  Scheduler->>DeliveryWorker: Dispatch retryable notifications
  DeliveryWorker->>FirebaseGateway: Send notification to FCM token
  FirebaseGateway-->>DeliveryWorker: Return delivery result
  DeliveryWorker->>NotificationRepository: Mark sent or schedule retry
Loading

Possibly related PRs

Suggested reviewers: yxpjseo, hyeoniss

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning WithdrawnUserCleanupRepository includes unrelated bookmark and recommendation cleanup changes beyond issue #45. Move unrelated bookmark and recommendation cleanup changes to a separate pull request, or document their required connection to issue #45.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing course-schedule push notifications.
Linked Issues check ✅ Passed The changes implement the objectives in issue #45, including settings, FCM tokens, D-1/D-Day reminders, delivery handling, deduplication, and tests.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/push-notifications

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (7)
src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java (1)

60-60: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add security tests for the FCM token endpoints.

Add unauthenticated POST and DELETE requests for /users/fcm-token. Expect 401 Unauthorized and the standard error payload.

The controller unit tests set SecurityContextHolder directly. They do not verify the SecurityConfig filter chain for these new state-changing endpoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java` at line
60, Add integration security coverage in TargetApiSecurityTest for
unauthenticated POST and DELETE requests to /users/fcm-token, exercising the
configured filter chain rather than setting SecurityContextHolder directly.
Assert each request returns 401 Unauthorized and the standard error payload,
using the existing fcmTokenService mock where required.
src/test/java/com/daytodo/domain/user/service/CourseReminderServiceTest.java (1)

45-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add D-0 reminder coverage.

This test verifies only COURSE_D1. It stubs the D-0 candidate query as empty, so it cannot detect a regression in createReminders(today, COURSE_D0).

Add a case with a course dated TODAY and verify NotificationType.COURSE_D0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/daytodo/domain/user/service/CourseReminderServiceTest.java`
around lines 45 - 64, Add a separate D-0 test alongside
createsD1ReminderForUserWithDefaultSettingsAndToken using a course dated TODAY,
stub the corresponding candidate query to return its member, and verify
eventCreator.create is called with NotificationType.COURSE_D0.
src/test/java/com/daytodo/domain/user/service/NotificationDeliveryWorkerTest.java (1)

57-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the retry limit.

This test verifies that the first failed delivery gets a retry time. It does not verify the required maximum of five attempts.

Add boundary cases for the final allowed attempt and the next delivery attempt. Verify that the terminal failure does not schedule another retry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/daytodo/domain/user/service/NotificationDeliveryWorkerTest.java`
around lines 57 - 70, Extend removesInvalidTokenAndSchedulesRetry and related
worker tests to cover the retry boundary: verify the fifth failed delivery
remains retryable with a retry time, then verify the sixth delivery becomes
terminal and does not schedule another retry. Assert the delivery status and
nextRetryAt for both boundary cases while preserving the existing invalid-token
removal assertions.
src/main/java/com/daytodo/domain/user/service/CourseNotificationScheduler.java (1)

18-30: 🩺 Stability & Availability | 🔵 Trivial

Plan for multi-instance scheduling and scheduler capacity.

Both jobs run on every application instance. Reminder creation stays idempotent through the duplicate check, and dispatch is serialized by the pessimistic lock, so correctness holds. Throughput and log noise still multiply with instance count.

Two operational points for deployment:

  • If you run more than one instance, gate these jobs with a distributed lock (for example ShedLock) or a leader election.
  • The default Spring scheduler pool holds one thread. The daily job now performs reminder creation and dispatch in sequence, so a slow dispatch delays other scheduled tasks. Size the task scheduler pool explicitly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/daytodo/domain/user/service/CourseNotificationScheduler.java`
around lines 18 - 30, The scheduling configuration around
CourseNotificationScheduler must support multi-instance deployments and avoid
serializing unrelated scheduled work. Configure a distributed lock or
leader-election guard for createDailyReminders and retryFailedNotifications, and
explicitly size the Spring task scheduler pool so dispatch work cannot block
other scheduled tasks.
src/main/java/com/daytodo/domain/course/repository/CourseMemberRepository.java (1)

24-39: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider paginating the reminder candidate query.

findReminderCandidates returns an unbounded list. The daily job loads every joined member of every course on the target date into memory at once, and CourseReminderService.createReminders then builds three additional collections from that list. As course volume grows, this becomes a large single fetch on a scheduled thread.

Add a Pageable parameter and process candidates in slices, or drive the loop with keyset pagination on (course.courseId, user.id), which the existing order by already supports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/daytodo/domain/course/repository/CourseMemberRepository.java`
around lines 24 - 39, Update findReminderCandidates to avoid returning all
reminder candidates in one unbounded fetch by adding pagination support,
preferably a Pageable parameter with a stable page order matching
course.courseId and user.id. Update CourseReminderService.createReminders and
its scheduled processing loop to fetch and process candidates in slices until
exhausted, preserving the existing filtering and reminder behavior.
src/main/java/com/daytodo/domain/user/push/DisabledPushNotificationGateway.java (1)

12-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Disabled Firebase produces five failed delivery attempts per notification.

This gateway always returns a retryable failure. With Firebase disabled, NotificationDispatchService picks each notification up on every cycle until attemptCount reaches 5, and each notification finally persists as FAILED with an error message. Local and CI environments accumulate this state without value.

Consider marking these notifications as terminal instead of retryable. One option is a skipped outcome on PushResult that the worker maps to a non-retryable status. Add a single warn-level log at startup so the disabled state stays visible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/daytodo/domain/user/push/DisabledPushNotificationGateway.java`
around lines 12 - 15, Update DisabledPushNotificationGateway.send and the
PushResult/NotificationDispatchService outcome flow so disabled Firebase
notifications are treated as terminal skipped results rather than retryable
failures, preventing repeated dispatch attempts and FAILED persistence; also add
one warn-level startup log indicating Firebase push notifications are disabled.
src/main/java/com/daytodo/domain/user/repository/NotificationRepository.java (1)

39-41: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Narrow the pessimistic lock to the notification-only delivery path.

findByIdForDelivery() currently lock-fetches notification.user, so the delivery transaction can hold locks on the user row while sending FCM requests. Keep the pessimistic write on the notification result and avoid the fetch join unless the worker needs the user entity before the FCM_TOKEN query; notification.getUser().getId() can be read from the managed FK association.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/daytodo/domain/user/repository/NotificationRepository.java`
around lines 39 - 41, Update findByIdForDelivery in NotificationRepository to
remove the join fetch of notification.user while preserving the
PESSIMISTIC_WRITE lock on the notification result. Ensure delivery can obtain
notification.getUser().getId() from the managed association before the FCM_TOKEN
query without locking or eagerly fetching the user row.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Line 13: Quote the COURSE_REMINDER_CRON value in the dotenv example so the
six-field expression, including its spaces, is preserved when parsed by
shell-compatible dotenv tooling.

In `@build.gradle`:
- Around line 60-61: Update FirebasePushNotificationGateway’s message
construction to replace Message.builder().setToken(...) with the supported
setFid(...) API, preserving the existing recipient value and payload
configuration unless the client contract explicitly requires server-provided FCM
tokens.

In `@src/main/java/com/daytodo/domain/user/entity/FcmToken.java`:
- Line 35: The FcmTokenService.register flow must atomically handle a globally
unique token across different users. Replace the separate token
lookup-and-insert sequence with a database upsert, or catch the
unique-constraint conflict, reload the existing token, and reassign it in a new
transaction while preserving the intended token ownership.

In `@src/main/java/com/daytodo/domain/user/entity/Notification.java`:
- Around line 26-32: Add a checked-in Flyway/Liquibase migration for the
notification schema matching the entity fields, including delivery_status and
attempt_count with appropriate non-null defaults, backfill existing rows
deterministically, resolve duplicate user/course/type records before creating
uk_notification_user_course_type, and add the unique constraint after cleanup.

In `@src/main/java/com/daytodo/domain/user/service/FcmTokenService.java`:
- Around line 33-36: Update the token registration flow in FcmTokenService
around fcmTokenRepository.findByToken so concurrent inserts for the unique token
are handled safely: use a database-native upsert or catch the save unique-key
conflict, re-read the existing FcmToken, and register it for the requested user
and platform. Preserve the existing behavior for tokens already found.

In
`@src/main/java/com/daytodo/domain/user/service/NotificationDeliveryWorker.java`:
- Around line 29-70: Refactor NotificationDeliveryWorker.deliver so the
pessimistic notification lock is held only during short claim and result-update
transactions: load/claim the retryable notification, perform token sends via
pushGateway.send outside any transaction, then persist the final sent/failed
state in a separate transaction. Update FirebaseConfig to configure explicit
connectTimeout and readTimeout for the Firebase HTTP client used by
FirebaseMessaging.send.

In
`@src/main/java/com/daytodo/domain/user/service/NotificationDispatchService.java`:
- Around line 16-34: Remove the class-level `@Transactional`(readOnly = true) from
NotificationDispatchService so dispatchRetryable does not run
deliveryWorker.deliver within one read-only transaction. Ensure
NotificationDeliveryWorker.deliver executes in an independent writable
transaction, using Propagation.REQUIRES_NEW if needed to avoid retaining batch
locks during FCM calls.

In `@src/main/resources/application.yml`:
- Around line 62-64: Update FirebaseConfig startup validation to require a
non-empty resolved firebase.project-id whenever firebase.enabled is true, and
fail initialization immediately when it is missing. Ensure validation occurs
before Firebase initialization or FirebasePushNotificationGateway delivery can
proceed, while leaving disabled Firebase behavior unchanged.

---

Nitpick comments:
In
`@src/main/java/com/daytodo/domain/course/repository/CourseMemberRepository.java`:
- Around line 24-39: Update findReminderCandidates to avoid returning all
reminder candidates in one unbounded fetch by adding pagination support,
preferably a Pageable parameter with a stable page order matching
course.courseId and user.id. Update CourseReminderService.createReminders and
its scheduled processing loop to fetch and process candidates in slices until
exhausted, preserving the existing filtering and reminder behavior.

In
`@src/main/java/com/daytodo/domain/user/push/DisabledPushNotificationGateway.java`:
- Around line 12-15: Update DisabledPushNotificationGateway.send and the
PushResult/NotificationDispatchService outcome flow so disabled Firebase
notifications are treated as terminal skipped results rather than retryable
failures, preventing repeated dispatch attempts and FAILED persistence; also add
one warn-level startup log indicating Firebase push notifications are disabled.

In
`@src/main/java/com/daytodo/domain/user/repository/NotificationRepository.java`:
- Around line 39-41: Update findByIdForDelivery in NotificationRepository to
remove the join fetch of notification.user while preserving the
PESSIMISTIC_WRITE lock on the notification result. Ensure delivery can obtain
notification.getUser().getId() from the managed association before the FCM_TOKEN
query without locking or eagerly fetching the user row.

In
`@src/main/java/com/daytodo/domain/user/service/CourseNotificationScheduler.java`:
- Around line 18-30: The scheduling configuration around
CourseNotificationScheduler must support multi-instance deployments and avoid
serializing unrelated scheduled work. Configure a distributed lock or
leader-election guard for createDailyReminders and retryFailedNotifications, and
explicitly size the Spring task scheduler pool so dispatch work cannot block
other scheduled tasks.

In
`@src/test/java/com/daytodo/domain/user/service/CourseReminderServiceTest.java`:
- Around line 45-64: Add a separate D-0 test alongside
createsD1ReminderForUserWithDefaultSettingsAndToken using a course dated TODAY,
stub the corresponding candidate query to return its member, and verify
eventCreator.create is called with NotificationType.COURSE_D0.

In
`@src/test/java/com/daytodo/domain/user/service/NotificationDeliveryWorkerTest.java`:
- Around line 57-70: Extend removesInvalidTokenAndSchedulesRetry and related
worker tests to cover the retry boundary: verify the fifth failed delivery
remains retryable with a retry time, then verify the sixth delivery becomes
terminal and does not schedule another retry. Assert the delivery status and
nextRetryAt for both boundary cases while preserving the existing invalid-token
removal assertions.

In `@src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java`:
- Line 60: Add integration security coverage in TargetApiSecurityTest for
unauthenticated POST and DELETE requests to /users/fcm-token, exercising the
configured filter chain rather than setting SecurityContextHolder directly.
Assert each request returns 401 Unauthorized and the standard error payload,
using the existing fcmTokenService mock where required.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 325ec330-69c0-4259-bfa7-c34321da01c9

📥 Commits

Reviewing files that changed from the base of the PR and between 13ff3e0 and cc9852d.

📒 Files selected for processing (40)
  • .env.example
  • README.md
  • build.gradle
  • src/main/java/com/daytodo/domain/auth/service/AuthService.java
  • src/main/java/com/daytodo/domain/course/repository/CourseMemberRepository.java
  • src/main/java/com/daytodo/domain/user/controller/UserController.java
  • src/main/java/com/daytodo/domain/user/dto/UserRequest.java
  • src/main/java/com/daytodo/domain/user/dto/UserResponse.java
  • src/main/java/com/daytodo/domain/user/entity/FcmToken.java
  • src/main/java/com/daytodo/domain/user/entity/Notification.java
  • src/main/java/com/daytodo/domain/user/entity/UserNotificationSetting.java
  • src/main/java/com/daytodo/domain/user/enums/DevicePlatform.java
  • src/main/java/com/daytodo/domain/user/enums/NotificationDeliveryStatus.java
  • src/main/java/com/daytodo/domain/user/push/DisabledPushNotificationGateway.java
  • src/main/java/com/daytodo/domain/user/push/FirebasePushNotificationGateway.java
  • src/main/java/com/daytodo/domain/user/push/PushNotificationGateway.java
  • src/main/java/com/daytodo/domain/user/repository/FcmTokenRepository.java
  • src/main/java/com/daytodo/domain/user/repository/NotificationRepository.java
  • src/main/java/com/daytodo/domain/user/repository/UserNotificationSettingRepository.java
  • src/main/java/com/daytodo/domain/user/repository/WithdrawnUserCleanupRepository.java
  • src/main/java/com/daytodo/domain/user/service/CourseNotificationScheduler.java
  • src/main/java/com/daytodo/domain/user/service/CourseReminderService.java
  • src/main/java/com/daytodo/domain/user/service/FcmTokenService.java
  • src/main/java/com/daytodo/domain/user/service/NotificationDeliveryWorker.java
  • src/main/java/com/daytodo/domain/user/service/NotificationDispatchService.java
  • src/main/java/com/daytodo/domain/user/service/NotificationEventCreator.java
  • src/main/java/com/daytodo/domain/user/service/UserNotificationService.java
  • src/main/java/com/daytodo/global/config/FirebaseConfig.java
  • src/main/java/com/daytodo/global/config/SecurityConfig.java
  • src/main/java/com/daytodo/global/config/TimeConfig.java
  • src/main/resources/application.yml
  • src/test/java/com/daytodo/DayTodoApplicationTests.java
  • src/test/java/com/daytodo/domain/course/service/CourseServiceTest.java
  • src/test/java/com/daytodo/domain/user/controller/UserControllerTest.java
  • src/test/java/com/daytodo/domain/user/service/CourseReminderServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/FcmTokenServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/NotificationDeliveryWorkerTest.java
  • src/test/java/com/daytodo/domain/user/service/UserNotificationServiceTest.java
  • src/test/java/com/daytodo/domain/user/service/WithdrawnUserDeletionIntegrationTest.java
  • src/test/java/com/daytodo/global/config/TargetApiSecurityTest.java

Comment thread .env.example
FIREBASE_PROJECT_ID=
GOOGLE_APPLICATION_CREDENTIALS=
NOTIFICATION_ZONE=Asia/Seoul
COURSE_REMINDER_CRON=0 0 9 * * *

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Quote the cron expression.

Line 13 contains spaces. If deployment or local tooling loads this file as a shell-compatible dotenv file, the unquoted assignment is parsed as multiple words. Quote the value to preserve the six-field cron expression.

Proposed fix
-COURSE_REMINDER_CRON=0 0 9 * * *
+COURSE_REMINDER_CRON="0 0 9 * * *"

The supplied dotenv-linter warning also identifies this value.

📝 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
COURSE_REMINDER_CRON=0 0 9 * * *
COURSE_REMINDER_CRON="0 0 9 * * *"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 13-13: [UnorderedKey] The COURSE_REMINDER_CRON key should go before the DB_PW key

(UnorderedKey)


[warning] 13-13: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example at line 13, Quote the COURSE_REMINDER_CRON value in the dotenv
example so the six-field expression, including its spaces, is preserved when
parsed by shell-compatible dotenv tooling.

Source: Linters/SAST tools

Comment thread build.gradle
Comment on lines +60 to +61
// Firebase Cloud Messaging
implementation 'com.google.firebase:firebase-admin:9.10.0'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

file="src/main/java/com/daytodo/domain/user/push/FirebasePushNotificationGateway.java"

if rg -n -C 4 'setToken\(|addToken\(|addAllTokens\(' "$file"; then
  echo "Deprecated token-builder usage found. Verify the gateway implementation."
else
  echo "No deprecated token-builder usage found."
fi

Repository: DayTodo/DayTodo_BE

Length of output: 644


🌐 Web query:

Firebase Admin Java 9.10.0 release notes message token MulticastMessage token deprecations

💡 Result:

The Firebase Admin Java SDK version 9.10.0, released on July 2, 2026, introduces a deprecation of the token-based messaging approach in favor of Firebase Installation IDs (FIDs) [1][2][3]. Key changes in version 9.10.0 include: Deprecation of Token Fields and Methods: - The token field in the Message class is deprecated [1]. - The tokens field in the MulticastMessage class is deprecated [1]. - The setToken method in Message.Builder is deprecated [1]. - The addToken and addAllTokens methods in MulticastMessage.Builder are deprecated [1]. Introduction of FID Support: - The SDK now supports sending messages using Firebase Installation IDs (FIDs) [1]. - New fields have been added: the fid field in the Message class and the fids field in the MulticastMessage class [1]. - New methods have been introduced: setFid in Message.Builder, and addFid and addAllFids in MulticastMessage.Builder [1]. While the MulticastMessage class continues to support both tokens and FIDs, developers are advised to migrate to the new FID-based fields, as the token-based fields are now officially deprecated [1]. [1][2]

Citations:


Replace the Message.setToken() call with the supported FID API.

FirebasePushNotificationGateway still uses Message.builder().setToken(...), which is deprecated in Firebase Admin Java 9.10.0. Use Message.builder().setFid(...) unless the client contract already requires server-provided FCM tokens.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.gradle` around lines 60 - 61, Update FirebasePushNotificationGateway’s
message construction to replace Message.builder().setToken(...) with the
supported setFid(...) API, preserving the existing recipient value and payload
configuration unless the client contract explicitly requires server-provided FCM
tokens.

@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(name = "token", nullable = false, unique = true, length = 512)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make token registration atomic across users.

Line 35 enforces global token uniqueness. FcmTokenService.register first reads by token and then inserts. Its user-row lock does not serialize requests for different users. Two concurrent registrations of the same new token can both see no row, and one request will fail with a unique-constraint error.

Use a database upsert, or handle the constraint conflict by reloading and reassigning the token in a new transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/daytodo/domain/user/entity/FcmToken.java` at line 35, The
FcmTokenService.register flow must atomically handle a globally unique token
across different users. Replace the separate token lookup-and-insert sequence
with a database upsert, or catch the unique-constraint conflict, reload the
existing token, and reassign it in a new transaction while preserving the
intended token ownership.

Comment on lines +26 to +32
@Table(
name = "notification",
uniqueConstraints = @UniqueConstraint(
name = "uk_notification_user_course_type",
columnNames = {"user_id", "course_id", "notification_type"}
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate schema-management configuration and migrations.
fd -t f -i '(flyway|liquibase|migration)|^V[0-9].*__.*\.(sql|xml|yaml|yml)$' .
rg -n -i -C 3 \
  'notification|delivery_status|attempt_count|uk_notification_user_course_type|flyway|liquibase|ddl-auto' \
  src

Repository: DayTodo/DayTodo_BE

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Relevant files:\n'
fd -t f 'Notification.java|application.yml|pom.xml|build.gradle|settings.gradle|flyway|liquibase|migration.sql|M[0-9]|db|schema' .

printf '\nNotification entity:\n'
cat -n src/main/java/com/daytodo/domain/user/entity/Notification.java

printf '\nApplication schema config:\n'
cat -n src/main/resources/application.yml | sed -n '1,80p'

printf '\nBuild schema-management deps:\n'
if [ -f pom.xml ]; then
  rg -n -C 2 '(flyway|liquibase|maven-fmp|spring-boot-maven-plugin)' pom.xml
fi

printf '\nFind migration scripts only:\n'
fd -t f 'migration|\.sql$|flyway|liquibase' . | sed -n '1,120p'

printf '\nSearch db-related tables/corresponding scripts:\n'
rg -n -i -C 2 'create table.*notification|alter table.*notification|notification_table|delivery_status|attempt_count|uk_notification_user_course_type|unique constraint|constraint' . \
  -g '*.sql' -g '*db*' -g 'src/main/resources/**' | sed -n '1,220p'

Repository: DayTodo/DayTodo_BE

Length of output: 7173


Add a migration for the notification schema changes.

The live profile uses hibernate.ddl-auto: update, but there are no checked-in Flyway/Liquibase migrations for delivery_status, attempt_count, or uk_notification_user_course_type. Existing rows need backfill values for non-null columns and deterministic duplicate-resolution before the unique constraint is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/daytodo/domain/user/entity/Notification.java` around lines
26 - 32, Add a checked-in Flyway/Liquibase migration for the notification schema
matching the entity fields, including delivery_status and attempt_count with
appropriate non-null defaults, backfill existing rows deterministically, resolve
duplicate user/course/type records before creating
uk_notification_user_course_type, and add the unique constraint after cleanup.

Comment thread src/main/java/com/daytodo/domain/user/service/FcmTokenService.java Outdated
Comment on lines +29 to +70
@Transactional
public void deliver(Long notificationId) {
Notification notification = notificationRepository.findByIdForDelivery(notificationId)
.orElse(null);
if (!isRetryable(notification)) {
return;
}

var tokens = fcmTokenRepository.findAllByUser_Id(notification.getUser().getId());
if (tokens.isEmpty()) {
notification.markFailed("등록된 FCM 토큰이 없습니다.", nextRetryAt(notification));
return;
}

boolean sent = false;
var errors = new ArrayList<String>();
for (FcmToken token : tokens) {
PushNotificationGateway.PushResult result = pushGateway.send(
token.getToken(),
notification.getTitle(),
notification.getBody(),
Map.of(
"courseId", String.valueOf(notification.getCourseId()),
"notificationType", notification.getNotificationType().name()
)
);
if (result.success()) {
sent = true;
} else {
if (result.invalidToken()) {
fcmTokenRepository.delete(token);
}
errors.add(result.errorMessage() == null ? "FCM 발송 실패" : result.errorMessage());
}
}

if (sent) {
notification.markSent(LocalDateTime.now(clock));
} else {
notification.markFailed(String.join(" | ", errors), nextRetryAt(notification));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the lastError column definition and any Firebase transport timeout configuration.
set -euo pipefail

fd -t f 'Notification.java' --exec rg -n -C 3 'lastError|columnDefinition|`@Column`' {}
fd -t f 'FirebaseConfig.java' --exec cat -n {}

Repository: DayTodo/DayTodo_BE

Length of output: 3613


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -t f 'NotificationDeliveryWorker.java|FirebasePushNotificationGateway.java|PushNotificationGateway.java|Repository.java|NotificationRepository.java' .

echo
echo "== NotificationDeliveryWorker =="
file="$(fd -t f 'NotificationDeliveryWorker.java' . | head -n 1)"
sed -n '1,140p' "$file" | cat -n

echo
echo "== Gateway files =="
for f in $(fd -t f 'FirebasePushNotificationGateway.java|PushNotificationGateway.java' .); do
  echo "--- $f"
  sed -n '1,180p' "$f" | cat -n
done

echo
echo "== repository methods =="
for f in $(fd -t f 'Repository.java|NotificationRepository.java' src/main/java | head -n 20); do
  if rg -q "findByIdForDelivery|PessimisticLock|PESSIMISTIC" "$f"; then
    echo "--- $f"
    rg -n -C 4 "findByIdForDelivery|`@Lock`|`@Query`|SetSessionTimeout|`@Modifying`|`@Transactional`" "$f"
  fi
done

echo
echo "== dependency versions mentioning firebase/http client (from lockfiles/pom/build scripts) =="
for f in pom.xml build.gradle build.gradle.kts; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n -C 2 'firebase|httpclient|okhttp|firebase-admin|google-messaging|spring-boot' "$f" || true
  fi
done

Repository: DayTodo/DayTodo_BE

Length of output: 14937


🌐 Web query:

Google Firebase Admin Java Messaging send synchronously send method timeout FirebaseMessagingOptions default sendTimeout AsyncMessaging transport

💡 Result:

In the Firebase Admin SDK for Java, the synchronous send method does not have a built-in, configurable timeout parameter directly on the method call [1][2]. Instead, the SDK relies on timeout settings configured globally via FirebaseOptions during the initialization of your FirebaseApp instance [3][4]. Configuration of Timeouts You can set connection, read, and write timeouts in milliseconds using the FirebaseOptions.Builder [3][5]: FirebaseOptions options = FirebaseOptions.builder.setCredentials(GoogleCredentials.getApplicationDefault).setConnectTimeout(10000) // 10 seconds.setReadTimeout(10000) // 10 seconds.setWriteTimeout(10000) // 10 seconds.build; FirebaseApp.initializeApp(options); Important Considerations: 1. Default Value is Zero: By default, these timeouts are initialized to 0, which signifies an infinite timeout [6][3]. In scenarios where the FCM server or network becomes unresponsive, this can lead to threads hanging indefinitely [6][7]. It is highly recommended to explicitly set non-zero, meaningful values [6]. 2. Scope of Settings: These timeout settings apply to most outgoing REST calls made by the SDK, but they do not affect Firebase Realtime Database or Cloud Firestore APIs [4][5]. 3. Asynchronous Alternative: If you need to enforce a specific timeout for a single request rather than a global configuration, use the sendAsync method [1][2]. This returns an ApiFuture, which allows you to call the.get(long timeout, TimeUnit unit) method to wait for the result with a specific deadline [8]: try { ApiFuture future = FirebaseMessaging.getInstance.sendAsync(message); String messageId = future.get(5, TimeUnit.SECONDS); // Wait max 5 seconds } catch (TimeoutException e) { // Handle specific request timeout } 4. Credential Resolution: Note that initial token fetching via GoogleCredentials may not strictly honor these FirebaseOptions timeouts in all edge cases, as some underlying credential libraries have their own retry logic [9]. For best results in production, ensure your infrastructure layer also enforces its own timeouts where appropriate [10].

Citations:


Shorten the transaction that holds the Notification lock.

deliver opens a transaction, locks notification with PESSIMISTIC_WRITE, then calls synchronous FirebaseMessaging.send inside that transaction. A slow or unresponsive FCM HTTP call keeps the lock and database connection held. Split this into short claim/update transactions and call pushGateway.send between them. Also set explicit Firebase connectTimeout and readTimeout in FirebaseConfig; the current FirebaseMessaging.send path does not provide per-call timeouts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/daytodo/domain/user/service/NotificationDeliveryWorker.java`
around lines 29 - 70, Refactor NotificationDeliveryWorker.deliver so the
pessimistic notification lock is held only during short claim and result-update
transactions: load/claim the retryable notification, perform token sends via
pushGateway.send outside any transaction, then persist the final sent/failed
state in a separate transaction. Update FirebaseConfig to configure explicit
connectTimeout and readTimeout for the Firebase HTTP client used by
FirebaseMessaging.send.

Comment on lines +62 to +64
firebase:
enabled: ${FIREBASE_ENABLED:false}
project-id: ${FIREBASE_PROJECT_ID:}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline src/main/java/com/daytodo/global/config/FirebaseConfig.java --items all

rg -n -C 6 \
  'firebase\.enabled|project-id|GOOGLE_APPLICATION_CREDENTIALS|FirebaseOptions|GoogleCredentials|FirebaseApp|DisabledPushNotificationGateway' \
  src/main/java/com/daytodo/global/config/FirebaseConfig.java \
  src/main/java/com/daytodo/domain/user/push \
  src/main/resources/application.yml \
  .env.example

Repository: DayTodo/DayTodo_BE

Length of output: 6777


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\nPush notification gateway implementations:\n'
fd -a '.*\.java' src/main/java/com/daytodo/domain/user/push | sort

printf '\nPush config/bean wiring references:\n'
rg -n -C 5 \
  'PushNotificationGateway|PushResult|firebaseMessaging|FirebaseMessaging|DisabledPushNotificationGateway|FirebaseConfig' \
  src/main/java || true

printf '\nFirebase config file with line numbers:\n'
cat -n src/main/java/com/daytodo/global/config/FirebaseConfig.java

printf '\nApplication config and env example:\n'
cat -n src/main/resources/application.yml | sed -n '55,70p'
cat -n .env.example | sed -n '1,16p'

Repository: DayTodo/DayTodo_BE

Length of output: 18062


🌐 Web query:

FirebaseAdmin Java FirebaseOptions projectId optional setProjectId null service account credentials get application default

💡 Result:

In the Firebase Admin SDK for Java, the projectId field within FirebaseOptions is technically optional when building your options, but your application must still be able to resolve a project ID for many Firebase features to work correctly [1][2]. If you do not explicitly call setProjectId in your FirebaseOptions.Builder, the FirebaseApp will attempt to resolve the project ID automatically during initialization [2]. It follows this lookup order [2]: 1. User-provided options: If setProjectId was called, that value is used [2]. 2. Service Account Credentials: If you provided ServiceAccountCredentials via setCredentials, the SDK attempts to extract the project ID from those credentials [2]. 3. Environment Variables: The SDK checks for the GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT environment variables [2]. When using GoogleCredentials.getApplicationDefault, if the project ID is not explicitly set in FirebaseOptions, it may not be automatically resolved unless it is present in the environment or the underlying credentials provide it [2]. While you can omit setProjectId, it is considered best practice to set it explicitly if your environment does not guarantee the presence of these specific environment variables or if you want to avoid ambiguity [3][2]. Note that while projectId is optional, setCredentials is mandatory [4]. You must initialize your FirebaseOptions with a valid credentials object (such as GoogleCredentials.getApplicationDefault) or the SDK will throw an exception [4][5]. Top results: [3][1][2][4]

Citations:


Validate required Firebase project ID at startup.

With firebase.enabled=true and an empty firebase.project-id, FirebaseConfig can initialize Firebase without calling setProjectId. Add an explicit startup check that fails fast when project ID resolution is missing, so deliveries do not go through FirebasePushNotificationGateway and arrive in the retry/failure path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/application.yml` around lines 62 - 64, Update
FirebaseConfig startup validation to require a non-empty resolved
firebase.project-id whenever firebase.enabled is true, and fail initialization
immediately when it is missing. Ensure validation occurs before Firebase
initialization or FirebasePushNotificationGateway delivery can proceed, while
leaving disabled Firebase behavior unchanged.

@nariming nariming left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

수고하셨습니다!!

코드래빗도 이 부분 짚었는데, NotificationDispatchService의 @transactional(readOnly = true) 확인 부탁드립니다! 안에서 호출되는 deliver()가 상태를 변경하는데 readOnly라 flush가 안 될 수 있어서 알림 재발송으로 이어질 수도 있을 것 같아요

@kwonwnsduf
kwonwnsduf force-pushed the feature/push-notifications branch from cc9852d to 1dad57c Compare August 6, 2026 14:14

@nariming nariming left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

수고하셨습니다!!!

@kwonwnsduf
kwonwnsduf merged commit dd51338 into develop Aug 6, 2026
1 check passed
@kwonwnsduf
kwonwnsduf deleted the feature/push-notifications branch August 6, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능을 추가하는 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 알림 설정 및 D-1, D-Day 알림 전송 기능 구현

2 participants