Skip to content

[#200] feat. 알림 삭제 API (단건·전체) - #202

Merged
howudong merged 6 commits into
mainfrom
feature/200
Sep 20, 2026
Merged

howudong merged 6 commits into
mainfrom
feature/200

Conversation

@howudong

Copy link
Copy Markdown
Contributor

Closes #200

FE 요청서: https://github.com/ditto-develop/ditto-fe/wiki/BE-Request-Delete-And-Profile-Edit (1번)

배경

알림 API에 삭제가 없어 FE가 localStorage(ditto.hiddenNotificationIds)로 기기 로컬 숨김을 붙여 우회 배포한 상태다. 다른 기기·재설치에서는 숨긴 알림이 다시 보인다.

추가한 API

DELETE /api/v1/notifications/{id}
  → 내 알림 하나. 남의 알림·이미 지운 알림은 404

DELETE /api/v1/notifications?category=MATCHING|CHAT|SYSTEM
  → 화면에 보이는 내 알림 전체 (category 생략 시 전부)

응답은 둘 다 { deletedCount }. 단건은 항상 1이지만 FE가 응답 처리를 하나로 쓸 수 있게 형식을 맞췄다.

소프트 삭제인 이유

처음엔 하드 삭제로 만들었다가 리뷰에서 막혔다. 같은 사건을 두 번 알리지 않는 판단이 알림 행의 존재 하나에 걸려 있어서(existsByMemberIdAndTypeAndTargetId), 행을 지우면 수렴 루프를 도는 스케줄러가 같은 대상을 다시 집어와 알림과 푸시를 다시 내보낸다. ChatRoomLifecycleScheduler.sweep()이 1분 주기라 지운 지 60초 안에 돌아온다.

deleted_at을 찍고 행은 남긴다. 존재 검사는 지운 행도 세므로 재발송이 막히고, 목록·미읽음 수·전체 읽음·단건 조회는 조건으로 걸러낸다. 30일이 지나면 기존 purge 배치가 다른 행과 함께 지우므로 보관 기간은 그대로다.

함께 고친 것 — 미성사 그룹 안내 재발송

같은 구조의 문제가 main에 이미 있었다. findUnformedUntil에 기간 하한이 없어 과거 미성사 그룹을 매 분 다시 집는데, 알림 행이 purge되면(30일) 존재 검사가 다시 통과해 안내와 푸시가 또 나간다. 새 행도 30일 뒤 지워져 그때부터 30일 주기로 반복된다. 사용자가 알림을 지우지 않아도 모든 대상자에게 일어난다.

스캔을 최근 2주 주차로 자른다(findUnformedBetween). purge 전에 대상에서 빠지고, 스케줄러가 한두 주 멈췄다 돌아와도 놓친 주차를 따라잡는다. 대신 2주보다 오래된 그룹은 안내가 나가지 않는다 — 마감 직후 안내라 지난 주차를 다시 집을 이유가 없고, 배포 시점(#192)에 과거분은 이미 소진됐다.

다른 ONCE_PER_TARGET 유형은 스캔 범위가 짧아 같은 문제가 없다(CHAT_ENDING_SOON은 종료 6시간 창, REVIEW_REQUEST·MATCH_RESULT는 이번 주기 처리분만).

마이그레이션

V20260919224428_알림 삭제 표시 컬럼 추가.sql

  • notification.deleted_at DATETIME(6) NULL 추가
  • notification_index_1을 (member_id, id) → (member_id, deleted_at, id)로 재생성. 목록 조회가 member_id = ? AND deleted_at IS NULL을 동등 조건으로 쓰므로 세 번째 컬럼 id로 정렬·커서를 그대로 태운다

검증

./gradlew :domain:test :api:test — 1233건 통과, 실패 0 (Jacoco 커버리지 게이트 포함).

회귀 테스트를 넣었다.

  • NotificationAppenderTest — "사용자가 지운 알림은 다시 적재되지 않는다"
  • UnformedGroupNotifierTest — 3주 전 주차는 제외, 지난주 주차는 포함
  • NotificationRepositoryTest — 지운 알림이 목록·미읽음 수에서 빠지되 행은 남고 중복 검사에 잡히는 것

문서

  • docs/domains/notification.md — 엔드포인트 2줄, "사용자 삭제는 행을 남긴다"·"중복 검사는 보관 기간까지만 유효하다" 불변식
  • docs/domains/match.md — 미성사 안내 멱등의 한계와 2주 창

🤖 Generated with Claude Code

howudong and others added 6 commits September 19, 2026 22:36
알림 센터의 "전체 삭제"가 필터 칩과 같은 기준으로 지울 수 있어야 한다.
회원 전체 삭제(deleteAllByMemberId)는 탈퇴 경로가 쓰고 있어 그대로 두고,
카테고리 조건을 얹은 쿼리를 따로 둔다.

카테고리는 컬럼이 아니라 유형에서 파생되므로 조건은 type in (...) 이다 —
목록 조회(findByMemberIdWithCursor)가 쓰는 것과 같은 매핑이다.

파생 삭제 대신 QueryDSL 벌크 DELETE 인 이유는 같은 파일의 다른 삭제들과 같다:
행을 로드해 하나씩 remove 하지 않고 단일 DELETE 로 끝낸다. Notification 은
연관관계도 @PreRemove 도 없어 영속성 컨텍스트를 건너뛰어 잃을 것이 없다.

Refs #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
알림에 삭제가 없어 FE 가 localStorage 로 기기 로컬 숨김을 붙여 우회 배포한
상태였다. 다른 기기·재설치에서는 숨긴 알림이 다시 보인다.

- DELETE /api/v1/notifications/{id} — 내 알림 하나. 남의 알림·이미 지운
  알림은 404 (읽음과 같은 기준)
- DELETE /api/v1/notifications?category= — 내 알림 전체, 칩 단위 선택 가능

하드 삭제다. 접기·탈퇴·보관 경과 정리가 모두 행을 지우는 쪽이라 소프트
삭제를 들이면 조회 세 경로에 제외 조건이 붙고 purge 와 뜻이 겹친다. 알림은
사건의 사본이고 본문에 닉네임·메시지 미리보기가 들어 있어 보이지 않는 행을
남겨둘 이유가 없다.

응답은 두 API 모두 deletedCount 로 맞춘다. 단건은 항상 1 이지만(없으면 404)
FE 가 응답 처리를 하나로 쓸 수 있다.

Refs #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
엔드포인트 표에 DELETE 두 줄을 넣고, 삭제 경로가 모두 하드 삭제라는 것을
불변식으로 남긴다 — 사용자 삭제가 들어오면서 "왜 소프트가 아닌가"가
되풀이될 자리라 판단 근거를 한곳에 둔다.

Refs #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
하드 삭제로 만든 삭제 API가 중복 검사의 근거를 지운다. 같은 사건을 두 번
알리지 않는 판단이 알림 행의 존재 하나에 걸려 있어서, 행이 사라지면 수렴
루프를 도는 스케줄러가 같은 대상을 다시 집어와 알림과 푸시를 또 내보낸다.
1분 주기라 지운 지 60초 안에 돌아온다 — CHAT_ENDING_SOON 은 방이 끝날
때까지, GROUP_NOT_FORMED 는 기간 하한이 없어 무기한이다.

deleted_at 을 찍고 행은 남긴다. 존재 검사는 지운 행도 세므로 재발송이
막히고, 목록·미읽음 수·전체 읽음은 조건으로 걸러낸다. 30일이 지나면 기존
purge 배치가 다른 행과 함께 지우므로 보관 기간도 그대로다.

미읽음 수는 이름이 74자가 되는 것을 피해 countUnread 로 옮겼다.

Refs #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
단건은 markDeleted, 전체는 벌크 UPDATE 다. 전체 삭제 대상을 보관 창 안으로
좁혔다 — 화면에 없던 행까지 세면 deletedCount 가 사용자가 보던 목록과
어긋난다. 뱃지 조회도 countUnread 로 맞춰 인앱 배지와 같은 수를 유지한다.

지운 알림이 다시 적재되지 않는 것을 NotificationAppenderTest 로 고정한다.
이 테스트가 깨지면 되살아나는 알림이 돌아온 것이다.

컨트롤러는 읽음 쌍 뒤에 삭제 쌍을 둔다(리뷰 지적).

Refs #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
중복 검사는 알림 행이 살아 있는 동안만 유효하다. findUnformedUntil 에
하한이 없어 과거 미성사 그룹을 매 분 다시 집는데, 보관 기간(30일)이 지나
purge 로 행이 사라지면 존재 검사가 다시 통과해 안내와 푸시가 또 나간다.
새 행도 30일 뒤 지워지므로 그때부터 30일 주기로 반복된다.

스캔에 하한을 준다. 2주면 purge 전에 대상에서 빠지고, 스케줄러가 한두 주
멈췄다 돌아와도 놓친 주차를 따라잡는다. 대신 2주보다 오래된 그룹은 안내가
나가지 않는다 — 마감 직후 안내라 지난 주차를 다시 집을 이유가 없고,
배포 시점(#192)에 과거분은 이미 소진됐다.

다른 ONCE_PER_TARGET 유형은 스캔 범위가 짧아 같은 문제가 없다. 그 근거를
notification.md 불변식과 match.md 에 남긴다.

Refs #200

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@howudong
howudong merged commit d8a852e into main Sep 20, 2026
2 checks passed
@howudong
howudong deleted the feature/200 branch September 20, 2026 07:22
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.

feat. 알림 삭제 API (단건·전체)

1 participant