알림 히스토리 삭제 기능 (단건·다건·전체·N일 자동삭제)#752
Conversation
- DELETE /api/v1/notifications 를 읽음(POST /read) 계약과 대칭으로 추가 — body {all XOR ids} 로 단건([id])·다건([id,...])·전체(all=true)를 한 엔드포인트가 처리. 요청 DTO 는 @AssertTrue XOR 검증·toCommand() 를 read DTO 에서 복제
- 삭제는 하드삭제·본인 소유만·멱등(타인/없는 id 무영향). 삭제로 안읽음이 사라지면 badge 도 줄도록 read flow(readAndSyncBadge)와 대칭인 NotificationDeleteOrchestrator 로 삭제 후 재집계 + SSE silent-sync + FCM syncBadge. 전체 삭제는 기존 hardDeleteAllByUserId(탈퇴 cascade) 재사용
- N일 자동삭제: NotificationCleanupScheduler(@scheduled cron, 매일 04:00)가 보존기간(notification.retention-days=30, env 오버라이드) 넘긴 알림을 하드삭제. 로직은 NotificationRetentionService.purgeOlderThan(cutoff)로 분리해 결정적 테스트 가능. admin 무관 코어라 AnnouncementScheduler 와 달리 ConditionalOnAdminEnabled 안 붙임
- created_at 단일 인덱스(idx_notifications_created_at) 마이그레이션 추가 — age 기준 전역 삭제가 풀스캔하지 않게. deleted_at 컬럼은 이미 있으나 하드삭제라 미사용
- 멀쩡한 클라가 받는 응답 전수 문서화(200/400/401): 어노테이션+example 동반, fail detail 은 DTO const single-source
- NotificationDeleteRequestTest(단위): all XOR ids 검증·toCommand 분기 망라 - NotificationDeleteIntegrationTest(통합): 단건·다건·전체 삭제, 타인 무영향(소유 검증), 멱등, 400(all·ids 동시/빈 ids), 401 contract - NotificationRetentionIntegrationTest(통합): cutoff < 경계(이후 생성분만 삭제, 최근 분 보존) - all=true(hardDeleteAllByUserId)는 벌크 삭제 후 컨텍스트를 clear 하지 않아 findById 가 1차 캐시 stale 을 돌려주므로, 삭제 검증은 DB 를 치는 existsById 로 확인
|
Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough알림 삭제 API가 추가되어 단건·다건·전체 하드삭제와 소유권 검증, unread 배지 재계산 및 동기화를 지원한다. 보존 기간 기반 자동 삭제 스케줄러와 Changes알림 삭제 및 자동 정리
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant NotificationHistoryController
participant NotificationDeleteOrchestrator
participant NotificationService
participant NotificationRepository
participant SilentSyncDispatcher
participant PushNotificationChannel
Client->>NotificationHistoryController: DELETE /api/v1/notifications
NotificationHistoryController->>NotificationDeleteOrchestrator: deleteAndSyncBadge(userId, command)
NotificationDeleteOrchestrator->>NotificationService: delete(userId, command)
NotificationService->>NotificationRepository: 사용자 소유 알림 하드삭제
NotificationRepository-->>NotificationService: 삭제 건수
NotificationService-->>NotificationDeleteOrchestrator: 카테고리별 unread count
NotificationDeleteOrchestrator->>SilentSyncDispatcher: UnreadCountChanged dispatch
NotificationDeleteOrchestrator->>PushNotificationChannel: OS badge 동기화
NotificationDeleteOrchestrator-->>NotificationHistoryController: unread count map
NotificationHistoryController-->>Client: NotificationDeleteResponse
Assessment against linked issues
🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (1 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
- 이 앱의 JVM 기본 TZ 는 UTC 라(로그 표시만 Asia/Seoul 고정), @scheduled 에 zone 을 안 주면 04:00 이 UTC 로 해석돼 13:00 KST 에 돌았다. 의도한 새벽 4시가 아니고 다른 cron 스케줄러(DailyActivityRecorder·WeeklyReportScheduler)의 zone="Asia/Seoul" 관례와도 어긋난다 - @scheduled 에 zone="Asia/Seoul" 을 명시해 매일 04:00 KST 로 고정. cutoff 계산(now-retentionDays)은 created_at 과 같은 JVM TZ 라 그대로 정확 — 실행 시각만 KST 로 옮긴다
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/kotlin/com/depromeet/piki/notification/config/NotificationProperties.kt`:
- Around line 10-12: NotificationProperties의 retentionDays에 양수 검증을 추가해 0 이하 값이
설정되지 않도록 하세요. 바인딩 검증 방식을 사용할 경우 클래스에 `@Validated를` 적용하고 retentionDays에
`@field`:Positive를 지정하며, 생성자 검증을 사용할 경우 retentionDays > 0을 require로 보장하세요.
In
`@src/main/kotlin/com/depromeet/piki/notification/controller/dto/NotificationDeleteRequest.kt`:
- Around line 14-15: Enforce a maximum number of IDs for
NotificationDeleteRequest.ids by adding bean validation with a defined MAX_IDS
limit, while allowing null and valid-size lists. Ensure requests containing
MAX_IDS + 1 IDs fail validation and return HTTP 400, and verify the relevant
server or gateway JSON body-size limit is configured.
In
`@src/main/kotlin/com/depromeet/piki/notification/service/NotificationRetentionService.kt`:
- Around line 18-22: Update NotificationRetentionService.purgeOlderThan to
collect affected userId values before the bulk delete, then after transaction
commit asynchronously recalculate each user’s unread count and publish
UnreadCountChanged with the corresponding REST/SSE/FCM badge synchronization,
matching NotificationDeleteOrchestrator behavior; do not leave stale counts
after removing unread notifications.
In
`@src/main/kotlin/com/depromeet/piki/notification/service/NotificationService.kt`:
- Around line 89-95: Update the repository method used by the
NotificationDeleteCommand.All path, hardDeleteAllByUserId, to configure its
`@Modifying` annotation with flushAutomatically = true and clearAutomatically =
true. Keep the existing deletion behavior unchanged and align it with the
ID-based bulk deletion method so the persistence context is flushed and cleared.
In
`@src/test/kotlin/com/depromeet/piki/notification/controller/NotificationDeleteIntegrationTest.kt`:
- Around line 60-79: 현재 통합 테스트는 응답의 unread count만 검증하므로 삭제 후 동기화 호출과 전달 값의 회귀를
확인하도록 보강합니다. NotificationDeleteOrchestrator 단위 테스트에서 알림 삭제 결과와 동일한 unread map을
기준으로 SilentSyncDispatcher와 PushNotificationChannel이 모두 호출되는지 검증하고, 각 채널에 전달된 값이
카테고리별 unread count와 일치하는지 확인하세요.
In
`@src/test/kotlin/com/depromeet/piki/notification/service/NotificationRetentionIntegrationTest.kt`:
- Around line 52-62: Update the test `cutoff 이전 알림은 남긴다...` to seed a
notification whose createdAt exactly equals the cutoff, using the existing
`seed` fixture or its underlying persistence setup. Assert that
`purgeOlderThan(cutoff)` returns 0 and that the exact-cutoff notification still
exists, preserving the test’s intended strict-boundary validation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0f6617b3-4370-4bec-978b-cf3c0a8f747e
📒 Files selected for processing (19)
src/main/kotlin/com/depromeet/piki/notification/config/NotificationProperties.ktsrc/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApi.ktsrc/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryApiExamples.ktsrc/main/kotlin/com/depromeet/piki/notification/controller/NotificationHistoryController.ktsrc/main/kotlin/com/depromeet/piki/notification/controller/dto/NotificationDeleteRequest.ktsrc/main/kotlin/com/depromeet/piki/notification/controller/dto/NotificationDeleteResponse.ktsrc/main/kotlin/com/depromeet/piki/notification/repository/NotificationJpaRepository.ktsrc/main/kotlin/com/depromeet/piki/notification/repository/NotificationRepository.ktsrc/main/kotlin/com/depromeet/piki/notification/repository/NotificationRepositoryImpl.ktsrc/main/kotlin/com/depromeet/piki/notification/service/NotificationCleanupScheduler.ktsrc/main/kotlin/com/depromeet/piki/notification/service/NotificationDeleteOrchestrator.ktsrc/main/kotlin/com/depromeet/piki/notification/service/NotificationRetentionService.ktsrc/main/kotlin/com/depromeet/piki/notification/service/NotificationService.ktsrc/main/kotlin/com/depromeet/piki/notification/service/dto/NotificationDeleteCommand.ktsrc/main/resources/application.ymlsrc/main/resources/db/migration/V20260715224011__add_index_on_notifications_created_at.sqlsrc/test/kotlin/com/depromeet/piki/notification/controller/NotificationDeleteIntegrationTest.ktsrc/test/kotlin/com/depromeet/piki/notification/controller/dto/NotificationDeleteRequestTest.ktsrc/test/kotlin/com/depromeet/piki/notification/service/NotificationRetentionIntegrationTest.kt
- N일 자동삭제가 안읽음 알림을 지우고도 배지 동기화를 안 해, 삭제된 unread 가 REST unreadCount·SSE·FCM 배지에 stale 로 남던 문제 수정. 단건·모두 삭제는 이미 배지를 쐈으므로 자동삭제만 빠진 비대칭이었다. 삭제 전 cutoff 미만 안읽음 보유 유저를 모아, 커밋 후 유저별로 SSE silent-sync + FCM 배지를 쏜다(NotificationDeleteOrchestrator 와 동일한 tx 밖 @async 방식). 읽음만 지워진 유저는 배지가 안 변하므로 대상에서 제외 - retentionDays 양수 검증 추가 — 잘못 구성한 env(0·음수)면 cutoff 가 현재/미래가 되어 알림을 대량 삭제할 수 있어, 불변식으로 부팅 시점에 require 로 막는다(ops 오설정이라 커스텀 예외가 아니라 require) - 전체 삭제(hardDeleteAllByUserId) 벌크 DELETE 에 flushAutomatically·clearAutomatically 추가 — deleteByUserIdAndIds·markRead 와 일관되게 맞춰, 같은 트랜잭션의 이후 조회가 1차 캐시 stale 과 어긋나지 않게 한다. 탈퇴 cascade 호출부는 삭제 후 log 만 있어 무해
- 자동삭제로 안읽음이 사라진 유저만 배지 재계산 대상에 담고, 읽음만 지워진 유저는 제외하는지 검증 추가(purgeOlderThan 반환 맵) - created_at == cutoff 인 알림을 native SQL 로 심어 삭제되지 않음을 검증 — 쿼리가 < 에서 <= 로 바뀌는 경계 회귀를 잡는다(기존 테스트는 cutoff 에서 멀어 못 잡았다) - 전체삭제 벌크에 clearAutomatically 가 붙어 findById 도 삭제 결과를 정확히 반영하므로, existsById 우회를 다시 findById 로 되돌림
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/test/kotlin/com/depromeet/piki/notification/service/NotificationRetentionIntegrationTest.kt (1)
68-89: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win배지 동기화 호출까지 검증하는 테스트가 필요합니다.
현재 테스트는
affectedUnreadByUser가 올바르게 계산되는지만 확인합니다.NotificationCleanupScheduler에서 사용자 ID,UnreadCountChanged, FCM badge 값이 실제로 전달되는지는 검증하지 않으므로 wiring이 회귀해도 테스트가 통과할 수 있습니다.별도 스케줄러 테스트가 없다면 영향 사용자에 대해 SSE·FCM이 각각 정확한 payload로 한 번씩 호출되고, 읽음 알림만 삭제된 사용자는 호출되지 않는지 추가로 검증해주세요.
As per path instructions, 핵심 비즈니스 규칙과 회귀를 잡는 테스트를 우선 검토해야 합니다.
🤖 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/kotlin/com/depromeet/piki/notification/service/NotificationRetentionIntegrationTest.kt` around lines 68 - 89, NotificationRetentionIntegrationTest는 affectedUnreadByUser 계산만 검증하므로 NotificationCleanupScheduler의 배지 동기화 wiring도 검증하도록 테스트를 추가하세요. 영향 사용자 userA에 대해 SSE와 FCM이 각각 정확히 한 번 호출되고 사용자 ID, UnreadCountChanged, FCM badge 값이 기대 payload로 전달되는지 확인하세요. 읽음 알림만 삭제된 userB에는 어떤 동기화 호출도 발생하지 않는지 검증하고, purgeOlderThan 결과와 기존 삭제 규칙도 유지하세요.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/kotlin/com/depromeet/piki/notification/service/NotificationRetentionService.kt`:
- Around line 24-26: NotificationRetentionService의 associateWith 및 사용자별
countUnreadByCategory 호출을 제거하고, notificationRepository에 affectedUsers 전체의
userId와 type/category별 unread 수를 GROUP BY로 한 번에 조회하는 메서드를 추가해 사용하세요. 조회 결과를
서비스에서 사용자별 집계 구조로 변환하고, 집계 결과가 없는 영향 사용자도 각 category/type을 0으로 보정하여 기존
affectedUnreadByUser 계약을 유지하세요.
---
Nitpick comments:
In
`@src/test/kotlin/com/depromeet/piki/notification/service/NotificationRetentionIntegrationTest.kt`:
- Around line 68-89: NotificationRetentionIntegrationTest는 affectedUnreadByUser
계산만 검증하므로 NotificationCleanupScheduler의 배지 동기화 wiring도 검증하도록 테스트를 추가하세요. 영향 사용자
userA에 대해 SSE와 FCM이 각각 정확히 한 번 호출되고 사용자 ID, UnreadCountChanged, FCM badge 값이 기대
payload로 전달되는지 확인하세요. 읽음 알림만 삭제된 userB에는 어떤 동기화 호출도 발생하지 않는지 검증하고,
purgeOlderThan 결과와 기존 삭제 규칙도 유지하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: ea550adb-9a06-49f4-b5de-44916b740368
📒 Files selected for processing (9)
src/main/kotlin/com/depromeet/piki/notification/config/NotificationProperties.ktsrc/main/kotlin/com/depromeet/piki/notification/repository/NotificationJpaRepository.ktsrc/main/kotlin/com/depromeet/piki/notification/repository/NotificationRepository.ktsrc/main/kotlin/com/depromeet/piki/notification/repository/NotificationRepositoryImpl.ktsrc/main/kotlin/com/depromeet/piki/notification/service/NotificationCleanupScheduler.ktsrc/main/kotlin/com/depromeet/piki/notification/service/NotificationRetentionService.ktsrc/main/kotlin/com/depromeet/piki/notification/service/dto/NotificationPurgeResult.ktsrc/test/kotlin/com/depromeet/piki/notification/controller/NotificationDeleteIntegrationTest.ktsrc/test/kotlin/com/depromeet/piki/notification/service/NotificationRetentionIntegrationTest.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- src/main/kotlin/com/depromeet/piki/notification/config/NotificationProperties.kt
- src/main/kotlin/com/depromeet/piki/notification/repository/NotificationRepositoryImpl.kt
- src/main/kotlin/com/depromeet/piki/notification/repository/NotificationJpaRepository.kt
- src/test/kotlin/com/depromeet/piki/notification/controller/NotificationDeleteIntegrationTest.kt
- purgeOlderThan 이 affectedUsers 를 돌며 유저마다 countUnreadByCategory 를 호출해, 보존 만료 알림이 많은 날 유저 수만큼 쿼리가 나가 트랜잭션이 길어지던 문제 수정 - countUnreadByCategoryForUsers(userIds) 를 추가 — userId·type 을 IN + GROUP BY 로 한 번에 집계한다. 안읽음 0 인 유저는 결과 행에 없으므로 대상 유저 전원을 모든 카테고리 0 으로 깔고 덮어, 모든 대상 유저 키를 보장하는 기존 계약을 유지한다
- 5c5f6d8 에서 countUnreadByCategoryForUsers(IN + GROUP BY) 를 추가했으나 git add 를 repository 디렉토리로만 해, 정작 호출부(NotificationRetentionService.purgeOlderThan)의 associateWith N+1 루프 → 배치 호출 교체가 커밋에서 빠졌다. 새 메서드가 dead code 로 남고 실제 경로엔 N+1 이 그대로였던 것을 CodeRabbit 리뷰가 잡았다 - purgeOlderThan 이 affectedUsers.associateWith { countUnreadByCategory(it) } 대신 countUnreadByCategoryForUsers(affectedUsers) 를 호출하도록 연결. affectedUsers 가 비면 배치 메서드가 emptyMap 을 반환해 별도 분기도 불필요
- 기존 retention 테스트는 purgeOlderThan 의 affectedUnreadByUser 계산만 검증해, 스케줄러가 실제로 SSE·FCM 배지를 쏘는 wiring 이 회귀해도 통과하는 사각이 있었다(CodeRabbit nitpick) - 실 빈 + 실제 커밋 + Awaitility 로 cleanup() 실행 후, 안읽음이 지워진 userA 에겐 SSE(UnreadCountChanged unreadCount=1)·FCM(badge=1)이 전파되고, 읽음만 지워진 userB 에겐 어느 쪽도 호출되지 않음을 검증. 내부 협력자 mock 없이 side effect 로 확인(규약 준수, NotificationBadgeSyncAsyncIntegrationTest 와 동일 결)
|
@coderabbitai review body 의 nitpick(스케줄러 배지 fan-out wiring 검증 테스트)을 반영했습니다 — |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
git fetch origin pull/752/head:pr752 2>&1 | tail -5
git checkout pr752 2>&1 | tail -5
fd NotificationCleanupSchedulerIntegrationTest
git log --oneline -5 a084faf 2>&1 || trueLength of output: 827 🏁 Script executed: #!/bin/bash
cat -n src/test/kotlin/com/depromeet/piki/notification/service/NotificationCleanupSchedulerIntegrationTest.ktLength of output: 6928 This test looks solid — it validates the exact fan-out wiring concern with real commits and Awaitility, without mocking internal collaborators, staying true to the project's testing conventions.
한 가지만 짚어드리면, 작은 참고: 전체적으로 이슈가 잘 해소됐다고 판단합니다. 좋은 작업이었어요! (=^・ω・^=) ノ彡☆ |
- 자동삭제 첫 실행은 30일 넘긴 백로그가 수백~수천 유저에 몰릴 수 있는데, 스케줄러가 유저당 @async 태스크(유저×2: SSE+FCM)를 한꺼번에 던져 notificationExecutor(pool 4·queue 200·포화 시 drop)를 넘겼다. 500명 실측 시 FCM 배지 전달률이 21.6%(392명 유실)였고, 같은 실행기를 쓰는 실시간 알림까지 굶길 수 있었다 - NotificationRetentionBadgeNotifier(단일 @async 배치)를 추가해 영향 유저를 한 워커에서 순차 전파한다. 어느 순간에도 실행기 점유 태스크가 1개뿐이라 drop 0, 실시간 알림 보존. 500명 재실측 전달률 100% - 이를 위해 PushNotificationChannel 에 동기 syncBadgeBlocking 을 분리(@async syncBadge 가 위임)하고, SSE 는 LocalSseDelivery.deliverSilentSync 를 직접 호출. 단건·모두 삭제(NotificationDeleteOrchestrator)는 영향 유저 1명이라 기존 유저당 @async 를 유지한다
Situation
POST /read {all:true})만 이미 있고 삭제 계열 3개가 통째로 미구현이었다.Task
all XOR ids계약을 그대로 미러링).Action
삭제 엔드포인트 (단건·다건·모두)
DELETE /api/v1/notifications하나로 세 가지를 처리한다. body 는 읽음과 똑같이 둘 중 정확히 하나:all=trueids=[id]ids=[id, ...]deleted_atsoft delete 컬럼이 이미 있으나, 히스토리에서 영구 제거가 목적이라 쓰지 않는다.user_id조건이 소유 검증을 겸한다. 타인 id나 없는 id를 섞어 보내도 그 부분만 무영향으로 걸러져 예외 없이 성공한다 (별도 소유 검증 예외를 두지 않은 이유).ids개수 상한도 두지 않는다: 대량은all=true가 담당하고, 본인 한정·멱등·본문 크기 상한으로 bounded (읽음 DTO의 무-cap 결정 미러링).hardDeleteAllByUserId와 동일 쿼리라 그대로 가져다 썼다. 이 벌크 DELETE에는flushAutomatically·clearAutomatically를 붙여, 같은 트랜잭션의 이후 조회가 1차 캐시 stale과 어긋나지 않게deleteByUserIdAndIds·markRead와 일관을 맞췄다.all·ids동시 전송 / 둘 다 없음 / 빈ids는 입력 경계에서 400. 읽음 DTO의@AssertTrueXOR 검증·toCommand()를 복제했다.뱃지 동기화 (모든 삭제 경로 공통, 읽음과 대칭)
삭제 후 안읽음 수를 다시 세어, 읽음 흐름과 완전히 같은 방식으로 여러 기기의 뱃지를 맞춘다. 단건·다건·모두 삭제뿐 아니라 자동삭제도 이 경로를 태운다.
두 동기화는 모두 비동기라 삭제 응답(스케줄 스레드)이 SSE write·FCM latency에 묶이지 않는다.
N일 자동삭제
zone="Asia/Seoul"을 명시하지 않으면 13:00 KST에 도는 함정이 있어, 다른 cron 스케줄러들과 동일하게 KST로 고정했다. 보존 기간은notification.retention-days(기본 30, env 오버라이드)로 두고 하드코딩하지 않는다. 0·음수 오설정은 cutoff를 현재/미래로 만들어 대량 삭제로 이어지므로, 불변식(require)으로 부팅 시점에 막는다.@Async(유저×2: SSE+FCM)를 한꺼번에 던지면 공유 실행기(pool 4·queue 200·포화 시 drop)를 넘겨 배지가 대량 유실되고 실시간 알림까지 굶는다 (500명 실측 FCM 전달률 21.6%). 그래서 fan-out을 단일@Async배치가 순차 전파하도록 해 어느 순간에도 실행기 점유 태스크가 1개뿐이게 했다 (500명 재실측 100%, 실시간 알림 비굶김). 유저별 재집계도 N+1 대신IN+GROUP BY한 쿼리라 유저 수에 비례하지 않는다. (단건·다건·모두 삭제는 영향 유저 1명이라 기존 유저당@Async유지.)created_at < cutoff)와 트랜잭션 경계는 별도 서비스가 맡아 cutoff를 직접 넘겨 결정적으로 테스트할 수 있다.ConditionalOnAdminEnabled가 붙지만, 알림 정리는 admin과 무관한 코어 유지보수라 전 환경에서 돈다.created_at단일 인덱스 마이그레이션을 더했다 (기존 인덱스는(user_id, ...)뿐이라 커버 안 됨).응답 전수 문서화
DELETE의 도달 가능한 응답을 빠짐없이 문서화했다 (어노테이션 + example 동반, fail detail은 DTO 상수 single-source).all·ids동시 / 둘 다 없음 / 빈idsResult
created_at == cutoff정확 경계(<→<=회귀 방지)·뱃지 대상 선별(안읽음 지워진 유저만·읽음만 지워진 유저 제외)·스케줄러 fan-out wiring(SSE·FCM이 영향 유저에게만 실제 전파) 검증(통합, 실 빈 + Awaitility).retention-days양수 검증, 자동삭제 재집계IN+GROUP BY(N+1 회피), 대량 백로그 fan-out을 순차 배치로 바운드 — 500명 실측으로 배지 전달률 21.6%→100% 회복·실시간 알림 비굶김을 확인.연관 이슈
Summary by CodeRabbit
all=true또는 선택 삭제ids지원).ids는 400으로 차단하고, 인증 실패는 401로 처리합니다.