Conversation
사용자가 작품별로 완결·휴재 복귀 알림을 구독하는 개념을 도메인에 정의한다. - NovelNotificationSubscription: 사용자-작품-알림유형 구독 엔티티 - NovelNotificationType: COMPLETION, HIATUS_RETURN 두 구독 유형 발송 여부(is_sent)를 함께 둔다. 등록 시 false로 시작하고, 발송은 이 서버가 아니라 어드민에서 처리하므로 값을 바꾸는 메서드는 두지 않는다. (user_id, novel_id, notification_type)에 UNIQUE 제약을 둬 같은 구독이 중복 저장되지 않게 한다. 목록 조회가 사용자·유형·발송 여부로 걸러 최신순 정렬하므로 인덱스도 (user_id, notification_type, is_sent, id) 순으로 만든다. DDL은 마이그레이션 도구 없이 직접 적용하는 기존 방식을 따라 db/ddl 아래에 이슈 번호를 붙여 둔다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
구독 목록 조회와 일괄 삭제를 QueryDSL로 작성한다. - findSubscriptions: 유형별 최신순 커서 조회. 아직 발송되지 않은 구독만 내려간다. 발송이 끝난 알림은 설정 화면에서 더 다룰 이유가 없기 때문이다. 작품은 fetch join으로 함께 읽어 목록 렌더링을 한 쿼리로 끝낸다. - deleteSubscriptions: 같은 유형의 여러 작품을 한 번에 삭제 단건 등록과 삭제는 별도 메서드를 두지 않는다. 조회한 엔티티를 그대로 JpaRepository의 save, delete에 넘기면 되기 때문이다. 벌크 삭제는 영속성 컨텍스트를 거치지 않으므로 실행 전후로 직접 flush/clear 해 컨텍스트와 DB 상태가 어긋나지 않게 한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
구독 등록·해제·조회 유스케이스를 서비스로 묶는다. updateSettings는 현재 등록 상태를 한 번 조회한 뒤, 요청 상태와 다른 유형만 저장하거나 삭제한다. 이미 요청과 같은 상태면 아무 쿼리도 보내지 않으므로 같은 요청을 반복해도 결과가 같고 불필요한 쓰기도 없다. 클라이언트가 현재 상태를 몰라도 토글 결과만 보내면 되도록 한 것이다. 발송이 끝난 구독은 조회와 갱신 양쪽에서 미등록으로 취급한다. - 설정 조회에서 제외한다. 알림을 이미 받은 뒤에도 토글이 켜져 있으면 다시 받는 것으로 오해하기 때문이다. - 다시 켜면 지우고 새로 등록해 등록일을 갱신한다. 지난 등록일을 유지하면 어드민이 이전 발송 시점을 기준으로 판단하게 된다. (user, novel, type) UNIQUE 제약이 있어 저장 전에 삭제를 먼저 반영한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
API가 주고받을 요청과 응답 형식을 정의한다. - NovelNotificationUpdateRequest: 두 알림의 목표 상태 - NovelNotificationDeleteRequest: 삭제할 유형과 작품 목록 (최대 100개) - NovelNotificationResponse: 작품 상세에서 쓸 두 토글 상태 - NovelNotificationPageResponse: 설정 화면 목록과 다음 커서 검증 실패 메시지는 NovelNotificationValidationMessage에 상수로 모은다. 어노테이션마다 문자열을 적으면 같은 뜻의 메시지가 조금씩 달라지기 때문이다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
작품 존재 검증과 구독 유스케이스를 조합한다. 설정 조회와 변경은 먼저 작품을 조회해 없으면 NOVEL-001을 던진다. 없는 작품에 알림을 등록해 두고 목록에서 빈 항목을 만나는 일을 막기 위해서다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- GET /novels/{novelId}/notification 작품 알림 설정 조회
- PUT /novels/{novelId}/notification 작품 알림 설정 변경
- GET /users/me/notification/novels 내 구독 목록 조회
- DELETE /users/me/notification/novels 내 구독 일괄 삭제
작품 기준 설정은 작품 하위 리소스에, 내 구독 목록은 이미 쓰고 있는
/users/me/... 컨벤션 아래에 둔다.
목록 조회는 size를 1~50으로 제한한다. 설정 화면이 한 번에 더 많이 받을
이유가 없고, 상한이 없으면 구독이 많은 사용자에서 응답이 커지기 때문이다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
발송된 작품 알림을 눌렀을 때 해당 작품으로 이동해야 하므로 Notification에 novelId를 추가하고, 알림 유형에 NOVEL 그룹을 둔다. 알림 목록 응답도 feedId와 나란히 novelId를 내려, 클라이언트가 이동 대상을 필드 유무로 판단하게 한다. 기존 알림 생성 계약은 그대로 두고 novelId를 null로 채우는 오버로드를 유지해, 공지·피드 알림을 만드는 쪽은 바뀌지 않는다. 이 변경은 notification 테이블의 novel_id 컬럼을 전제한다. DDL을 먼저 적용하지 않으면 기존 알림 목록 조회까지 실패한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@validated가 붙은 Controller의 PathVariable, RequestParam 검증 위반은 ConstraintViolationException으로 나오는데 핸들러가 없었다. 처리되지 않은 예외는 응답 없이 ERROR 디스패치로 넘어가고, 그 재요청은 SecurityContext가 비어 있어 인증에 실패한다. 그래서 검증 실패가 401 AUTH-001 "유효하지 않은 토큰입니다"로 응답되어, 제약 조건에 정의한 메시지가 전달되지 않았다. 작품 알림 API에서는 NOVEL_ID_POSITIVE, SIZE_MIN, SIZE_MAX, LAST_SUBSCRIPTION_ID_POSITIVE_OR_ZERO 네 메시지가 여기에 해당한다. 같은 경로를 타던 필수 파라미터 누락과 타입 변환 실패도 함께 처리한다. 세 예외 모두 RequestBody 검증과 동일하게 400 BAD_REQUEST로 응답한다. - ConstraintViolationException → 위반한 제약 조건의 메시지 - MissingServletRequestParameterException → 없는 파라미터 이름 - MethodArgumentTypeMismatchException → 변환하지 못한 값의 이름 토큰이 없거나 만료된 실제 인증 실패는 그대로 401을 반환한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ljy1348
marked this pull request as draft
August 8, 2026 01:05
API 4개의 계약을 문서 테스트로 남긴다. 성공 응답과 애플리케이션이 정의한 실패 응답을 모두 요청으로 재현하고, 상태 코드와 함께 에러 코드·메시지까지 단언해 OpenAPI 3 명세를 만든다. - GetNovelNotificationDocsTest (7 케이스) - PutNovelNotificationDocsTest (10 케이스) - GetMyNovelNotificationsDocsTest (10 케이스) - DeleteMyNovelNotificationsDocsTest (10 케이스) 에러 코드가 여러 개인 400과 401은 문서 식별자를 원인별로 나눠 Swagger UI의 Examples에서 코드별 본문을 고를 수 있게 했다. 필수 파라미터 누락은 그 문서에서만 queryParameters 서술을 뺀다. REST Docs가 선언한 파라미터를 요청에서 찾지 못하면 snippet 생성을 실패시키기 때문이다. optional()로 우회하면 생성 명세의 required가 실제 계약과 어긋난다. PUT의 401 문서는 헤더를 빼는 대신 변조 토큰을 보낸다. Bearer 헤더가 하나도 없는 문서가 섞이면 생성기가 operation의 security requirement를 만들지 않아 Swagger UI에 Authorize 자물쇠가 붙지 않았다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
세 파라미터가 모두 문자열로 문서화돼 알림 유형을 직접 입력해야 했고, 커서와 조회 개수도 타입과 기본값이 드러나지 않았다. - notificationType: enumValues 속성으로 유효값을 명세에 넣어 Swagger UI가 선택 상자를 띄우게 한다. 값은 NovelNotificationType에서 읽으므로 유형이 늘어나면 문서도 따라간다. - lastSubscriptionId, size: 정수 타입과 기본값(0, 10)을 지정해 Try it out 입력칸이 기본값으로 채워지게 한다. 요청 본문의 notificationType에는 같은 방식을 쓸 수 없다. restdocs-api-spec의 JSON 스키마 생성기가 enumValues 속성을 읽지 않아 유효값이 명세에 반영되지 않으므로, 본문 쪽은 서술로만 남긴다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Related Issue
Key Changes
작품별 완결·휴재 복귀 알림 구독을 저장하는 도메인과 테이블을 추가했습니다.
(user_id, novel_id, notification_type)에 유니크 제약을 둬 같은 구독이 중복 저장되지 않게 하고, 발송 여부(is_sent)를 함께 두어 등록 시false로 시작합니다. 발송은 어드민에서 처리하므로 이 서버에는 값을 바꾸는 메서드를 두지 않았습니다. 목록 조회가 사용자·유형·발송 여부로 걸러 최신순 정렬하므로 인덱스도(user_id, notification_type, is_sent, id)순으로 구성했습니다. (38b5011)구독 목록 조회와 일괄 삭제를 QueryDSL로 작성했습니다. 목록은 아직 발송되지 않은 구독만 커서 방식으로 내려가며, 작품을
fetch join으로 함께 읽어 한 쿼리로 끝냅니다. 단건 등록·삭제는 별도 메서드 없이JpaRepository의save,delete를 그대로 사용합니다. 벌크 삭제는 영속성 컨텍스트를 거치지 않으므로 실행 전후로 직접flush/clear합니다. (cb8ea84)구독 등록·해제·조회 유스케이스를 서비스로 묶었습니다.
updateSettings는 현재 등록 상태를 한 번 조회한 뒤 요청과 다른 유형만 저장하거나 삭제하므로, 같은 요청을 반복해도 결과가 같고 이미 같은 상태면 쿼리를 보내지 않습니다. 클라이언트는 현재 상태를 몰라도 토글 결과만 보내면 됩니다. 발송이 끝난 구독은 조회와 갱신 양쪽에서 미등록으로 취급합니다. 설정 조회에서 제외하고, 다시 켜면 지우고 새로 등록해 등록일을 갱신합니다. (3e4f82d)요청·응답 모델을 정의했습니다. 변경 요청은 두 알림의 목표 상태를, 삭제 요청은 유형과 작품 목록(최대 100개)을 받습니다. 응답은 작품 상세용 두 토글 상태와 설정 화면용 목록·다음 커서입니다. 검증 실패 메시지는
NovelNotificationValidationMessage에 상수로 모아 같은 뜻의 메시지가 갈리지 않게 했습니다. (6a24f12)작품 존재 검증과 구독 유스케이스를 조합하는 애플리케이션 계층을 추가했습니다. 설정 조회·변경은 먼저 작품을 조회해 없으면
NOVEL-001을 던져, 없는 작품에 알림이 등록돼 목록에 빈 항목이 생기는 것을 막습니다. (cc250a7)작품 알림 API 4개를 추가했습니다. 작품 기준 설정은 작품 하위 리소스에, 내 구독 목록은 기존
/users/me/...컨벤션 아래에 뒀습니다. 목록 조회는size를 1~50으로 제한합니다. (961390b)GET /novels/{novelId}/notification작품 알림 설정 조회PUT /novels/{novelId}/notification작품 알림 설정 변경GET /users/me/notification/novels내 구독 목록 조회DELETE /users/me/notification/novels내 구독 일괄 삭제앱 내 알림 목록이 작품 알림을 다루도록 확장했습니다.
Notification에novelId를 추가하고 알림 유형에NOVEL그룹을 뒀으며, 목록 응답이feedId와 나란히novelId를 내려 클라이언트가 이동 대상을 필드 유무로 판단하게 했습니다. 기존 생성 계약은novelId를null로 채우는 오버로드로 유지해 공지·피드 알림 쪽은 바뀌지 않습니다. (f938629)@Validated파라미터 검증 실패가 401로 둔갑하던 문제를 수정했습니다.ConstraintViolationException에 핸들러가 없어 예외가 ERROR 디스패치로 넘어갔고, 그 재요청은SecurityContext가 비어 인증에 실패해AUTH-001이 나갔습니다. 그래서 제약 조건에 정의한 메시지가 클라이언트에 전달되지 않았습니다. 같은 경로를 타던 필수 파라미터 누락과 타입 변환 실패도 함께 400으로 처리하며, 실제 인증 실패는 그대로 401입니다. (53ae7e5)작품 알림 API 4개를 REST Docs로 문서화했습니다(37 케이스). 성공 응답과 애플리케이션이 정의한 실패 응답을 모두 실제 요청으로 재현하고 에러 코드·메시지까지 단언합니다. 에러 코드가 여러 개인 400과 401은 문서 식별자를 원인별로 나눠 Swagger UI의 Examples에서 코드별 본문을 고를 수 있습니다. (131c8c3)
구독 목록 조회의 쿼리 파라미터에 타입과 유효값을 명시했습니다.
notificationType은enumValues속성으로 유효값을 명세에 넣어 Swagger UI가 선택 상자를 띄우고, 값은NovelNotificationType에서 읽으므로 유형이 늘어나면 문서도 따라갑니다.lastSubscriptionId·size는 정수 타입과 기본값(0, 10)을 지정해 Try it out 입력칸이 채워집니다. 요청 본문 쪽은restdocs-api-spec의 JSON 스키마 생성기가enumValues를 읽지 않아 서술로만 남겼습니다. (8f3ef42)To Reviewers
notification테이블의novel_id컬럼이 없으면 신규 API뿐 아니라 기존GET /notifications까지 실패합니다.src/main/resources/db/ddl/567_novel_notification_subscription.sql을 배포 전에 적용해 주세요./novels/{novelId}/notification-settings,/novel-notification-settings를 각각/novels/{novelId}/notification,/users/me/notification/novels로 정리했습니다. 같은 기능인데 한쪽만 최상위 하이픈 경로였고, 구독 목록은 "내" 리소스인데 소유자가 경로에 드러나지 않아서입니다. 이슈 본문도 함께 수정했습니다.is_sent) 컬럼을 두고 어드민이true로 갱신하면 목록에서 제외되는 방식으로 바꿨습니다. 발송 이력이 남아 재시도와 추적이 가능합니다. 어드민 쪽 구현이 이 전제와 맞는지 확인이 필요합니다.GET /novels/{novelId}/notification에서도false로 내려갑니다. 이 상태에서 다시 켜면 기존 구독을 지우고 새로 등록해 등록일이 갱신됩니다.(user, novel, type)UNIQUE 제약이 있어 저장 전에 삭제를 먼저 반영하며, 등록일은@CreatedDate가 새로 채웁니다. 이슈의 "해제 후 재등록 시 새 등록일 저장" 정책을 발송 완료 건에도 동일하게 적용한 것입니다.notification_type데이터가 이 PR에 없습니다.NotificationTypeGroup.NOVEL이 "완결 알림", "휴재 복귀 알림" 두 이름을 참조하는데, 해당 행을 넣는 DDL은 포함하지 않았습니다. 어드민이 알림을 만들 때 쓸notification_type_id가 필요하다면 별도로 넣어야 합니다.notification테이블에novel_id를 채워 INSERT하는 전제입니다.ON DELETE CASCADE로 처리합니다. 회원 탈퇴·작품 삭제 시 고아 데이터가 남지 않습니다.GlobalExceptionHandler수정은 이 API에 필요해 포함했지만 전역에 적용됩니다. 지금까지 401로 나가던 파라미터 검증 실패·필수 파라미터 누락·타입 변환 실패가 400으로 바뀌므로, 다른 API의 클라이언트가 이 401에 의존하고 있지 않은지 확인 부탁드립니다.References
./gradlew clean apiDocs통과, 알림 관련 테스트 54건 통과, dev DB 연결 상태에서 등록·재등록(멱등)·해제·목록 조회·검증 오류 응답을 실제 요청으로 확인했습니다.is_sent동작은 dev DB에 컬럼 적용 후 확인이 필요합니다.