-
Notifications
You must be signed in to change notification settings - Fork 0
알림 히스토리 삭제 기능 (단건·다건·전체·N일 자동삭제) #752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
dad7743
feat: 알림 히스토리 삭제 기능 추가 (단건·다건·전체·N일 자동삭제)
sevineleven de9d7d1
test: 알림 삭제·자동삭제 테스트 추가
sevineleven 30c8791
fix: 알림 자동삭제 cron 을 KST 04:00 로 고정 (zone 누락 정정)
sevineleven eb3b692
fix: 자동삭제가 배지를 갱신하지 않던 문제 등 리뷰 지적 3건 반영
sevineleven d51ee3d
test: 자동삭제 배지 대상·정확 cutoff 경계 검증 보강
sevineleven 5c5f6d8
fix: 자동삭제 배지 재집계의 N+1 을 IN + GROUP BY 한 쿼리로 제거
sevineleven 174f4b4
Merge branch 'dev' into feat/notification-history-delete
sevineleven 69d0ebd
fix: 자동삭제 재집계 N+1 배치 메서드의 호출부 wiring 누락 반영
sevineleven a084faf
test: 자동삭제 스케줄러의 배지 fan-out wiring 통합 검증 추가
sevineleven 3d9c376
fix: 자동삭제 배지 fan-out 을 순차 배치로 바꿔 대량 백로그 drop 제거
sevineleven File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
src/main/kotlin/com/depromeet/piki/notification/config/NotificationProperties.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.depromeet.piki.notification.config | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||
|
|
||
| /** | ||
| * 알림 도메인 설정. `@ConfigurationPropertiesScan`(PikiApplication)으로 자동 등록. | ||
| * | ||
| * @property retentionDays 알림 보존 기간(일). 생성 후 이 기간을 넘긴 알림은 N일 자동삭제 스케줄러가 하드삭제한다. 하드코딩 금지 — 여기서 협의값을 둔다. | ||
| */ | ||
| @ConfigurationProperties(prefix = "notification") | ||
| data class NotificationProperties( | ||
| val retentionDays: Long = 30, | ||
| ) { | ||
| init { | ||
| // 불변식 — 잘못 구성한 env(0·음수)면 cutoff(now-retentionDays)가 현재/미래가 되어 자동삭제가 알림을 대량 삭제한다. | ||
| // ops 오설정이라 클라가 닿는 계약이 아니므로 커스텀 예외가 아니라 require 로 부팅 시점에 실패시킨다. | ||
| require(retentionDays > 0) { "notification.retention-days 는 양수여야 한다 (현재=$retentionDays)" } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
src/main/kotlin/com/depromeet/piki/notification/controller/dto/NotificationDeleteRequest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.depromeet.piki.notification.controller.dto | ||
|
|
||
| import com.depromeet.piki.notification.service.dto.NotificationDeleteCommand | ||
| import com.fasterxml.jackson.annotation.JsonIgnore | ||
| import io.swagger.v3.oas.annotations.media.Schema | ||
| import jakarta.validation.constraints.AssertTrue | ||
|
|
||
| // 삭제 요청 — all=true(모두 삭제) XOR ids(지정 단건/다건). 읽음(NotificationReadRequest)과 같은 계약을 미러링한다. | ||
| // 정확히 하나만 유효(둘 다·둘 다 없음·빈 ids 는 400). 삭제는 읽음 무관 하드삭제이고 본인 소유만 걸러 멱등이다. | ||
| @Schema(description = "알림 삭제 요청 — all=true(모두) 또는 ids(지정) 중 정확히 하나") | ||
| data class NotificationDeleteRequest( | ||
| @field:Schema(description = "true 면 본인 알림 전부 삭제 (읽음 무관, 모두 삭제 버튼). ids 와 동시 사용 불가", nullable = true, example = "true") | ||
| val all: Boolean? = null, | ||
| @field:Schema(description = "삭제할 알림 id 목록 (단건은 [id] 1개, 다건은 [id, ...]). all 과 동시 사용 불가", nullable = true, example = "[1024]") | ||
| val ids: List<Long>? = null, | ||
|
sevineleven marked this conversation as resolved.
|
||
| ) { | ||
| // all XOR ids — 정확히 한쪽만. 둘 다·둘 다 없음·빈 ids 는 400(입력 경계 계약, NotificationReadRequest 와 동형). | ||
| @get:JsonIgnore | ||
| @get:AssertTrue(message = VALID_SELECTION_MESSAGE) | ||
| val validSelection: Boolean | ||
| get() { | ||
| val byAll = all == true | ||
| val byIds = !ids.isNullOrEmpty() | ||
| return byAll xor byIds | ||
| } | ||
|
|
||
| // validSelection 통과 후 호출 — all=true 면 All, 아니면 ids 는 non-null·non-empty 가 보장된다(불변식). | ||
| fun toCommand(): NotificationDeleteCommand = | ||
| if (all == true) { | ||
| NotificationDeleteCommand.All | ||
| } else { | ||
| NotificationDeleteCommand.Ids(requireNotNull(ids) { "validSelection 통과 시 ids 는 non-null 이다" }) | ||
| } | ||
|
|
||
| companion object { | ||
| // Bean Validation 위반 메시지의 single source — ApiExamples 가 같은 상수를 참조한다(detail single-source). | ||
| // ids 개수 상한은 두지 않는다 — 대량 삭제는 all=true 가 담당하고, ids 는 본인 알림 선택(보통 소수)이라 | ||
| // 인증·본인 한정·멱등 + HTTP 본문 크기 상한으로 이미 안전하다(임의 캡으로 정상 요청을 400 으로 막지 않는다). | ||
| // 응답 detail 은 사용자 대면이라 친화 문구로 둔다(어느 필드가 잘못됐는지는 앱이 자기 요청으로 안다). | ||
| const val VALID_SELECTION_MESSAGE = "요청을 처리하지 못했어요." | ||
| } | ||
| } | ||
25 changes: 25 additions & 0 deletions
25
src/main/kotlin/com/depromeet/piki/notification/controller/dto/NotificationDeleteResponse.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.depromeet.piki.notification.controller.dto | ||
|
|
||
| import com.depromeet.piki.notification.domain.NotificationCategory | ||
| import com.depromeet.piki.notification.service.toUnreadTotal | ||
| import io.swagger.v3.oas.annotations.media.Schema | ||
|
|
||
| // 삭제 처리 응답 — 삭제 후 안읽음 수(전체·탭별)를 서버 권위 값으로 내려, 클라가 +1/-1 산수 없이 그대로 미러링하게 한다. | ||
| // 읽음 응답(NotificationReadResponse)과 같은 셰입(unreadCount + unreadCountByCategory)이라, 삭제로 안읽음이 사라지면 | ||
| // 앱 badge 와 활동/시스템 탭 badge 를 한 왕복에서 갱신한다(다른 기기는 재진입 시 GET /notifications 로 보정). | ||
| @Schema(description = "알림 삭제 처리 응답 — 삭제 후 안읽음 수(전체·탭별)") | ||
| data class NotificationDeleteResponse( | ||
| @field:Schema(description = "삭제 후 본인 전체 안읽음 수 (앱 badge). 클라는 이 값을 그대로 badge 로 미러링한다", example = "1") | ||
| val unreadCount: Long, | ||
| @field:Schema(description = "삭제 후 카테고리별 안읽음 수 (탭 badge). 모든 카테고리 키 포함, 없으면 0", example = "{\"ACTIVITY\":1,\"SYSTEM\":0}") | ||
| val unreadCountByCategory: Map<NotificationCategory, Long>, | ||
| ) { | ||
| companion object { | ||
| // total 은 카테고리 합으로 도출 — toUnreadTotal 단일 소스를 거쳐 전체·탭별 두 수치가 어긋날 여지를 없앤다(read 와 동일 규칙). | ||
| fun of(unreadCountByCategory: Map<NotificationCategory, Long>): NotificationDeleteResponse = | ||
| NotificationDeleteResponse( | ||
| unreadCount = unreadCountByCategory.toUnreadTotal(), | ||
| unreadCountByCategory = unreadCountByCategory, | ||
| ) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.