diff --git a/specs/006-notification-domain/checklists/requirements.md b/specs/006-notification-domain/checklists/requirements.md new file mode 100644 index 00000000..9ae588a8 --- /dev/null +++ b/specs/006-notification-domain/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: 알림(Notification) 기능 + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-17 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) — FCM/Firebase/JPA 구현 용어 본문 미포함, "푸시 알림 외부 채널"로 추상화 +- [x] Focused on user value and business needs — 우선순위(P1~P3)로 사용자 가치 정렬 +- [x] Written for non-technical stakeholders — 한국어 비기술자 친화 서술 +- [x] All mandatory sections completed — User Scenarios / Requirements / Success Criteria 모두 작성 + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain — 1개 마커 해소 (Q1: 현재 평생 누적·수동 삭제 미제공, 향후 최근 N일 자동 삭제 도입 예정) +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded — 트리거를 발화하는 도메인 로직 자체는 명시적 범위 외(각 도메인 PRD 책임) +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- 2026-05-17 1차 검증: 마커 해소, 모든 항목 통과. `/speckit-clarify`(선택) 또는 `/speckit-plan`으로 진행 가능. +- 본 PRD는 *발화 트리거의 카탈로그*만 정의하며, 각 트리거를 *언제 발화하는지*는 해당 도메인 PRD가 책임진다(피드/팔로우/방/RoomPost). 알림 도메인이 트리거를 받았을 때의 동작·표시·전달이 본 PRD의 본질이다. +- 향후 트리거: 알림 보존 기간 제한(최근 N일 자동 삭제) 도입 시 FR-021 / Edge Cases 업데이트 필요(Assumptions에 명시됨). diff --git a/specs/006-notification-domain/spec.md b/specs/006-notification-domain/spec.md new file mode 100644 index 00000000..8e4799d4 --- /dev/null +++ b/specs/006-notification-domain/spec.md @@ -0,0 +1,212 @@ +# Feature Specification: 알림(Notification) 기능 + +**Feature Branch**: `006-notification-domain` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "notification 관련 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "알림(Notification)" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 외부 푸시 채널 의존(현재 Firebase Cloud Messaging)은 의도적으로 추상화하고, 사용자가 무엇을(WHAT) 왜(WHY) 알림에 대해 할 수 있어야 하는지에 집중한다. + +## User Scenarios & Testing *(mandatory)* + +THIP의 **알림**은 서비스 안에서 사용자에게 일어난 일을 두 가지 채널로 전달한다: (a) 사용자 디바이스로 보내는 *푸시 알림*, (b) 앱 안의 *알림 센터* 목록. 알림은 두 카테고리로 구분된다: **피드(FEED)**와 **모임(ROOM)**. 각 알림은 클릭 시 *연관된 화면*으로 이동하기 위한 라우팅 정보를 포함한다. + +본 PRD는 *발화 트리거*의 *카탈로그*를 정의하지만, 각 트리거가 *언제* 발화되는지는 해당 도메인 PRD(피드/팔로우/방/RoomPost)가 책임진다. 본 PRD가 다루는 것은 **트리거 수신 이후의 알림 동작**이다: 알림 저장·표시·읽음 처리·라우팅·푸시 전달·디바이스 토큰·수신 여부 설정. + +### User Story 1 - 내가 받은 알림을 알림 센터에서 확인하기 (Priority: P1) + +사용자는 자신에게 도착한 알림을 알림 센터에서 최신순으로 본다. 카테고리(피드/모임/둘 다)로 필터링할 수 있다. 읽지 않은 알림이 있는지 단일 호출로 빠르게 확인하는 신호도 받는다. + +**Why this priority**: 알림 센터는 알림 도메인의 1차 진입점이다. 푸시 알림을 놓쳤거나 끈 사용자도 여기서 활동을 다시 잡는다. + +**Independent Test**: 사용자가 다수의 알림을 가진 상태에서 (a) 전체 알림을 최신순 커서 페이지로 받고, (b) `type=feed`/`type=room`/`type=feedAndRoom`(기본) 필터로 결과를 좁히며, (c) 안 읽은 알림 존재 여부를 단일 호출로 확인할 수 있다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 다수의 알림을 가질 때, **When** 알림 목록을 요청하면, **Then** 자신에게 도착한 알림만 최신순으로 커서 페이지로 반환된다(다른 사용자의 알림은 절대 포함되지 않는다). +2. **Given** 사용자가 `type` 파라미터로 카테고리 필터를 지정한다, **When** `feed`/`room`/`feedAndRoom`(기본) 중 하나를 보낸다, **Then** 해당 카테고리에 해당하는 알림만 반환된다. 인정되지 않는 `type` 값은 거부된다. +3. **Given** 사용자에게 안 읽은 알림이 1건 이상 있을 때, **When** 사용자가 "안 읽은 알림 존재 여부"를 요청하면, **Then** *true*가 반환된다. 안 읽은 알림이 없으면 *false*가 반환된다. +4. **Given** 알림 목록의 각 항목, **Then** 카테고리 표기(예: "[피드]", "[모임]"가 제목에 자연스럽게 표시되거나 카테고리 메타로 구분 가능), 본문, 읽음 여부, 클릭 시 이동할 화면을 식별할 수 있는 라우팅 정보를 함께 제공한다. + +--- + +### User Story 2 - 알림을 클릭해 관련 화면으로 이동하기 (Priority: P1) + +사용자가 푸시 알림 또는 알림 센터의 항목을 클릭하면, 해당 알림이 *읽음*으로 처리되고 클라이언트는 어디로 이동할지에 필요한 정보를 응답으로 받는다. 이미 읽음 처리된 알림을 다시 클릭해도 라우팅 정보는 항상 응답된다. + +**Why this priority**: 알림은 "확인"이 아닌 *행동 유도*가 본질. 라우팅이 끊기면 알림의 효용이 사라진다. + +**Independent Test**: 사용자가 임의 알림 ID로 "읽음 처리" 요청을 보냈을 때 (a) 최초 1회만 읽음 상태가 변경되고, (b) 응답에는 클라이언트가 다음으로 이동할 화면을 결정할 수 있는 라우팅 정보(이동 대상 종류 + 필요한 식별자들)가 포함되며, (c) 이미 읽음 처리된 알림에 대해서도 라우팅 정보는 동일하게 응답된다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 미확인 알림 ID로 읽음 처리를 요청한다, **When** 작업이 성공한다, **Then** 그 알림의 읽음 상태가 *읽음*으로 바뀌고, 응답에 라우팅 정보가 포함된다. +2. **Given** 사용자가 *이미 읽음 처리된* 알림 ID로 다시 요청을 보낸다, **Then** 읽음 상태는 변경되지 않으나 응답에는 동일한 라우팅 정보가 그대로 포함된다(클라이언트 화면 이동에는 지장이 없다). +3. **Given** 사용자가 *다른 사용자의 알림 ID*로 읽음 처리를 시도한다, **Then** 작업이 거부된다. +4. **Given** 라우팅 종류는 다음 중 하나여야 한다: 이동 안 함 / 팔로우한 사용자의 피드 목록 / 피드 상세 / 모임 메인 / 모임 상세 / 모임 게시글 상세(게시글 종류가 기록인지 투표인지 함께 식별). + +--- + +### User Story 3 - 푸시 알림을 받기 위해 디바이스 등록하기 (Priority: P1) + +사용자는 자신의 디바이스를 푸시 알림 채널에 등록하고, 같은 디바이스에서 토큰이 갱신되면 자동으로 최신 토큰으로 대체된다. 더 이상 사용하지 않는 디바이스의 토큰은 명시적으로 삭제한다. + +**Why this priority**: 디바이스 등록이 없으면 푸시 알림 자체가 존재하지 않는다. P1 핵심. + +**Independent Test**: 사용자가 (a) 임의 디바이스 ID와 플랫폼(ANDROID/WEB) 정보로 토큰을 등록할 수 있고, (b) 같은 디바이스 ID에서 새 토큰을 다시 등록하면 기존 토큰이 갱신되며(추가가 아니라 대체), (c) 임의 시점에 토큰을 삭제할 수 있다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 처음 디바이스 ID·플랫폼·토큰을 보내 등록을 요청한다, **When** 작업이 성공한다, **Then** 그 사용자에 대해 그 디바이스의 토큰이 저장된다. +2. **Given** 사용자가 같은 디바이스 ID로 새 토큰을 다시 등록한다, **When** 작업이 성공한다, **Then** 기존 토큰이 새 토큰으로 갱신되며, 같은 사용자·같은 디바이스에 대해 활성 토큰은 정확히 1건이다(중복 누적 없음). +3. **Given** 사용자가 디바이스 토큰의 삭제를 요청한다, **When** 작업이 성공한다, **Then** 그 디바이스로는 더 이상 푸시 알림이 전달되지 않는다. +4. **Given** 인정되지 않는 플랫폼 값(ANDROID/WEB 외)이 입력된다, **Then** 작업이 거부된다. + +--- + +### User Story 4 - 디바이스별로 푸시 알림 수신 여부 켜고 끄기 (Priority: P2) + +사용자는 디바이스 단위로 푸시 알림 수신을 켜거나 끈다. 같은 사용자라도 한 디바이스에서는 켜고 다른 디바이스에서는 끌 수 있다. 알림 센터의 알림 목록은 수신 여부와 무관하게 계속 보존된다(앱 안에서는 보임, 푸시만 차단). + +**Why this priority**: 사용자 제어. 푸시 강제는 이탈 요인이라 켜고/끄기는 P1과 함께 출시되어야 하나, 토글 자체는 등록·라우팅 다음의 보조 시나리오. + +**Independent Test**: 사용자가 같은 계정의 디바이스 A·B에 대해 서로 다른 수신 여부를 설정했을 때 (a) 디바이스 A에서는 푸시가 도달하고 B에서는 도달하지 않으며, (b) 두 디바이스 모두에서 알림 센터 항목은 동일하게 나타난다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 디바이스 ID를 명시해 푸시 수신 여부 변경(true/false)을 요청한다, **When** 작업이 성공한다, **Then** 그 디바이스의 수신 여부가 갱신된다. +2. **Given** 사용자가 디바이스 ID를 명시해 현재 수신 여부 조회를 요청한다, **When** 작업이 성공한다, **Then** 그 디바이스의 현재 수신 여부가 응답된다. +3. **Given** 이미 동일한 상태로 토글 요청을 보낸다(이미 켜진 디바이스에 *켜기* 요청 등), **Then** 작업이 거부되어 사용자가 의도하지 않은 무동작을 방지한다. +4. **Given** 다른 사용자 소유의 디바이스 토큰의 수신 여부를 변경하려고 시도한다, **Then** 작업이 거부된다. +5. **Given** 어떤 디바이스의 수신 여부가 *꺼짐*일 때, **When** 새 알림이 발생한다, **Then** 그 디바이스로는 푸시가 전달되지 않으나, 알림 센터에는 동일하게 누적된다. + +--- + +### User Story 5 - 다른 도메인의 행동으로부터 알림 발화 받기 (Priority: P1) + +사용자가 받는 알림은 다른 도메인(피드/팔로우/방/RoomPost)에서 일어난 의미 있는 행동의 결과로 자동 발화된다. 본 PRD는 어떤 *종류*의 트리거가 존재하는지 카탈로그를 정의하며, 트리거를 받았을 때 알림이 저장·표시·전달되는 흐름을 보장한다. + +**Why this priority**: 알림 도메인 자체의 가치는 트리거가 들어와야 발생한다. 트리거 카탈로그가 명문화되어 있어야 다른 도메인 PRD와 정합성이 유지된다. + +**Independent Test**: 임의의 트리거 종류(예: "팔로우됨", "피드 좋아요됨", "모임 게시글 댓글됨")가 입력되었을 때 (a) 대상 사용자의 알림 센터에 해당 카테고리(FEED 또는 ROOM)의 새 알림이 누적되고, (b) 대상 사용자의 수신 여부가 켜진 디바이스에 대해 푸시가 전달된다. + +**Acceptance Scenarios**: + +각 트리거가 발화되면, 시스템은 (i) 대상 사용자의 알림 센터에 새 알림 1건을 저장하고, (ii) 그 사용자가 보유한 *수신 여부 켜진 디바이스*에 푸시를 전달해야 한다. + +**알림 트리거 카탈로그 (확정)**: + +| 카테고리 | 트리거 종류 | 발화 도메인 (책임) | 라우팅 대상 | +|---|---|---|---| +| FEED | 누군가 나를 팔로우함 | 팔로우(#355) | 팔로우한 사용자의 피드 목록 | +| FEED | 팔로잉한 사용자가 새 피드를 작성함 | 피드(#354) + 팔로우(#355) | 그 피드 상세 | +| FEED | 내 피드에 좋아요가 눌림 | 피드(#354) | 그 피드 상세 | +| FEED | 내 피드에 댓글이 달림 | 피드(#354) | 그 피드 상세 | +| FEED | 내 피드 댓글에 답글이 달림 | 피드(#354) | 그 피드 상세 | +| FEED | 내 피드 댓글에 좋아요가 눌림 | 피드(#354) | 그 피드 상세 | +| ROOM | 내가 호스트인 방에 새 참여자가 들어옴 | 방(#358) | 모임 상세 | +| ROOM | 내가 참여한 방의 모집이 조기 마감됨 | 방(#358) | 모임 상세 | +| ROOM | 내가 참여한 방의 활동(진행)이 시작됨 | 방(#358) | 모임 메인 | +| ROOM | 내가 참여한 방에서 새 기록이 작성됨 | RoomPost(#357) | 모임 게시글 상세 (기록) | +| ROOM | 내가 참여한 방에서 새 투표가 시작됨 | RoomPost(#357) | 모임 게시글 상세 (투표) | +| ROOM | 내가 작성한 방 게시글에 댓글이 달림 | 댓글 도메인 | 모임 게시글 상세 | +| ROOM | 내가 작성한 방 게시글 댓글에 답글이 달림 | 댓글 도메인 | 모임 게시글 상세 | +| ROOM | 내가 작성한 방 게시글에 좋아요가 눌림 | 좋아요 공통 도메인 | 모임 게시글 상세 | +| ROOM | 내가 작성한 방 게시글 댓글에 좋아요가 눌림 | 좋아요 공통 도메인 | 모임 게시글 상세 | + +> 각 트리거의 *발화 조건*과 *발화 횟수 보증*은 해당 발화 도메인 PRD가 책임진다(예: 팔로우 PRD는 "팔로우 성공 1회당 알림 트리거 1회"를 명시). 본 PRD는 *트리거를 받은 뒤*의 알림 흐름만 보장한다. + +--- + +### Edge Cases + +- **알림 소유자 검증**: 사용자는 자신이 수신자인 알림에 대해서만 읽음 처리·삭제·상세 조회가 가능하다. 타인의 알림에 대한 어떤 조작도 거부된다. +- **이미 읽음 처리된 알림의 재읽음**: 상태는 변경되지 않으나 라우팅 정보는 매번 응답된다(클라이언트가 클릭 시 항상 화면 이동 가능해야 함). +- **수신 여부 토글 멱등성 차단**: 이미 동일한 상태로 변경을 요청하면 거부된다(켜진 상태에서 *켜기*, 꺼진 상태에서 *끄기*). +- **자기 행위에 대한 알림**: 자기 자신에게 알림이 가는 행위(예: 내 댓글에 내가 답글)는 본 PRD가 직접 방지하지 않는다. 각 발화 도메인 PRD가 그 의미 없는 자기 알림을 *발화하지 않도록* 책임을 진다(또는 그렇게 설계되어 있다). +- **수신 여부 OFF 디바이스의 동작**: 그 디바이스로 푸시는 전달되지 않으나, 알림 센터에는 변함없이 누적된다. 사용자가 앱을 열면 모두 보인다. +- **타 사용자 토큰 조작 방어**: 다른 사용자의 디바이스 토큰을 자신이 수정·삭제·수신 여부 토글하려는 시도는 거부된다. +- **알림 라우팅 미정**: 라우팅 종류가 "이동 안 함"(NONE)인 알림은 클릭해도 화면이 이동하지 않으나 읽음 처리는 정상 동작한다. +- **외부 푸시 채널 일시 장애**: 외부 푸시 전달이 실패해도 알림 센터에 누적된 알림은 손실되어서는 안 된다(저장과 전달은 독립적). +- **삭제·탈퇴된 발화 주체**: 발화를 만든 행위자(예: 좋아요 누른 사용자)가 이후 탈퇴해도, 이미 발화된 알림 자체는 사용자 입장에서 일관되게 보여야 한다(상세 정책은 사용자 라이프사이클 도메인 PRD를 따른다). +- **알림 보존 (확정 — 현재 정책)**: 사용자별 알림은 *평생 누적*된다. 자동 삭제·만료 정책은 없으며, 사용자 수동 삭제 기능도 *현재 시점에는 제공되지 않는다*. 사용자 알림 센터에 도착한 알림은 읽음 처리 여부와 무관하게 계속 남는다. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 알림 센터 (앱 내) + +- **FR-001**: 사용자는 자신에게 도착한 알림 목록을 최신순으로 커서 페이지로 조회할 수 있어야 한다. 다른 사용자의 알림이 결과에 포함되어서는 안 된다. +- **FR-002**: 사용자는 카테고리 필터(`feed`/`room`/`feedAndRoom` — 기본 `feedAndRoom`)로 알림 목록을 좁힐 수 있어야 한다. 인정되지 않는 값은 거부된다. +- **FR-003**: 시스템은 사용자에게 *안 읽은 알림이 1건 이상 존재하는지*를 단일 호출로 응답할 수 있어야 한다. +- **FR-004**: 각 알림은 카테고리(FEED/ROOM)·제목·본문·읽음 여부·라우팅 정보를 포함한다. + +#### 읽음 처리 및 라우팅 + +- **FR-005**: 사용자는 자신의 알림에 대해 읽음 처리를 요청할 수 있다. 최초 1회만 상태가 *읽음*으로 바뀌며, 이미 읽음 처리된 알림은 상태가 변경되지 않는다. +- **FR-006**: 읽음 처리 응답에는 클라이언트가 화면을 이동할 수 있는 라우팅 정보(라우팅 종류 + 필요한 식별자)가 항상 포함되어야 한다. 이미 읽음 처리된 알림의 재요청에 대해서도 동일한 라우팅 정보가 응답된다. +- **FR-007**: 라우팅 종류는 다음 6종 중 하나여야 한다: *이동 안 함*, *팔로우한 사용자의 피드 목록*, *피드 상세*, *모임 메인*, *모임 상세*, *모임 게시글 상세*(게시글 종류 기록/투표를 함께 식별). +- **FR-008**: 사용자가 *자신이 아닌 다른 사용자의 알림* ID로 읽음 처리를 시도하면 거부된다. + +#### 디바이스 토큰 + +- **FR-009**: 사용자는 자신의 디바이스 ID·플랫폼(ANDROID/WEB)·푸시 토큰을 등록할 수 있다. 같은 사용자·같은 디바이스 ID에 대해서는 활성 토큰이 정확히 1건만 존재한다(재등록 시 갱신). +- **FR-010**: 사용자는 자신의 디바이스 토큰을 삭제할 수 있다. 삭제 후 그 디바이스로는 푸시가 전달되지 않는다. +- **FR-011**: 인정되지 않는 플랫폼 값은 거부된다. +- **FR-012**: 다른 사용자 소유 디바이스 토큰에 대한 등록·갱신·삭제·수신 여부 토글 시도는 거부된다. + +#### 푸시 수신 여부 + +- **FR-013**: 사용자는 *디바이스 단위로* 푸시 알림 수신 여부를 켜거나 끌 수 있다(같은 사용자라도 디바이스별 독립 설정). +- **FR-014**: 사용자는 디바이스 ID를 명시해 현재 수신 여부를 조회할 수 있다. +- **FR-015**: 현재 상태와 동일한 토글 요청(이미 켜진 디바이스에 *켜기*, 이미 꺼진 디바이스에 *끄기*)은 거부된다. +- **FR-016**: 수신 여부가 *꺼짐*인 디바이스로는 푸시가 전달되지 않는다. 알림 센터에 누적되는 알림은 수신 여부와 무관하다. + +#### 트리거 수신과 발화 + +- **FR-017**: 시스템은 외부 도메인(피드·팔로우·방·RoomPost·댓글·좋아요)으로부터 알림 발화 트리거를 수신할 수 있어야 한다. 트리거를 받은 시스템은 (i) 대상 사용자의 알림 센터에 새 알림 1건을 저장하고, (ii) 대상 사용자의 *수신 여부 켜진 디바이스*에 푸시를 전달해야 한다. +- **FR-018**: 알림 카테고리는 트리거 종류에 따라 FEED 또는 ROOM 중 하나로 결정된다(상세 매핑은 User Story 5의 카탈로그 표를 따른다). +- **FR-019**: 알림 저장(알림 센터)과 푸시 전달은 독립적으로 보장된다. 외부 푸시 채널 일시 장애 시에도 알림 센터의 누적은 손실되지 않아야 한다. +- **FR-020**: 같은 트리거가 도메인 책임 영역에서 *1회로 정의*되어 발화되면, 본 도메인은 그에 대해 *정확히 1건*의 알림을 만든다. 발화 횟수의 정합성은 발화 도메인 PRD가 책임지며 본 도메인은 받은 트리거의 1건당 1건을 보장한다. + +#### 알림 보존 + +- **FR-021**: 사용자별 알림은 *평생 누적*된다. 알림 도메인은 자동 삭제·만료·압축 작업을 수행하지 않으며, 사용자 수동 삭제 경로도 현재 시점에는 제공하지 않는다. 모든 알림은 읽음 여부와 무관하게 알림 센터에 계속 노출된다. + +### Key Entities + +- **Notification (알림)**: 사용자에게 표시되는 알림 1건. 제목·본문·카테고리(FEED/ROOM)·읽음 여부·수신자(targetUserId)·라우팅 정보를 가진다. +- **FcmToken (디바이스 토큰)**: 한 사용자의 한 디바이스에 대한 푸시 채널 식별자. 디바이스 ID·플랫폼(ANDROID/WEB)·수신 여부 플래그·마지막 사용 시각을 가진다. (사용자, 디바이스 ID) 쌍에 대해 활성 토큰은 정확히 1건이다. +- **NotificationCategory (카테고리)**: FEED / ROOM 두 종. +- **MessageRoute (라우팅 종류)**: 알림 클릭 시 이동 대상의 추상 식별자. *이동 안 함* / *피드 사용자 목록* / *피드 상세* / *모임 메인* / *모임 상세* / *모임 게시글 상세* 6종. +- **Notification Trigger (알림 트리거)**: 외부 도메인이 발화하는 추상 이벤트. 본 PRD는 트리거의 카탈로그(User Story 5)만을 정의하며 *발화 조건*은 외부 도메인 PRD가 정의한다. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 다른 사용자의 알림이 본인 알림 목록에 노출되는 사건이 0건이다(권한 누수 0). +- **SC-002**: 알림 트리거 1건당 알림 센터에 누적되는 알림이 정확히 1건이며, 누락·중복 사건이 0건이다. +- **SC-003**: 알림을 클릭한 사용자가 의도된 화면으로 이동하지 못하는 사건(라우팅 정보 누락·잘못)이 0건이다. +- **SC-004**: 디바이스 수신 여부 *꺼짐* 상태에서 그 디바이스로 푸시가 전달되는 사건이 0건이다. +- **SC-005**: 한 사용자·한 디바이스에 대한 활성 푸시 토큰이 2건 이상 존재하는 사건이 0건이다(중복 누적 0). +- **SC-006**: 외부 푸시 채널 일시 장애가 발생해도 알림 센터에 누적된 알림이 손실되는 사건이 0건이다(저장과 전달의 독립성). +- **SC-007**: 사용자가 알림을 클릭한 뒤 클라이언트가 화면 이동에 필요한 정보를 추가 호출 없이 즉시 받는 비율이 100%이다(읽음 처리 응답에 라우팅 정보 항상 포함). + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 요구사항은 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"의 형태로 읽힌다. +- **인증 전제**: 모든 알림 시나리오는 인증된 사용자를 전제로 한다. +- **외부 푸시 채널은 외부 의존**: 현재 외부 푸시 채널로 Firebase Cloud Messaging이 사용되지만, 본 PRD는 *벤더 중립*으로 다룬다. 벤더 교체는 본 PRD의 변경 트리거가 아니다. +- **트리거 발화 조건은 외부 도메인의 책임**: 본 PRD는 *어떤 종류의 트리거가 존재하는지*만 카탈로그로 정의한다. *언제 발화하는지*는 발화 도메인 PRD(피드/팔로우/방/RoomPost/댓글/좋아요)가 정의하며, 그 PRD가 트리거의 발화 횟수 보증(예: "팔로우 1회 = 1건")을 책임진다. +- **자기 행위 알림 방지**: 본 PRD는 *받은 트리거*에 대해 1건의 알림을 만든다. 자기 자신에 대한 무의미한 알림(예: 자기 댓글에 자기 답글)을 *발화하지 않을 책임*은 발화 도메인 PRD에 있다. +- **댓글·좋아요 행위는 외부 도메인**: 본 PRD는 댓글/좋아요로 인한 알림 트리거를 *받는* 입장만을 다룬다. 댓글/좋아요의 사용자 흐름 자체는 댓글 도메인·좋아요 공통 도메인(Post) PRD가 정의한다. +- **사용자 라이프사이클 외부 의존**: 발화 주체나 수신자의 탈퇴·삭제 시 알림 표시 정책은 별도 사용자 라이프사이클 PRD가 정의한다. +- **알림 본문 다국어**: 본 PRD는 현재 한국어 본문을 전제로 한다. 다국어 지원은 본 PRD의 변경 트리거다. +- **페이지네이션 일관성**: 알림 목록은 다른 도메인과 동일하게 커서 기반을 따른다. +- **외부 푸시 전달 비용 통제**: 외부 푸시 채널의 호출 비용·할당량 정책은 본 PRD의 범위가 아니며 운영 정책이 별도 다룬다. +- **향후 도입 예정 — 알림 보존 기간 제한**: 현재 정책(평생 누적)은 단순함을 위한 초기 운영 정책이다. 후속 단계에서 *최근 N일* 기준 자동 삭제 정책이 도입될 예정이다(N의 구체 값은 운영 데이터 보고 결정). 도입 시 FR-021, Edge Cases를 개정 트리거로 본다.