From 904c53b886e2549bb401bd2cc632beb6bf83c1fe Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 16:55:18 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[docs]=20=ED=94=BC=EB=93=9C(Feed)=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20PRD=20=EC=97=AD=EC=84=A4=EA=B3=84?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20(specs/001-feed-features)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 중인 피드 도메인을 사용자 관점으로 정형화 (신규 기능 정의 아님) - User Story 5건 (P1: 작성/관리·둘러보기, P2: 반응/저장·작성 보조, P3: 개인화 추천) - Functional Requirements 25건 (FR-001 ~ FR-025) - 측정 가능한 Success Criteria 7건, Edge Cases 8건, Assumptions 8건 - 노출 모드 선택 규칙 명문화: 개인화 > 팔로잉 우선 > 기본 (코드 클래스명 Personalized / FollowingPriority / Basic 병기) - 신고 누적 시 노출 정책 명문화: 즉시 숨김 → 검수 큐 → 복원/영구 숨김 (신고 트리거 자체는 별도 신고 도메인 PRD에서 정의) - 책 도메인, 알림, 신고 트리거는 명시적으로 범위 외 처리 - 헌법 v1.0.0의 "API 계약 안정성" 원칙 반영 (#336 컨텍스트) --- .../checklists/requirements.md | 34 +++ specs/001-feed-features/spec.md | 203 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 specs/001-feed-features/checklists/requirements.md create mode 100644 specs/001-feed-features/spec.md diff --git a/specs/001-feed-features/checklists/requirements.md b/specs/001-feed-features/checklists/requirements.md new file mode 100644 index 000000000..acef85c49 --- /dev/null +++ b/specs/001-feed-features/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: 피드(Feed) 기능 + +**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) — 본문에 Spring/JPA/REST 등 구현 용어 미포함, 비즈니스 어휘만 사용 +- [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 — 2개 마커 모두 해소 (Q1: 알고리즘 선택 규칙 명문화 / Q2: 노출 정책 = 즉시 숨김 → 검수 큐 → 복원/영구 숨김, 트리거는 범위 외) +- [x] Requirements are testable and unambiguous — 모든 FR이 사용자 관찰 가능한 동작으로 기술 +- [x] Success criteria are measurable — SC 7개 모두 측정 가능한 결과 명시 +- [x] Success criteria are technology-agnostic — 응답시간/RPS 등 기술 임계치 배제 +- [x] All acceptance scenarios are defined — 5개 User Story 모두 Given-When-Then 시나리오 보유 +- [x] Edge cases are identified — Edge Cases 섹션에 8개 케이스 명시 +- [x] Scope is clearly bounded — Book/Report/Notification/Auth는 명시적으로 범위 외 처리 +- [x] Dependencies and assumptions identified — Assumptions 섹션에 8개 항목 기록 + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria — FR-001~FR-025가 User Story 시나리오와 매핑됨 +- [x] User scenarios cover primary flows — 작성/조회/반응/저장/추천/작성 보조 모두 커버 +- [x] Feature meets measurable outcomes defined in Success Criteria — SC가 P1~P3 시나리오와 정렬됨 +- [x] No implementation details leak into specification — 구현 디테일은 Assumptions에서 외부화 + +## Notes + +- 2026-05-17 1차 검증: 2개 마커 해소, 모든 항목 통과. `/speckit-clarify`(선택) 또는 `/speckit-plan`으로 진행 가능. diff --git a/specs/001-feed-features/spec.md b/specs/001-feed-features/spec.md new file mode 100644 index 000000000..5a73d5551 --- /dev/null +++ b/specs/001-feed-features/spec.md @@ -0,0 +1,203 @@ +# Feature Specification: 피드(Feed) 기능 + +**Feature Branch**: `001-feed-features` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "피드 관련 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "피드" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 구현 상세(스택·아키텍처)는 의도적으로 배제하고, 사용자가 무엇을(WHAT) 왜(WHY) 할 수 있어야 하는지에 집중한다. + +## User Scenarios & Testing *(mandatory)* + +THIP은 같은 책을 읽는 사람들이 감상을 공유하는 독서 커뮤니티이며, **피드**는 사용자가 작성한 독서 기록을 다른 사용자와 공유·발견·반응하는 핵심 면이다. 아래 사용자 스토리는 우선순위(P1 ~ P3)로 정렬되어 있으며, 각각 독립적으로 검증·배포 가능한 슬라이스다. + +### User Story 1 - 책에 대한 감상을 피드로 남기고 관리하기 (Priority: P1) + +사용자는 자신이 읽은 책에 대해 글·이미지·태그로 감상을 기록하고, 다른 사용자에게 공개할지 비공개로 둘지 선택한다. 작성한 피드는 언제든지 수정·삭제할 수 있어야 한다. + +**Why this priority**: 콘텐츠가 없으면 커뮤니티는 존재하지 않는다. 작성·수정·삭제는 다른 모든 시나리오의 전제 조건이다. + +**Independent Test**: 단일 사용자가 회원 가입 후 공개 피드 1건, 비공개 피드 1건을 각각 작성하고, 본문/이미지/태그를 수정한 뒤 삭제할 수 있다. 비공개 피드는 본인 외 다른 사용자에게 노출되지 않는다. + +**Acceptance Scenarios**: + +1. **Given** 로그인한 사용자가 책 한 권을 선택했을 때, **When** 본문·이미지(0장 이상)·태그·공개 여부를 입력해 피드 작성을 요청하면, **Then** 새 피드가 저장되고 사용자에게 생성된 피드 식별자가 반환된다. +2. **Given** 사용자가 작성한 피드가 존재할 때, **When** 본문/태그/이미지/공개 여부 중 하나 이상을 변경해 수정을 요청하면, **Then** 해당 피드의 내용이 갱신되며 연결된 책은 변경되지 않는다. +3. **Given** 비공개로 표시된 피드가 존재할 때, **When** 작성자가 아닌 사용자가 해당 피드를 조회하면, **Then** 시스템은 접근을 거부한다. +4. **Given** 사용자가 자신의 피드를 삭제하면, **When** 동일 피드를 다시 조회하면, **Then** 더 이상 어떤 사용자에게도 노출되지 않는다. +5. **Given** 다른 사용자의 피드가 존재할 때, **When** 본인이 아닌 사용자가 수정·삭제를 요청하면, **Then** 작업이 거부된다. + +--- + +### User Story 2 - 피드 둘러보기와 발견 (Priority: P1) + +사용자는 자신의 홈 피드, 특정 사용자의 피드, 그리고 특정 책에 대한 다른 사람들의 피드를 둘러보며 새로운 콘텐츠를 발견한다. 모든 목록은 부드럽게 이어지는 연속 스크롤(커서 기반 페이지) 경험을 제공해야 한다. + +**Why this priority**: 작성된 콘텐츠가 소비되지 않으면 작성자의 동기가 사라진다. 발견 경험은 리텐션의 근간이다. + +**Independent Test**: 여러 사용자의 공개 피드가 존재하는 환경에서, 사용자가 (a) 전체 피드, (b) 자신의 피드, (c) 다른 사용자의 공개 피드, (d) 특정 ISBN의 책에 대한 피드를 각각 커서 페이지로 끝까지 탐색할 수 있다. + +**Acceptance Scenarios**: + +1. **Given** 다수의 공개 피드가 존재할 때, **When** 사용자가 전체 피드 목록을 요청하면, **Then** 정해진 페이지 크기만큼의 피드와 다음 페이지를 가져올 수 있는 다음 커서가 함께 반환된다. +2. **Given** 사용자가 다음 커서로 다시 요청하면, **When** 이전 페이지에 포함된 피드는 제외되고 다음 묶음이 반환되며, **Then** 더 이상 데이터가 없으면 다음 커서가 비어있음으로 응답된다. +3. **Given** 다른 사용자의 비공개 피드가 존재할 때, **When** 그 사용자의 피드 목록을 조회하면, **Then** 비공개 피드는 결과에서 제외된다. +4. **Given** 특정 ISBN(13자리) 책에 대한 피드를 조회할 때, **When** 정렬 기준을 "좋아요 순" 또는 "최신 순"으로 지정하면, **Then** 해당 기준에 따라 정렬된 결과가 반환된다. +5. **Given** 사용자가 자신의 피드 화면 또는 다른 사용자의 피드 화면에 진입했을 때, **When** 상단 영역을 조회하면, **Then** 해당 사용자의 프로필 요약·팔로워 수·작성한 (공개) 피드 개수·(타인일 경우) 내가 팔로잉 중인지 여부가 함께 반환된다. + +--- + +### User Story 3 - 좋아요·저장으로 반응하고 다시 찾아보기 (Priority: P2) + +사용자는 마음에 드는 피드에 좋아요를 누르거나 저장(북마크)해두고, 저장한 피드는 별도의 목록에서 다시 확인한다. + +**Why this priority**: 작성자에 대한 사회적 보상(좋아요)과 사용자의 재방문 동기(저장)는 커뮤니티 성장 곡선을 결정한다. P1 이후 가장 큰 효과를 낸다. + +**Independent Test**: 사용자가 임의의 공개 피드에 좋아요/좋아요 취소를 토글했을 때 좋아요 수가 정확히 반영되고, 같은 피드를 저장/저장 해제했을 때 저장한 피드 목록에 정확히 나타나거나 사라진다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 공개 피드를 보고 있을 때, **When** 좋아요 상태를 ON으로 변경하면, **Then** 해당 피드의 좋아요 수가 1 증가하고 사용자의 좋아요 상태가 ON으로 반환된다. +2. **Given** 좋아요를 누른 상태에서, **When** 다시 좋아요 상태를 OFF로 변경하면, **Then** 해당 피드의 좋아요 수가 1 감소한다. +3. **Given** 동일 사용자가 같은 피드에 대해 좋아요 요청을 동시에 여러 번 보내더라도, **Then** 최종 좋아요 수는 의도된 한 번의 상태 변경만큼만 변한다(중복 누계 금지). +4. **Given** 비공개 피드에 대해, **When** 작성자가 아닌 사용자가 좋아요 또는 댓글 작성을 시도하면, **Then** 작업이 거부된다. +5. **Given** 사용자가 피드를 저장한 뒤, **When** 저장한 피드 목록을 조회하면, **Then** 해당 피드가 커서 페이지에 포함된다. **And** 저장을 해제하면 다음 조회부터 사라진다. + +--- + +### User Story 4 - 개인화된 피드 추천 (Priority: P3) + +전체 피드 목록은 단순 시간순이 아니라, 사용자가 팔로잉하는 사람·관심 도메인을 우선해서 노출함으로써 발견 효율을 높인다. 시스템은 사용자 상황(신규 사용자 / 활동 사용자)에 따라 적절한 노출 모드를 선택한다. + +**Why this priority**: P1·P2가 안정화된 이후 의미가 있다. 노출 모드 변경은 측정 가능한 효과(체류 시간·반응율)를 동반해야 한다. + +**Independent Test**: 동일한 피드 풀에서, (a) 팔로잉이 없는 신규 사용자와 (b) 활동 이력이 있는 사용자가 각자 전체 피드를 요청했을 때 노출 순서가 의도된 정책에 따라 달라진다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 다수의 다른 사용자를 팔로잉하고 있을 때, **When** 전체 피드 목록을 요청하면, **Then** 팔로잉하는 사용자의 피드가 비-팔로잉 피드보다 상단에 노출된다. +2. **Given** 사용자가 충분한 활동 이력을 가진 경우, **When** 전체 피드 목록을 요청하면, **Then** 시스템은 개인화된 노출 정책을 적용한다. +3. **Given** 사용자가 신규이거나 활동 이력이 적은 경우, **When** 전체 피드 목록을 요청하면, **Then** 시스템은 기본 노출 정책(시간순 또는 인기순)을 적용한다. + +**노출 모드 선택 규칙 (확정)**: + +본 시스템은 세 종류의 노출 모드를 가지며, 다음 단순 규칙으로 하나를 선택한다. 괄호 안은 현재 구현 클래스명(참조용). + +1. **기본 노출 모드 (`Basic`)**: 팔로잉이 임계치(N명) 미만인 사용자에게 적용. 시스템 전체에서 최신/인기 신호를 사용한다. +2. **팔로잉 우선 노출 모드 (`FollowingPriority`)**: 팔로잉이 임계치(N명) 이상인 사용자에게 적용. 팔로잉 대상자의 피드를 우선 노출하고 나머지를 보충한다. +3. **개인화 노출 모드 (`Personalized`)**: 위 두 모드의 상위 모드로, 사용자별 활동 누적치(좋아요·저장·체류 등)가 별도 임계치를 넘은 경우에만 적용 대상이 된다. 적용 트리거의 구체 임계치는 본 PRD 범위 외에서 정의·튜닝한다. + +임계치(N, 활동 누적치)의 구체 수치는 운영 데이터에 따라 조정 가능하며, 본 PRD는 결정 규칙만을 정의한다. 모드 우선순위는 개인화(자격이 있을 때) > 팔로잉 우선 > 기본 순으로 평가된다. + +--- + +### User Story 5 - 피드 작성을 위한 컨텍스트 제공 (Priority: P2) + +사용자가 피드 작성 화면에 진입했을 때, 본문에 첨부할 수 있는 카테고리·태그 목록과 이미지를 업로드할 수 있는 안전한 업로드 경로를 받는다. + +**Why this priority**: 작성 경험의 마찰을 낮춰 P1의 완료율을 끌어올리는 보조 시나리오. + +**Independent Test**: 작성 화면에서 사용 가능한 카테고리/태그 목록과, 이미지 업로드를 위한 직접 업로드 URL을 시스템에 요청해 받을 수 있다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 작성 화면을 열었을 때, **When** 작성 보조 정보를 요청하면, **Then** 선택 가능한 카테고리와 그 하위 태그 목록이 반환된다. +2. **Given** 사용자가 이미지 첨부를 시도할 때, **When** 업로드 URL 발급을 요청하면, **Then** 시스템은 일정 시간 동안만 유효한 직접 업로드 경로를 사용자별로 발급한다. +3. **Given** 발급된 업로드 경로로 업로드된 이미지가 아닌 임의의 외부 이미지 URL을 피드에 사용하려고 시도하면, **Then** 시스템은 거부한다. + +--- + +### Edge Cases + +- **비공개 피드**: 작성자만 조회/수정/삭제/좋아요/댓글이 가능하다. 다른 사용자에게는 존재가 노출되지 않는다(목록 제외 + 직접 조회 거부). +- **카운트 무결성**: 좋아요·댓글·저장의 토글이 동시 요청으로 발생해도 최종 수치는 정합해야 한다(작성자/뷰어 모두에게 일관된 값을 보여야 함). +- **댓글 카운트 언더플로우**: 어떤 경우에도 댓글 수가 음수가 되지 않는다. +- **삭제된 피드**: 삭제된 피드에 대한 후속 좋아요·댓글·저장 요청은 명확한 오류로 거부된다. +- **ISBN 입력 오류**: 13자리가 아닌 ISBN으로 책 관련 피드를 요청하면 잘못된 입력으로 응답한다. +- **이미지 소유권**: 다른 사용자에게 발급된 업로드 경로로 올린 이미지를 자신의 피드에 첨부하려는 시도는 거부된다. +- **수정 시 책 변경 시도**: 피드 수정 시 책(targetBook)을 다른 책으로 바꾸려는 요청은 거부된다. +- **신고 누적 시 노출 정책 (확정)**: 신고 누적치가 시스템 임계를 도달하면 해당 피드는 **즉시 일반 노출에서 숨김** 처리되어 어떤 사용자 목록·검색 결과에도 포함되지 않는다. 동시에 운영자 **검수 큐**에 자동 진입하며, 검수 결과에 따라 (a) **노출 복원** 또는 (b) **영구 숨김** 중 하나로 종료된다. 작성자에게는 검수 결과가 전달된다. 신고 행위 자체(트리거 API/사용자 흐름)는 본 PRD 범위 외(별도 신고 도메인 PRD)에서 정의한다. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 작성·수정·삭제 + +- **FR-001**: 사용자는 본문, (선택) 이미지 목록, (선택) 태그 목록, 공개/비공개 여부, 연결할 책(ISBN 기반)을 지정해 피드를 작성할 수 있어야 한다. +- **FR-002**: 시스템은 작성된 피드의 작성자, 작성 시각, 공개 여부, 연결 책을 영구적으로 보존해야 한다. +- **FR-003**: 작성자는 본인의 피드 본문·태그·공개 여부·이미지 목록을 수정할 수 있다. 연결 책은 수정 대상에서 제외한다. +- **FR-004**: 이미지 수정은 "삭제 또는 유지"만 가능하다(수정 요청 본문에는 최종적으로 남아야 할 이미지 식별자 집합만 전달되며, 새 이미지의 임의 추가는 별도 업로드 경로를 통과한 것에 한정). +- **FR-005**: 작성자는 본인의 피드를 삭제할 수 있다. 삭제 이후 해당 피드는 어떤 사용자에게도 노출되지 않는다. +- **FR-006**: 작성자가 아닌 사용자가 수정 또는 삭제를 시도하면 거부해야 한다. + +#### 조회·발견 + +- **FR-007**: 시스템은 사용자에게 다음 다섯 종류의 피드 목록을 제공한다: (a) 전체 피드, (b) 자신의 피드, (c) 특정 사용자의 공개 피드, (d) 특정 책(ISBN)에 연관된 피드, (e) 자신이 저장한 피드. +- **FR-008**: 모든 피드 목록은 커서 기반 페이지네이션을 사용하며, 응답에 다음 페이지를 위한 커서 또는 종료 신호를 포함해야 한다. +- **FR-009**: 특정 책에 연관된 피드 목록은 "좋아요 순(기본)"과 "최신 순" 정렬을 지원한다. +- **FR-010**: 시스템은 사용자가 단일 피드의 상세 정보(본문, 태그, 이미지, 좋아요 수, 댓글 수, 저장 여부, 좋아요 여부, 작성자 정보)를 조회할 수 있게 해야 한다. +- **FR-011**: 시스템은 피드 화면 상단을 위해 (a) 본인 또는 (b) 다른 사용자의 프로필 요약, 팔로워 수, 작성한 피드 수, (b의 경우) 내가 팔로잉 중인지 여부를 함께 제공해야 한다. +- **FR-012**: 비공개 피드는 어떤 목록·검색 결과에도 작성자 본인이 아닌 사용자에게 노출되어서는 안 된다. +- **FR-013**: 사용자가 단일 피드를 직접 조회할 때 비공개 피드라면 작성자 본인 외 사용자에게는 접근을 거부한다. + +#### 반응·저장 + +- **FR-014**: 사용자는 공개 피드에 좋아요를 토글할 수 있어야 한다(ON/OFF). +- **FR-015**: 비공개 피드에 대한 다른 사용자의 좋아요 또는 댓글 작성 시도는 거부되어야 한다. +- **FR-016**: 좋아요 카운트는 동시 토글 시나리오에서도 정합해야 한다(중복 증가/감소 없음). +- **FR-017**: 사용자는 임의의 공개 피드를 저장/저장 해제할 수 있으며, 저장된 피드는 자신만 볼 수 있는 별도 목록으로 제공되어야 한다. + +#### 작성 보조 + +- **FR-018**: 시스템은 피드 작성 화면을 위해 선택 가능한 카테고리와 하위 태그 목록을 제공해야 한다. +- **FR-019**: 피드에 첨부되는 이미지는 시스템이 발급한 직접 업로드 경로를 통해서만 업로드되어야 하며, 다른 사용자에게 발급된 경로로 업로드된 이미지를 자신의 피드에 첨부할 수 없다. +- **FR-020**: 이미지 업로드 경로는 한정된 시간 동안만 유효해야 한다. + +#### 노출 정책 + +- **FR-021**: 전체 피드 목록은 단순 시간순이 아니라, 사용자의 팔로잉·활동 이력을 반영해 우선순위를 적용한 결과를 반환할 수 있어야 한다. +- **FR-022**: 시스템은 사용자별로 다음 규칙으로 노출 모드를 선택한다. (1) 개인화 노출 모드(`Personalized`) 적용 자격(활동 누적치 임계치 이상)이 있으면 개인화 모드, (2) 그렇지 않고 팔로잉 수가 임계치 N 이상이면 팔로잉 우선 노출 모드(`FollowingPriority`), (3) 그 외에는 기본 노출 모드(`Basic`). 구체 임계치는 운영 튜닝 대상이며 본 PRD는 결정 규칙만 정의한다. + +#### 안전성·정합성 + +- **FR-023**: 시스템은 신고 누적치가 임계에 도달한 피드를 일반 노출에서 즉시 숨김 처리하고 운영자 검수 큐에 자동 진입시켜야 한다. 검수 결과에 따라 노출 복원 또는 영구 숨김 중 하나로 종료되며, 작성자에게 결과가 전달되어야 한다. 신고 트리거 자체는 본 PRD의 범위가 아니다. +- **FR-024**: 어떤 경우에도 피드의 댓글 수·좋아요 수가 음수가 될 수 없다. +- **FR-025**: ISBN으로 책 연관 피드를 조회할 때, 13자리 숫자가 아닌 입력은 잘못된 입력으로 거부되어야 한다. + +### Key Entities + +- **Feed (피드)**: 사용자가 책 한 권에 대해 남기는 감상 단위. 본문, 작성자, 연결된 책, 공개 여부, 좋아요 수, 댓글 수, 신고 수, 태그 목록, 이미지 목록을 가진다. +- **Tag (태그)**: 피드에 첨부되는 분류 키워드. 카테고리 하위에 속한다. +- **Image Content (이미지)**: 피드 본문에 첨부되는 이미지 자원. 업로드 시 사용자별 소유권을 가진다. +- **Saved Feed (저장된 피드)**: 사용자가 다시 보기 위해 북마크한 피드와 사용자 간 연결. +- **Book (책)**: 피드가 연관되는 외부 도서 자원. ISBN으로 식별된다(상세 정의는 본 PRD 범위 외). +- **User (사용자)**: 피드의 작성자/소비자. 팔로잉·팔로워 관계를 가진다(상세 정의는 본 PRD 범위 외). + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 사용자는 작성 화면 진입부터 피드 생성 완료까지 평균 60초 이내에 마칠 수 있다(이미지 0~3장 기준). +- **SC-002**: 사용자가 피드 목록(전체/내/타인/책별/저장) 첫 화면을 요청한 뒤, 95%의 요청이 사용자가 "지연 없이 떴다"고 인식하는 시간 내에 첫 결과를 받는다. +- **SC-003**: 동일 사용자가 같은 피드에 좋아요를 빠르게 반복 토글해도 최종 좋아요 수가 정확히 한 번의 변화만 반영한다(중복/누락 0건). +- **SC-004**: 비공개 피드가 작성자 외 사용자에게 노출되는 사건이 발생하지 않는다(노출 사건 0건). +- **SC-005**: 저장한 피드를 사용자가 다시 찾으려고 할 때, 저장 직후 또는 다음 세션에서 모두 동일한 결과를 본다(저장 직후 사라짐·중복 노출 0건). +- **SC-006**: 신규로 가입한 사용자가 최초 전체 피드를 조회한 뒤 1분 안에 최소 1개의 피드를 클릭/스크롤한다(콘텐츠 발견 효과). +- **SC-007**: 책별 피드 조회 시 사용자가 선택한 정렬 기준(좋아요 순 / 최신 순)을 변경했을 때, 사용자는 정렬이 적용되었음을 첫 화면에서 식별할 수 있다. + +> 본 PRD는 사용자 경험 차원의 성공 기준만 정의하며, 백엔드 응답 시간/RPS 등 기술 임계치는 헌법(constitution)의 성능 가드 원칙과 별도의 부하 시나리오에서 정의한다. + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 따라서 본 PRD가 도출하는 요구사항은 "구현되어 있어야 한다"가 아니라 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"이다. +- **인증 전제**: 모든 피드 시나리오는 인증된 사용자를 전제로 한다. 인증/인가의 상세 동작은 별도 PRD가 다룬다. +- **책(Book) 도메인은 외부 의존**: 피드는 ISBN을 통해 책을 참조한다. 책의 등록·수정·동기화는 별도 도메인 PRD가 정의한다. +- **신고(Report)는 별도 도메인**: 신고 트리거 API와 사용자 흐름은 별도 신고 도메인 PRD가 정의한다. 본 PRD는 신고 누적 결과(노출 정책: 즉시 숨김 → 검수 큐 → 복원/영구 숨김)만을 정의한다. +- **이미지 저장소**: 이미지 업로드/저장은 안전한 외부 객체 저장소를 사용한다고 가정한다. 구체적 저장소 선택은 본 PRD 범위 외다. +- **알림 연동**: 좋아요·댓글에 따른 알림 발생은 별도 알림 도메인이 다룬다. 본 PRD는 알림 발화 조건이 존재한다는 사실만 인정한다. +- **카테고리/태그 사전**: 작성 화면에서 보여지는 카테고리·태그는 시스템에 의해 사전 정의되어 있으며, 본 PRD는 그 편집 흐름을 정의하지 않는다. +- **페이지 크기**: 모든 커서 페이지의 기본 페이지 크기는 시스템 설정값으로 고정되며, 본 PRD는 그 값을 단정하지 않는다. From 908f5500e1af8f40b2b335d0e89406017d14423b Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 17:06:14 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[docs]=20=ED=8C=94=EB=A1=9C=EC=9A=B0(Follow?= =?UTF-8?q?)=20=EB=8F=84=EB=A9=94=EC=9D=B8=20PRD=20=EC=97=AD=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EC=9E=91=EC=84=B1=20(specs/002-follow-domain)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 중인 팔로우 도메인을 사용자 관점으로 정형화 (신규 기능 정의 아님) - User Story 5건 (P1: 토글·관계 둘러보기, P2: 팔로잉 최근 피드·알림 트리거, P3: 동시성·재시도 일관성) - Functional Requirements 16건 (FR-001 ~ FR-016) - 측정 가능한 Success Criteria 7건, Edge Cases 8건, Assumptions 8건 - 재시도 한계 초과 시 사용자 경험 명문화: 자원 경합 비노출, 일반 안내만 (운영자/개발자는 내부 코드·로그로 식별) - 알림 트리거 발화 보증(팔로우 1회당 1건, 언팔로우는 미발화) - 차단/비공개 계정/알림 도메인/사용자 라이프사이클은 명시적 범위 외 - 헌법 v1.0.0의 "API 계약 안정성"·"성능 가드" 원칙 반영 (#335 follow count 이슈, #336 중복 에러코드 컨텍스트) --- .specify/feature.json | 2 +- .../checklists/requirements.md | 34 ++++ specs/002-follow-domain/spec.md | 177 ++++++++++++++++++ 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 specs/002-follow-domain/checklists/requirements.md create mode 100644 specs/002-follow-domain/spec.md diff --git a/.specify/feature.json b/.specify/feature.json index 17fa44684..ddeb6e6eb 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/001-feed-features" + "feature_directory": "specs/002-follow-domain" } diff --git a/specs/002-follow-domain/checklists/requirements.md b/specs/002-follow-domain/checklists/requirements.md new file mode 100644 index 000000000..31c1ddb26 --- /dev/null +++ b/specs/002-follow-domain/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: 팔로우(Follow) 기능 + +**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) — 비관적 락/Retry/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: 재시도 소진 시 "잠시 후 다시 시도" 일반 안내, 자원 경합 원인 비노출) +- [x] Requirements are testable and unambiguous — 모든 FR이 관찰 가능한 결과로 기술 +- [x] Success criteria are measurable — SC 7건 모두 측정 가능 +- [x] Success criteria are technology-agnostic — 응답시간/RPS 등 기술 임계치 배제 +- [x] All acceptance scenarios are defined — 5개 User Story 모두 Given-When-Then 보유 +- [x] Edge cases are identified — Edge Cases 섹션 8건 +- [x] Scope is clearly bounded — 차단/비공개 계정, 알림 도메인, 사용자 라이프사이클 등 명시적 범위 외 처리 +- [x] Dependencies and assumptions identified — Assumptions 섹션 8건 + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria — FR-001~FR-016가 User Story 시나리오와 매핑 +- [x] User scenarios cover primary flows — 토글/조회/알림/동시성 모두 커버 +- [x] Feature meets measurable outcomes defined in Success Criteria — SC가 P1~P3 시나리오와 정렬 +- [x] No implementation details leak into specification — 구현 디테일은 Assumptions에서 외부화 + +## Notes + +- 2026-05-17 1차 검증: 마커 해소, 모든 항목 통과. `/speckit-clarify`(선택) 또는 `/speckit-plan`으로 진행 가능. diff --git a/specs/002-follow-domain/spec.md b/specs/002-follow-domain/spec.md new file mode 100644 index 000000000..f8e61dc8e --- /dev/null +++ b/specs/002-follow-domain/spec.md @@ -0,0 +1,177 @@ +# Feature Specification: 팔로우(Follow) 기능 + +**Feature Branch**: `002-follow-domain` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "follow 관련 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "팔로우" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 구현 상세(스택·아키텍처·재시도 메커니즘 등)는 의도적으로 배제하고, 사용자가 무엇을(WHAT) 왜(WHY) 할 수 있어야 하는지에 집중한다. + +## User Scenarios & Testing *(mandatory)* + +THIP은 같은 책을 읽는 사람들이 감상을 공유하는 독서 커뮤니티이며, **팔로우**는 사용자가 관심 있는 다른 사용자의 활동을 자신의 피드 흐름에 포함시키기 위해 사용하는 *비대칭 관계*다. 아래 사용자 스토리는 우선순위(P1~P3)로 정렬되어 있으며, 각각 독립적으로 검증·배포 가능한 슬라이스다. + +### User Story 1 - 다른 사용자를 팔로우/언팔로우하기 (Priority: P1) + +사용자는 다른 사용자의 프로필 또는 피드에서 팔로우/언팔로우 상태를 토글할 수 있다. 동작은 즉시 반영되며 동일 대상에 대한 빠른 반복 토글에서도 카운트가 정합해야 한다. + +**Why this priority**: 팔로우 관계가 없으면 피드 도메인의 "팔로잉 우선 노출 모드"가 의미를 잃는다. 본 시나리오는 커뮤니티 형성의 1차 트리거이며, 모든 후속 시나리오의 전제 조건이다. + +**Independent Test**: 두 사용자 A, B가 존재할 때 A가 B를 팔로우/언팔로우 상태로 토글했을 때 (1) A의 팔로잉 목록, (2) B의 팔로워 목록, (3) A→B 팔로잉 여부 확인, (4) B의 팔로워 수가 모두 일관된 결과를 보인다. + +**Acceptance Scenarios**: + +1. **Given** A가 B를 팔로우하지 않은 상태에서, **When** A가 B에 대한 팔로우 요청을 보내면, **Then** A→B 팔로잉 관계가 생성되고 B의 팔로워 수가 1 증가한다. +2. **Given** A가 B를 이미 팔로우하고 있을 때, **When** A가 B에 대한 언팔로우 요청을 보내면, **Then** A→B 팔로잉 관계가 해제되고 B의 팔로워 수가 1 감소한다. +3. **Given** A가 B를 이미 팔로우하고 있을 때, **When** A가 B에 대해 다시 팔로우 요청을 보내면, **Then** 작업은 "이미 팔로우 중" 오류로 거부되고 카운트는 변하지 않는다. +4. **Given** A가 B를 팔로우하지 않을 때, **When** A가 B에 대한 언팔로우 요청을 보내면, **Then** 작업은 "이미 언팔로우 상태" 오류로 거부되고 카운트는 변하지 않는다. +5. **Given** 사용자 A는 자기 자신에 대한 팔로우 또는 언팔로우 요청을 보낸다, **When** 어느 경우든, **Then** 작업이 거부된다. +6. **Given** 다수의 사용자가 동시에 B를 팔로우/언팔로우 한다, **When** 모든 요청이 처리된 뒤, **Then** B의 최종 팔로워 수는 실제 활성 팔로잉 행 수와 정확히 일치한다(중복 누계·누락 없음). + +--- + +### User Story 2 - 팔로우 관계 둘러보기 (Priority: P1) + +사용자는 자신의 팔로잉 목록과 임의 사용자의 팔로워 목록을 조회하고, 특정 사용자에 대한 자신의 팔로잉 여부를 즉시 확인할 수 있다. + +**Why this priority**: 토글 행동만 가능하고 결과 가시화가 없으면 사용자는 자신의 관계 상태를 신뢰할 수 없다. 가시화는 토글과 함께 출시되어야 의미가 있다. + +**Independent Test**: A가 사용자 B, C를 팔로우한 상태에서 (1) A의 팔로잉 목록에 B와 C가, (2) B와 C의 팔로워 목록에 A가, (3) A→B is-following 확인이 true로 반환된다. + +**Acceptance Scenarios**: + +1. **Given** A가 다수의 사용자를 팔로우하고 있을 때, **When** A가 자신의 팔로잉 목록을 요청하면, **Then** 활성 팔로잉 대상의 프로필 요약 목록이 반환된다. +2. **Given** B가 다수의 팔로워를 가진 상태에서, **When** 임의 사용자가 B의 팔로워 목록을 요청하면, **Then** B를 팔로우 중인 사용자들의 프로필 요약 목록이 반환된다. +3. **Given** A가 B를 팔로우 중이거나 아닌 상태에서, **When** A가 B에 대한 팔로잉 여부 확인을 요청하면, **Then** 시스템은 현재 관계 상태를 단일 값(true/false)으로 반환한다. + +--- + +### User Story 3 - 내가 팔로우한 사람들의 최근 활동 확인 (Priority: P2) + +사용자는 자신이 팔로우한 사용자들의 최근 피드만을 빠르게 확인해 "지금 무슨 이야기를 하는지" 알 수 있다. + +**Why this priority**: 팔로우의 가치는 결국 피드 노출로 이어진다. 별도 진입 경로(예: 홈 상단 위젯, 팔로잉 전용 탭)에서 즉시 확인 가능해야 발견 효율이 의미를 가진다. + +**Independent Test**: A가 사용자 B와 C를 팔로우한 상태에서 B와 C가 새 피드를 작성한 뒤, A가 "내 팔로잉의 최근 피드" 화면을 열면 B와 C의 피드가 최근순으로 함께 보인다(공개 피드 한정). + +**Acceptance Scenarios**: + +1. **Given** A가 다수의 사용자를 팔로우 중이고 그들 중 일부가 최근 공개 피드를 작성했을 때, **When** A가 "팔로잉 최근 활동" 화면을 요청하면, **Then** 활성 팔로잉 대상의 최근 공개 피드 묶음이 반환된다. +2. **Given** A가 팔로우 중인 사용자가 비공개 피드를 작성했을 때, **When** A가 동일 화면을 요청하면, **Then** 해당 비공개 피드는 결과에서 제외된다(공개 정책은 피드 PRD를 따름). +3. **Given** A가 누구도 팔로우하지 않을 때, **When** A가 동일 화면을 요청하면, **Then** 빈 결과 또는 안내가 일관되게 반환된다. + +--- + +### User Story 4 - 팔로우 시 상대에게 알림이 전달되는 경험 (Priority: P2) + +사용자가 팔로우될 때 상대 사용자는 그 사실을 알 수 있어야 한다(시스템 알림 메커니즘을 통해). 본 PRD는 알림 도메인의 상세를 정의하지 않으며, "팔로우 행위가 알림 발화 트리거가 된다"는 사실만 보증한다. + +**Why this priority**: 팔로우의 사회적 효과(상호 인지·맞팔)는 알림 발화 여부에 크게 좌우된다. 알림은 별도 도메인이지만, 트리거 발화 누락은 본 도메인 책임이다. + +**Independent Test**: B가 사용자 A에 의해 팔로우될 때 알림 시스템에 발화 이벤트가 정확히 1회 도달한다(언팔로우 시 알림 미발화). + +**Acceptance Scenarios**: + +1. **Given** A가 B를 처음 팔로우할 때, **When** 팔로우 요청이 성공하면, **Then** 알림 도메인에 "팔로우됨" 트리거가 정확히 1회 발화된다. +2. **Given** A가 이전에 B를 팔로우/언팔로우한 이력이 있고 다시 팔로우할 때, **When** 새 팔로우 요청이 성공하면, **Then** 알림 트리거가 다시 1회 발화된다. +3. **Given** A가 B에게 언팔로우 요청을 할 때, **When** 요청이 성공하면, **Then** 알림 트리거는 발화되지 않는다. + +--- + +### User Story 5 - 동시 토글 및 일시적 장애 상황에서의 일관된 사용자 경험 (Priority: P3) + +사용자가 빠르게 같은 대상을 반복 토글하거나, 시스템 일시적 부하로 1차 처리가 실패한 경우에도 사용자는 "최종 상태가 무엇인지" 명확히 알 수 있어야 한다. + +**Why this priority**: 평상시에는 P1/P2로 충분하지만, 트래픽 피크 또는 사용자가 빠르게 탭하는 상황에서 카운트 깨짐·중복 알림은 신뢰 손상으로 직결된다. + +**Independent Test**: 동일 사용자가 짧은 시간 안에 같은 대상에 대해 N번 토글했을 때 최종 관계 상태가 마지막 사용자 의도와 일치하며, 팔로워 수가 정합하다. + +**Acceptance Scenarios**: + +1. **Given** A가 짧은 시간 안에 B에 대해 팔로우 → 언팔로우 → 팔로우를 빠르게 요청했을 때, **When** 모든 요청이 처리된 후, **Then** 최종 관계 상태는 "팔로우", 팔로워 수는 1번의 팔로우만 반영된다(중복 증가 0건). +2. **Given** 시스템이 일시적 자원 경합 상태로 1차 처리에 실패하더라도, **When** 사용자에게 응답이 돌아왔을 때, **Then** 응답에는 "성공" 또는 "사용자가 다시 시도해야 함을 명확히 안내하는 오류" 중 하나만 포함되며, "성공으로 보이지만 실제로는 처리 안 됨" 상태는 발생하지 않는다. +3. **Given** 동시 토글로 인해 카운트 갱신이 직렬화될 때, **When** 모든 요청이 처리된 뒤, **Then** B의 팔로워 수는 활성 팔로잉 관계 수와 정확히 일치한다. + +**재시도 한계 초과 시 사용자 경험 (확정)**: + +시스템은 일시적 경합을 흡수하기 위해 내부 자동 재시도를 수행한다. 모든 자동 재시도가 소진되어도 처리에 실패한 경우, 사용자에게는 **"잠시 후 다시 시도해주세요" 형태의 일반 안내**만 노출된다. 자원 경합 같은 시스템 내부 원인은 사용자에게 노출하지 않는다. 운영자와 개발자는 내부 오류 코드·로그를 통해 원인을 식별한다. + +--- + +### Edge Cases + +- **자기 자신 팔로우 시도**: 시스템은 작업을 거부한다. +- **이미 팔로우 중인 대상에 대한 중복 팔로우**: 작업은 거부되며 카운트는 변하지 않는다. +- **존재하지 않는 팔로우 관계에 대한 언팔로우**: 작업은 거부되며 카운트는 변하지 않는다. +- **동시 팔로우 토글**: 동일 (요청자, 대상) 쌍에 대해 중복 관계 행이 절대 생성되지 않는다(데이터베이스 차원의 유일성 보장 포함). +- **카운트 무결성**: 어떤 동시성·재시도 시나리오에서도 팔로워 수가 실제 활성 관계 수와 일치한다. +- **재시도 후 최종 실패**: 시스템이 내부 재시도 한계를 모두 소진해도 처리에 실패하면 사용자에게는 "잠시 후 다시 시도해주세요" 형태의 일반 안내가 반환되며 자원 경합 원인은 비노출된다. 부분 상태(관계는 생성됐는데 카운트는 안 올라간 상태 등)는 절대 남지 않는다. +- **자체 알림 발화 방지**: 자기 자신이 자신에게 알림을 보내지 않는다(자기 팔로우가 거부되므로 자연 충족). +- **삭제·탈퇴 사용자**: 어느 한쪽이 탈퇴한 사용자라면 팔로잉/팔로워 목록에 그가 노출되지 않거나 명확히 안내된다(상세 정책은 사용자 라이프사이클 도메인을 따름). + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 팔로우 토글 + +- **FR-001**: 사용자는 임의의 다른 사용자를 대상으로 팔로우 또는 언팔로우 상태를 토글할 수 있어야 한다. +- **FR-002**: 시스템은 사용자 본인을 대상으로 한 팔로우/언팔로우 요청을 거부해야 한다. +- **FR-003**: 시스템은 이미 팔로우 중인 사용자에 대한 중복 팔로우 요청을 거부하고, 팔로우 관계가 없는 사용자에 대한 언팔로우 요청을 거부해야 한다. +- **FR-004**: 시스템은 동일 (요청자, 대상) 쌍에 대해 중복된 활성 팔로잉 관계가 절대 존재하지 않도록 보장해야 한다(데이터 차원의 유일성). + +#### 카운트와 동시성 + +- **FR-005**: 팔로우/언팔로우의 결과로 대상 사용자의 팔로워 수가 정확히 1씩 증가/감소해야 한다. +- **FR-006**: 동시 다중 요청, 빠른 반복 토글 시에도 대상 사용자의 팔로워 수는 활성 팔로잉 관계 수와 항상 일치해야 한다(누락·중복 증가 0). +- **FR-007**: 어떤 경우에도 팔로워 수가 음수가 되어서는 안 된다. +- **FR-008**: 일시적 자원 경합으로 1차 처리에 실패한 경우 시스템은 자동 재시도로 흡수를 시도한다. 모든 자동 재시도가 실패한 경우 사용자에게는 자원 경합 원인을 비노출한 채 "잠시 후 다시 시도해주세요" 형태의 일반 안내만 반환한다. 부분 상태(관계 생성 ↔ 카운트 미반영)는 발생하지 않아야 한다. 내부 원인 식별을 위한 시스템 차원의 오류 코드·로그는 별도로 유지한다. + +#### 조회 + +- **FR-009**: 사용자는 임의 사용자의 팔로워 목록을 조회할 수 있어야 한다. +- **FR-010**: 사용자는 자신의 팔로잉 목록을 조회할 수 있어야 한다. +- **FR-011**: 사용자는 특정 대상 사용자에 대한 자신의 팔로잉 여부(true/false)를 단일 호출로 확인할 수 있어야 한다. +- **FR-012**: 사용자는 자신이 팔로우한 사용자들의 최근 공개 피드 묶음을 단일 진입점에서 확인할 수 있어야 한다(공개 정책은 피드 PRD를 따름). + +#### 알림 연동 + +- **FR-013**: 팔로우 요청이 성공한 경우, 시스템은 알림 도메인에 "팔로우됨" 트리거를 정확히 1회 발화해야 한다. +- **FR-014**: 언팔로우 요청이 성공한 경우, 시스템은 알림 트리거를 발화하지 않아야 한다. +- **FR-015**: 알림 트리거의 메시지 내용·전달 방식·수신자 설정 우선순위는 본 PRD의 범위가 아니며 알림 도메인 PRD를 따른다. + +#### 사용자 라이프사이클 연동 + +- **FR-016**: 탈퇴·삭제된 사용자는 다른 사용자의 팔로잉/팔로워 목록 결과에서 노출되지 않거나, 노출되더라도 라이프사이클 상태를 명확히 안내해야 한다(상세 정책은 사용자 라이프사이클 도메인 PRD를 따름). + +### Key Entities + +- **Following (팔로잉 관계)**: "사용자 A가 사용자 B를 팔로우 중"이라는 *방향 있는* 관계. (요청자, 대상) 쌍에 대해 활성 관계가 최대 1건 존재한다. +- **User (사용자)**: 팔로잉 관계의 주체이자 대상. 본 PRD는 사용자가 보유한 *팔로워 수* 속성만을 의미 있게 다룬다(상세 사용자 정의는 본 PRD 범위 외). +- **Follow Notification Trigger (팔로우 알림 트리거)**: 팔로우 성공의 사후 효과로 발화되는 추상 이벤트. 본 PRD는 발화 조건만 정의하며, 알림 자체는 알림 도메인 PRD가 다룬다. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 사용자가 임의 대상의 팔로우 버튼을 눌렀을 때, 사용자가 "지연 없이 반영됐다"고 인식하는 시간 내에 변경된 관계 상태(true/false)와 즉시 일치하는 화면 갱신을 본다. +- **SC-002**: 동일 사용자가 같은 대상에 대해 짧은 시간 안에 토글을 N회 반복했을 때 최종 카운트 차이는 마지막 사용자 의도와 정확히 일치한다(중복 증가·누락 0건). +- **SC-003**: 임의 사용자의 팔로워 수와 "그를 팔로우 중인 사용자 목록의 크기"가 항상 일치한다(불일치 사건 0건). +- **SC-004**: 동시 N건의 팔로우/언팔로우 요청 부하 상황에서 모든 요청은 (성공 / 명확한 오류) 중 하나로 종결되며 "성공 응답을 받았는데 실제로는 처리되지 않은" 상태는 발생하지 않는다. +- **SC-005**: 팔로우 성공 1건당 알림 도메인에 도달하는 "팔로우됨" 트리거는 정확히 1건이다(누락·중복 0건). +- **SC-006**: 사용자가 "내 팔로잉의 최근 피드" 화면을 열었을 때, 자신이 팔로우 중인 사용자들의 공개 피드만 결과에 포함되어 있음을 신뢰할 수 있다(비공개 피드 노출 0건). +- **SC-007**: 시스템이 일시적 경합으로 1차 실패했을 때, 자동 재시도로 흡수되어 사용자에게는 정상 응답으로 보이는 비율이 일정 수준 이상(운영 기준치)이다. 자동 재시도가 모두 실패한 경우에도 사용자에게는 항상 정확한 결과(성공 또는 명시적 오류)가 전달된다. + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 요구사항은 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"의 형태로 읽힌다. +- **인증 전제**: 모든 팔로우 시나리오는 인증된 사용자를 전제로 한다. 인증/인가의 상세 동작은 별도 PRD가 다룬다. +- **사용자 라이프사이클 외부 의존**: 탈퇴·삭제·차단된 사용자에 대한 노출 정책은 별도 사용자 라이프사이클/계정 정책 도메인 PRD가 정의한다. 본 PRD는 그 결과만을 인정한다. +- **차단(Block)·비공개 계정(Private Account)은 범위 외**: 현재 THIP에는 명시적인 차단·비공개 계정 기능이 없다. 향후 도입 시 본 PRD는 영향을 받는다(별도 개정 트리거). +- **알림(Notification)은 별도 도메인**: 본 PRD는 "팔로우 성공이 알림 트리거를 발화한다"는 사실만 보증한다. 트리거의 메시지·푸시 채널·수신자 설정은 알림 도메인 PRD가 정의한다. +- **피드(Feed)와의 연동**: 본 PRD가 다루는 "팔로잉 최근 피드" 화면은 피드 도메인의 공개 정책을 그대로 따른다. 본 PRD는 노출 규칙을 중복 정의하지 않는다. +- **카운트 정합성**: 본 PRD는 *사용자가 관찰 가능한 일관성*만 정의한다. 일관성을 달성하기 위한 구체 메커니즘(락 전략, 재시도 정책, 인덱스/제약)은 구현 영역이며 본 PRD의 범위가 아니다. +- **목록 페이지네이션**: 팔로워/팔로잉 목록의 페이지네이션 정책(커서 사용 여부, 페이지 크기)은 본 PRD가 단정하지 않는다. 다른 목록 도메인(피드 등)과의 일관성을 따른다. From c4c008b4a377193dbfab737e7225aea70086edf2 Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 17:29:04 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[docs]=20=EC=B1=85(Book)=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20PRD=20=EC=97=AD=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(specs/003-book-domain)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 중인 책 도메인을 사용자 관점으로 정형화 (신규 기능 정의 아님) - User Story 5건 (P1: 검색·상세·저장, P2: 방 생성용 책 선택·인기/모집 발견) - Functional Requirements 18건 (FR-001 ~ FR-018) - 측정 가능한 Success Criteria 7건, Edge Cases 8건, Assumptions 8건 - 외부 도서 데이터 소스(현재 Naver Book API)는 벤더 중립으로 다룸 - 인기 검색 책 산정 기준 명문화: 전날 책 상세 조회 호출 수 기준, 개인화 없음, 일 단위 경계 이후 갱신 (주의: 신호가 키워드 검색이 아닌 '상세 조회'라는 점) - 사용되지 않는 책 자동 정리(BookCleanUpService)의 안전 조건 명시 (현재 참조 중인 책이 사라지지 않아야 함) - 외부 데이터 일시 장애 시 사용자 경험은 팔로우 PRD와 동일 정책 (자원/외부 원인 비노출, "잠시 후 다시 시도" 일반 안내) - 방·피드·최근 검색어·메타 갱신 정책은 명시적 범위 외 --- .../checklists/requirements.md | 35 ++++ specs/003-book-domain/spec.md | 191 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 specs/003-book-domain/checklists/requirements.md create mode 100644 specs/003-book-domain/spec.md diff --git a/specs/003-book-domain/checklists/requirements.md b/specs/003-book-domain/checklists/requirements.md new file mode 100644 index 000000000..4107cdbc7 --- /dev/null +++ b/specs/003-book-domain/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: 책(Book) 기능 + +**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) — 벤더(Naver), Spring/캐시 구현 용어 본문 미포함, 비즈니스 어휘로 기술 +- [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: 전날 상세 조회 호출 수 기준 / 개인화 없음 / 일 단위 갱신) +- [x] Requirements are testable and unambiguous — 모든 FR이 관찰 가능한 결과로 기술 +- [x] Success criteria are measurable — SC 7건 모두 측정 가능 +- [x] Success criteria are technology-agnostic — 응답시간 절대치/RPS 등 기술 임계치 배제 +- [x] All acceptance scenarios are defined — 5개 User Story 모두 Given-When-Then 보유 +- [x] Edge cases are identified — Edge Cases 섹션 8건 +- [x] Scope is clearly bounded — 외부 벤더, 최근 검색어, 방, 피드, 메타 갱신 정책 등 명시적 범위 외 처리 +- [x] Dependencies and assumptions identified — Assumptions 섹션 8건 + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria — FR-001~FR-018가 User Story 시나리오와 매핑 +- [x] User scenarios cover primary flows — 검색/상세/저장/선택/발견 모두 커버 +- [x] Feature meets measurable outcomes defined in Success Criteria — SC가 P1~P3 시나리오와 정렬 +- [x] No implementation details leak into specification — 구현 디테일은 Assumptions에서 외부화 + +## Notes + +- 2026-05-17 1차 검증: 마커 해소, 모든 항목 통과. `/speckit-clarify`(선택) 또는 `/speckit-plan`으로 진행 가능. +- 주의: 인기 기준 신호가 *키워드 검색*이 아닌 *상세 조회 호출*이라는 점. 화면 라벨이 "인기 검색 책"이라 사용자가 검색 횟수로 오해할 여지가 있어 라벨 검토는 별도 백로그. diff --git a/specs/003-book-domain/spec.md b/specs/003-book-domain/spec.md new file mode 100644 index 000000000..58e6709e2 --- /dev/null +++ b/specs/003-book-domain/spec.md @@ -0,0 +1,191 @@ +# Feature Specification: 책(Book) 기능 + +**Feature Branch**: `003-book-domain` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "book 관련 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "책" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 외부 도서 데이터 연동(Naver Book API)이 존재하지만, 구현 상세(스택·API 벤더·캐시 메커니즘)는 의도적으로 배제하고, 사용자가 무엇을(WHAT) 왜(WHY) 할 수 있어야 하는지에 집중한다. + +## User Scenarios & Testing *(mandatory)* + +THIP은 같은 책을 읽는 사람들이 감상을 공유하는 독서 커뮤니티이며, **책**은 모든 다른 도메인(피드·방·검색·저장)의 *공유 자원*이다. 사용자는 책을 검색·탐색·저장하며, 책은 다른 도메인이 참조하는 식별자(ISBN)로 사용된다. + +### User Story 1 - 책 검색하기 (Priority: P1) + +사용자는 키워드(제목·저자 등)로 책을 검색해 결과 목록과 페이지를 넘기며 원하는 책을 찾는다. 검색 결과는 외부 도서 데이터 소스가 있어도 사용자에게는 단일한 결과로 보인다. + +**Why this priority**: 검색이 없으면 사용자는 피드 작성·방 생성·저장 등 모든 후속 행동의 시작점을 잃는다. 책 도메인의 출입구. + +**Independent Test**: 임의의 키워드로 검색했을 때 페이지 단위 결과를 받을 수 있고, 페이지를 넘기면 다음 결과가, 키워드가 비어있거나 페이지 범위를 벗어나면 명확한 오류가 응답된다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 검색 키워드를 입력했을 때, **When** 1페이지 결과를 요청하면, **Then** 결과 묶음(책 제목·저자·표지 등 요약 정보)과 추가 페이지가 있는지 여부가 함께 반환된다. +2. **Given** 사용자가 이미 1페이지를 받은 상태에서, **When** 다음 페이지를 요청하면, **Then** 이전과 중복되지 않는 다음 묶음이 반환된다. +3. **Given** 사용자가 빈 키워드(공백만)로 검색을 요청하면, **Then** 작업이 거부된다. +4. **Given** 사용자가 1보다 작은 페이지 번호로 검색을 요청하면, **Then** 작업이 거부된다. +5. **Given** 사용자가 결과가 존재하는 키워드로 페이지 범위를 벗어난 페이지를 요청하면, **Then** 페이지 범위 초과 오류로 응답된다. +6. **Given** 사용자가 검색 입력을 *확정*했음을 명시(`isFinalized=true`)할 때, **When** 검색이 성공하면, **Then** 시스템은 해당 키워드를 사용자별 *최근 검색어*로 기록한다. 입력 중(`isFinalized=false`)에는 기록하지 않는다. + +--- + +### User Story 2 - 책 상세 정보 보기 (Priority: P1) + +사용자는 특정 ISBN의 책 상세 정보(제목·저자·출판사·소개·표지·쪽수·베스트셀러 여부 등)와 함께, 이 책과 관련된 자신의 상태(저장 여부 등)를 한 번의 요청으로 확인한다. + +**Why this priority**: 피드 작성·방 생성·저장 등 모든 후속 사용자 액션은 "내가 보고 있는 책이 무엇인지" 가 확정된 상태에서 시작한다. + +**Independent Test**: 임의의 13자리 ISBN으로 상세를 요청하면 책 기본 정보와 사용자 컨텍스트(저장 여부 등)가 함께 반환된다. 13자리 숫자가 아닌 입력은 거부된다. + +**Acceptance Scenarios**: + +1. **Given** 시스템에 이미 알려진 책의 ISBN이 주어졌을 때, **When** 상세 정보를 요청하면, **Then** 책의 표준 메타데이터와 사용자 컨텍스트(저장 여부 등)가 함께 반환된다. +2. **Given** 외부 데이터 소스에는 존재하지만 시스템에 처음 조회되는 책일 때, **When** 상세 정보를 요청하면, **Then** 시스템은 외부에서 메타데이터를 가져와 응답하고, 이후 동일 ISBN 조회에 대비해 자체적으로 기록한다. +3. **Given** ISBN 형식이 13자리 숫자가 아닐 때, **When** 상세 요청을 보내면, **Then** 잘못된 입력으로 거부된다. +4. **Given** 외부 데이터 소스에서도 찾을 수 없는 ISBN일 때, **When** 상세 요청을 보내면, **Then** 책 없음 오류로 응답된다. + +--- + +### User Story 3 - 책 저장하고 다시 찾아보기 (Priority: P1) + +사용자는 마음에 드는 책을 저장(북마크)해두고, 저장한 책 목록을 별도 화면에서 다시 확인한다. 저장 상태는 ON/OFF로 토글된다. + +**Why this priority**: 저장은 재방문 동기의 근간이며, "방 생성 시 책 선택" 흐름(User Story 4)의 입력 소스다. + +**Independent Test**: 사용자가 임의의 책을 저장하면 저장 목록에 나타나고, 저장 해제하면 사라진다. 동일 책에 대한 빠른 토글에도 최종 상태가 사용자 의도와 일치한다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 어떤 책의 상세 또는 검색 결과에서 저장 ON을 요청할 때, **When** 작업이 성공하면, **Then** 해당 책이 사용자의 저장 목록에 포함되고 응답에 현재 저장 상태가 반환된다. +2. **Given** 이미 저장된 책에 대해 저장 OFF를 요청할 때, **When** 작업이 성공하면, **Then** 해당 책이 저장 목록에서 제외된다. +3. **Given** 사용자가 임의 책에 대해 짧은 시간 안에 저장 ON/OFF를 반복 토글할 때, **When** 모든 요청이 처리된 뒤, **Then** 최종 저장 상태는 마지막 사용자 의도와 정확히 일치하며 카운트가 어긋난 상태(저장됨처럼 보이는데 목록에는 없음 등)는 발생하지 않는다. +4. **Given** 사용자가 자신의 저장한 책 목록을 요청할 때, **Then** 저장한 책들이 커서 기반 페이지로 반환된다. + +--- + +### User Story 4 - 방 생성을 위한 책 선택 (Priority: P2) + +사용자가 새 독서 방을 만들 때, 책 선택 화면에서는 (a) 자신이 저장한 책 또는 (b) 자신이 이미 참여 중인 방의 책을 선택할 수 있다. 두 종류는 사용자 선택으로 전환 가능하다. + +**Why this priority**: 방 도메인의 입력 의존성을 책 도메인이 명시적으로 제공한다는 점에서 핵심 보조 시나리오. P1 직후 가장 큰 효과. + +**Independent Test**: `type=SAVED`로 요청하면 저장한 책 목록이, `type=JOINING`으로 요청하면 참여 중인 방의 책 목록이 동일한 페이지네이션 인터페이스로 반환된다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 책 1권 이상을 저장한 상태에서, **When** `type=SAVED`로 선택가능 책 목록을 요청하면, **Then** 저장한 책의 커서 페이지 결과가 반환된다. +2. **Given** 사용자가 1개 이상의 방에 참여 중인 상태에서, **When** `type=JOINING`으로 선택가능 책 목록을 요청하면, **Then** 참여 중 방의 책 목록이 커서 페이지로 반환된다. +3. **Given** `type` 값이 정의된 값(SAVED/JOINING)이 아닐 때, **Then** 잘못된 입력으로 거부된다. + +--- + +### User Story 5 - 인기 검색 책과 책으로 모집 중인 방 발견 (Priority: P2) + +사용자는 다른 사용자들이 많이 검색하고 있는 책을 둘러보거나, 특정 책으로 현재 모집 중인 방을 살펴봄으로써 새 콘텐츠와 모임을 발견한다. + +**Why this priority**: 검색·저장의 *능동적* 행동을 채워주는 *수동적* 발견. P1·P2가 안정된 뒤 리텐션을 끌어올린다. + +**Independent Test**: (a) 인기 검색 책 화면을 열면 사용자별로 의미 있는 책 묶음이 반환되고, (b) 특정 ISBN의 모집 중 방 화면을 열면 그 책으로 현재 모집 상태인 방들이 커서 페이지로 반환된다. + +**Acceptance Scenarios**: + +1. **Given** 시스템에 검색 통계가 누적된 상태에서, **When** 사용자가 인기 검색 책을 요청하면, **Then** 사용자에게 의미 있는 정해진 분량의 인기 책 묶음이 반환된다. +2. **Given** 특정 책으로 현재 모집 중인 방이 존재할 때, **When** 사용자가 해당 책 ISBN으로 모집 중 방을 요청하면, **Then** 모집 상태인 방의 커서 페이지 결과가 반환된다. 책이 존재하지 않거나 모집 중인 방이 없으면 빈 결과 또는 안내가 일관되게 반환된다. + +**"인기 검색 책" 산정 기준 (확정)**: + +- **신호**: 책 상세 조회(상세 정보 보기) 호출 횟수. *키워드 검색*이 아닌, *상세 페이지를 실제로 열어본* 행동을 인기로 본다(검색 입력만으로는 인기로 잡지 않음). +- **윈도우**: 전날(직전 1일) 한정. 같은 책이 오늘 0회 조회되었어도 어제 많이 조회되었으면 오늘의 인기로 노출. +- **개인화**: 없음. 모든 사용자에게 동일한 결과가 노출된다. +- **갱신 시점**: 일 단위 경계(자정) 이후 첫 호출부터 새 결과가 반영된다(정확한 갱신 메커니즘은 본 PRD 범위 외). + +--- + +### Edge Cases + +- **빈 키워드·잘못된 페이지 번호**: 빈 키워드 또는 1 미만 페이지는 즉시 거부된다. +- **페이지 범위 초과**: 결과 총량을 초과하는 페이지 요청은 명확한 페이지 범위 초과 오류로 응답된다. +- **잘못된 ISBN 형식**: 13자리 숫자가 아닌 ISBN은 모든 책 API에서 거부된다. +- **외부 데이터 소스 일시 장애**: 외부 도서 데이터 소스가 응답하지 않을 때 사용자에게 "잠시 후 다시 시도해주세요" 형태의 일반 안내가 전달된다. +- **신규 책 자동 등록**: 사용자가 시스템에 처음 보는 책의 상세를 조회할 때, 시스템은 그 책을 자체적으로 기록해 다음부터 빠르게 응답할 수 있어야 한다. +- **연결이 없는 책 정리**: 어떤 사용자·피드·방도 더는 참조하지 않는 책 데이터는 시스템 차원에서 주기적으로 정리되며, 정리 결과로 사용자가 *현재 참조하고 있는* 책 상세가 사라지는 일은 발생해서는 안 된다. +- **저장 토글의 정합성**: 빠른 반복 토글 시에도 최종 상태가 마지막 사용자 의도와 일치하며, "저장 응답을 받았는데 목록에는 없음" 같은 부분 상태가 발생하지 않는다. +- **외부 데이터의 메타 변경**: 외부 데이터 소스에서 같은 ISBN의 책 메타데이터(쪽수 등)가 갱신되었을 때, 시스템이 이를 따라가는지 여부는 본 PRD 범위 외(별도 정책)다. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 검색·발견 + +- **FR-001**: 사용자는 키워드 + 페이지 번호로 책을 검색할 수 있어야 한다. +- **FR-002**: 시스템은 빈 키워드·1 미만 페이지·결과 범위 초과 페이지 요청을 명확한 오류로 거부해야 한다. +- **FR-003**: 사용자가 검색 입력을 *확정*한 경우에만 시스템은 해당 키워드를 사용자별 최근 검색어로 기록한다. 입력 중인 상태에서는 기록하지 않는다. +- **FR-004**: 사용자는 "현재 인기 있는 검색 책" 묶음을 단일 호출로 받을 수 있어야 한다. 인기는 *전날(직전 1일)* 동안의 *책 상세 조회 호출 횟수* 기준으로 산정하며(키워드 검색 횟수 아님), 모든 사용자에게 동일한 결과로 노출된다. 새 결과는 일 단위 경계 이후 반영된다. +- **FR-005**: 사용자는 특정 ISBN의 책에 대해 *현재 모집 상태인 방* 목록을 커서 페이지로 받을 수 있어야 한다. + +#### 상세 + +- **FR-006**: 사용자는 ISBN으로 책의 상세 정보(제목·저자·출판사·소개·표지·쪽수·베스트셀러 여부 등 표준 메타데이터)를 조회할 수 있어야 한다. +- **FR-007**: 상세 조회 응답은 사용자 컨텍스트(예: 저장 여부)를 함께 포함해야 한다(별도 호출 없이 단일 응답으로 화면 구성 가능). +- **FR-008**: 시스템에 알려지지 않은 ISBN이라도 외부 도서 데이터에 존재하면 응답되어야 하며, 시스템은 다음 동일 ISBN 조회를 위해 자체적으로 기록해 두어야 한다. +- **FR-009**: 시스템과 외부 데이터 어디에도 존재하지 않는 ISBN은 "책 없음" 오류로 응답되어야 한다. +- **FR-010**: 모든 책 관련 API의 ISBN 입력은 13자리 숫자 형식만 허용한다. + +#### 저장 + +- **FR-011**: 사용자는 임의 책의 저장 상태를 ON/OFF로 토글할 수 있어야 한다. +- **FR-012**: 사용자는 자신이 저장한 책의 목록을 커서 페이지로 조회할 수 있어야 한다. +- **FR-013**: 저장 토글은 동시 다중 요청과 빠른 반복 토글 상황에서도 정합해야 한다(저장된 것처럼 보이는데 목록에 없음 등의 부분 상태 금지). + +#### 다른 도메인 입력 제공 + +- **FR-014**: 시스템은 방 생성 흐름을 위해 (a) `SAVED`(저장한 책) 또는 (b) `JOINING`(사용자가 참여 중인 방의 책) 두 종류의 책 선택 목록을 커서 페이지로 제공해야 한다. +- **FR-015**: `SAVED`/`JOINING` 외의 `type` 값은 잘못된 입력으로 거부되어야 한다. + +#### 데이터 정리 + +- **FR-016**: 시스템은 어떤 사용자·피드·방도 더 이상 참조하지 않는 책 데이터를 주기적으로 정리해야 한다. +- **FR-017**: 정리 작업으로 인해 *현재 어떤 도메인이 참조하고 있는* 책 데이터가 사라져서는 안 된다. + +#### 외부 데이터 안정성 + +- **FR-018**: 외부 도서 데이터 소스 호출이 실패한 경우 사용자에게는 자원 경합·외부 장애 원인을 비노출한 채 "잠시 후 다시 시도해주세요" 형태의 일반 안내를 반환한다(팔로우 도메인 PRD의 재시도 정책과 일관). 내부 식별을 위한 시스템 차원의 오류 코드·로그는 별도로 유지한다. + +### Key Entities + +- **Book (책)**: 도서의 표준 메타데이터를 보관하는 단위. ISBN을 자연 식별자로 가진다. 제목·저자·출판사·표지·쪽수·소개·베스트셀러 여부 등을 가진다. +- **Saved Book (저장한 책)**: 사용자가 다시 보기 위해 북마크한 책과 사용자 간 연결. +- **Recent Search (최근 검색어)**: 사용자가 *확정한* 검색 키워드의 사용자별 기록(상세 정의는 별도 도메인 PRD). +- **External Book Source (외부 도서 데이터)**: 시스템 외부의 표준 도서 메타데이터 공급원. 본 PRD는 *벤더 중립*으로 다룬다. +- **Room (방)**: 책별 모집중인 방 목록의 출처. 상세는 방 도메인 PRD가 정의. +- **Feed (피드)**: 책별 피드 목록의 출처. 상세는 피드 도메인 PRD(#354)가 정의. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 사용자가 키워드 검색을 했을 때 95% 이상의 요청이 사용자가 "지연 없이 떴다"고 인식하는 시간 내에 첫 페이지 결과를 받는다. +- **SC-002**: 사용자가 시스템에 처음 본 책의 상세를 두 번째로 조회할 때, 첫 번째보다 빠른 응답을 받는다(자체 기록이 사용됨). +- **SC-003**: 동일 사용자가 같은 책에 대해 저장 ON/OFF를 빠르게 반복 토글해도, 최종 저장 목록과 응답된 저장 상태가 어긋나는 사건이 0건이다. +- **SC-004**: 사용자가 "내 저장한 책 목록"을 조회했을 때 반환된 목록과 각 책의 "저장됨" 상태 응답이 항상 일치한다(불일치 사건 0건). +- **SC-005**: 외부 도서 데이터 소스 일시 장애가 있을 때 사용자가 "장애 원인"을 인지하는 응답을 받는 사건이 0건이다(사용자에게는 일반 안내만 전달). +- **SC-006**: 시스템에 더 이상 참조되지 않는 책 데이터가 누적되어 저장소 운영을 위협하지 않는다(정리 잡이 실제로 동작함을 운영 지표로 확인). +- **SC-007**: 방 생성 흐름에서 책 선택 화면을 진입한 사용자가 "내가 저장한 책 또는 참여 중인 방의 책 중에서" 최소 1권을 선택하는 비율이 일정 수준 이상이다(흐름 효율 지표). + +> 본 PRD는 사용자 경험 차원의 성공 기준만 정의하며, 백엔드 응답 시간/RPS/외부 API 호출 비용 등 기술 임계치는 헌법(constitution)의 성능 가드 원칙과 별도의 부하 시나리오·운영 지표에서 정의한다. + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 요구사항은 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"의 형태로 읽힌다. +- **인증 전제**: 모든 책 시나리오는 인증된 사용자를 전제로 한다. 인증/인가의 상세 동작은 별도 PRD가 다룬다. +- **외부 도서 데이터 소스는 외부 의존**: 표준 도서 메타데이터의 1차 공급원은 외부 도서 데이터 서비스이다. 본 PRD는 그 *벤더 중립적* 인터페이스 행동만을 다루며, 구체 벤더(현재 Naver Book API)는 본 PRD의 변경 트리거가 아니다. +- **최근 검색어는 별도 도메인**: 최근 검색어의 저장·만료·조회 정책은 별도 검색 이력 도메인 PRD가 정의한다. 본 PRD는 *언제 기록되는지*(`isFinalized=true`인 검색 성공 시점)만 명시한다. +- **방·피드는 외부 도메인**: 책으로 연결되는 방 목록·피드 목록은 각 도메인 PRD를 따른다. 본 PRD는 그 *입력 식별자(ISBN)*만 제공한다. +- **메타데이터 갱신 정책**: 외부 데이터 소스에서 메타데이터가 갱신되었을 때 시스템이 이를 따라가는 빈도·정책은 본 PRD의 범위 외다. 다만 *시스템에 한 번 기록된 후* 사용자가 그 책을 다시 조회할 때 *그 시점의 시스템 기록*을 보게 됨을 인정한다. +- **데이터 정리의 빈도**: 사용되지 않는 책 데이터 정리 잡의 실행 주기는 운영 결정 사항이며 본 PRD가 단정하지 않는다. +- **페이지네이션 일관성**: 책 도메인의 목록(저장한 책·선택가능 책·모집중인 방)은 다른 도메인(피드·팔로우)과 동일하게 커서 기반을 따른다. From a704056556c7ef135a01ec32817a22d498e23ffb Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 17:42:31 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[docs]=20=EB=B0=A9=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80(RoomPost)=20-=20=EA=B8=B0=EB=A1=9D=C2=B7=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20PRD=20=EC=97=AD?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EC=9E=91=EC=84=B1=20(specs/004-roompost-d?= =?UTF-8?q?omain)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 중인 RoomPost 도메인(기록·투표 한정)을 사용자 관점으로 정형화 - 오늘의 한마디(AttendanceCheck)는 사용자 정의에 따라 별도 도메인으로 명시적 범위 외 처리 - User Story 5건 (P1: 기록 작성·투표 만들기/참여·목록 조회, P2: 피드 핀·AI 독후감) - Functional Requirements 25건 (FR-001 ~ FR-025) - 측정 가능한 Success Criteria 7건, Edge Cases 9건, Assumptions 10건 - 총평 조건 차이 명문화: 기록은 책 마지막 페이지에서만, 투표는 책 진행률 80% 이상에서만 (코드 상 실제 차이) - 투표 참여 정합성: 한 사용자가 한 투표에 정확히 한 선택지, 항목 변경 시 카운트 이동, 진행 중 방에서만 참여 가능 - 방-게시글 소속 검증: 수정·삭제·핀 시 방 ID와 게시글 방 ID 일치 필수 - AI 사용량 한도 정책 명문화: 전역(모든 방 합산) 평생 누적 5회, 리셋 없음 기록 작성 횟수는 안내용 (한도 게이트 아님) - 방·책·피드·댓글/좋아요·신고·알림은 명시적 범위 외 --- .../checklists/requirements.md | 35 +++ specs/004-roompost-domain/spec.md | 211 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 specs/004-roompost-domain/checklists/requirements.md create mode 100644 specs/004-roompost-domain/spec.md diff --git a/specs/004-roompost-domain/checklists/requirements.md b/specs/004-roompost-domain/checklists/requirements.md new file mode 100644 index 000000000..a32a5d89c --- /dev/null +++ b/specs/004-roompost-domain/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: 방 게시글(RoomPost) — 기록·투표 + +**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) — Spring AI / 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: 전역 평생 누적 5회 / 리셋 없음 / 기록 작성 횟수는 안내용) +- [x] Requirements are testable and unambiguous — 모든 FR이 관찰 가능한 결과로 기술 +- [x] Success criteria are measurable — SC 7건 모두 측정 가능 +- [x] Success criteria are technology-agnostic — 응답시간 절대치/RPS 등 기술 임계치 배제 +- [x] All acceptance scenarios are defined — 5개 User Story 모두 Given-When-Then 보유 +- [x] Edge cases are identified — Edge Cases 섹션 9건 +- [x] Scope is clearly bounded — 오늘의 한마디, 방, 책, 피드, 댓글·좋아요 행위는 명시적 범위 외 +- [x] Dependencies and assumptions identified — Assumptions 섹션 10건 + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria — FR-001~FR-024가 User Story 시나리오와 매핑 +- [x] User scenarios cover primary flows — 기록 작성/투표 작성·참여/목록 조회/핀/AI 모두 커버 +- [x] Feature meets measurable outcomes defined in Success Criteria — SC가 P1~P3 시나리오와 정렬 +- [x] No implementation details leak into specification — 구현 디테일은 Assumptions에서 외부화 + +## Notes + +- 2026-05-17 1차 검증: 마커 해소, 모든 항목 통과. `/speckit-clarify`(선택) 또는 `/speckit-plan`으로 진행 가능. +- 의도적 보존: 총평 조건이 기록(전체 페이지 일치)과 투표(진행률 80%+)에서 다르다는 점은 코드 상의 실제 정책 차이를 그대로 PRD에 명문화한 것임. 의도 외 차이라면 코드 또는 PRD 한쪽 정정 필요. diff --git a/specs/004-roompost-domain/spec.md b/specs/004-roompost-domain/spec.md new file mode 100644 index 000000000..24b173ff0 --- /dev/null +++ b/specs/004-roompost-domain/spec.md @@ -0,0 +1,211 @@ +# Feature Specification: 방 게시글(RoomPost) — 기록(Record) · 투표(Vote) + +**Feature Branch**: `004-roompost-domain` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "RoomPost 기록 투표 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "방 게시글(RoomPost)" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 본 PRD가 다루는 RoomPost는 두 종류로 한정한다: **기록(Record)**과 **투표(Vote)**. 같은 컨트롤러에 함께 라우팅되는 "오늘의 한마디(AttendanceCheck)"는 사용자 정의에 따라 별도 도메인으로 분리하여 본 PRD의 범위에서 제외한다. + +## User Scenarios & Testing *(mandatory)* + +THIP의 **방**은 같은 책을 함께 읽는 사용자 모임이며, 방 안에서 참여자들은 두 종류의 게시글을 만든다. 기록은 *자기 진행도에 대한 일지·감상*이고, 투표는 *함께 묻고 답하는 짧은 합의 도구*다. 본 PRD의 사용자 스토리는 우선순위(P1~P3)로 정렬되어 있으며, 각각 독립적으로 검증·배포 가능한 슬라이스다. + +### User Story 1 - 기록(Record) 작성·수정·삭제 (Priority: P1) + +방 참여자는 자신이 읽은 페이지에 대한 기록을 남기고, 작성자 본인 한정으로 본문을 수정하거나 기록을 삭제한다. *총평*으로 표시된 기록은 책의 마지막 페이지에서만 작성될 수 있다. + +**Why this priority**: 기록이 없으면 방은 의미를 잃는다. THIP의 핵심 가치 제안. + +**Independent Test**: 방 참여자 A가 임의 페이지에 기록을 작성·수정·삭제할 수 있다. 방 비참여자는 작성이 거부된다. 총평은 마지막 페이지에서만 작성 가능하고, 총평이 아닌 기록은 1~마지막 페이지 범위 내에서만 작성 가능하다. + +**Acceptance Scenarios**: + +1. **Given** A가 어떤 방의 참여자일 때, **When** A가 본문과 페이지를 지정해 기록 작성을 요청하면, **Then** 새 기록이 저장되고 생성된 기록 식별자가 반환된다. +2. **Given** A가 본인이 작성한 기록을 수정 요청한다, **When** 본문 내용만을 변경한다, **Then** 기록 본문이 갱신된다. 페이지·총평 여부·방·작성자는 수정 대상이 아니다. +3. **Given** B가 A가 작성한 기록을 수정 또는 삭제하려고 시도하면, **Then** 작업이 거부된다. +4. **Given** 기록 작성 요청의 페이지가 1보다 작거나 책 전체 페이지 수를 초과하면, **Then** 페이지 범위 오류로 거부된다. +5. **Given** 총평 플래그가 켜진 기록 작성 요청이 들어왔는데 페이지가 책의 마지막 페이지와 다르면, **Then** 총평 작성 조건 불충족으로 거부된다. +6. **Given** A가 작성한 기록이 존재할 때, **When** A가 동일 방·자신의 기록 ID로 삭제를 요청하면, **Then** 기록이 제거되고 이후 목록·상세에서 노출되지 않는다. +7. **Given** 기록이 어떤 방에 속해 있을 때, **When** 사용자가 *다른 방* 식별자로 동일 기록의 수정·삭제·핀을 요청하면, **Then** 작업이 거부된다(방-기록 소속 검증). + +--- + +### User Story 2 - 투표(Vote) 만들기·수정·삭제 그리고 투표하기 (Priority: P1) + +방 참여자는 짧은 질문과 선택지 묶음으로 투표를 생성하고, 다른 참여자는 그 투표에 참여한다(한 사용자는 한 투표에 정확히 하나의 선택지만 선택; 다른 선택지로 바꾸려면 항목을 변경한다; 더 이상 의견이 없으면 투표를 취소한다). 투표는 *진행 중인 방*에서만 가능하다. + +**Why this priority**: 가벼운 합의 도구는 방의 활성도를 직접적으로 끌어올린다. 기록과 함께 P1로 묶임. + +**Independent Test**: 방 참여자 A가 투표를 만들고, B가 그 투표에 참여·항목 변경·취소를 수행할 때 각 단계마다 항목별 카운트와 비율이 정합하게 유지된다. + +**Acceptance Scenarios**: + +1. **Given** A가 방의 참여자일 때, **When** A가 질문과 2개 이상의 선택지를 지정해 투표 생성을 요청하면, **Then** 새 투표가 저장되고 식별자가 반환된다. +2. **Given** 방이 만료되었거나 진행 중이 아닐 때, **When** 임의 참여자가 투표 참여를 요청하면, **Then** 작업이 거부된다. +3. **Given** B가 어떤 투표에 한 번도 참여하지 않은 상태에서, **When** B가 임의 선택지에 "투표하기"를 요청하면, **Then** 선택지의 카운트가 1 증가하고 B는 그 투표의 참여자로 기록된다. +4. **Given** B가 이미 어떤 선택지에 투표한 상태에서, **When** B가 동일 투표의 *다른* 선택지에 "투표하기"를 요청하면, **Then** 이전 선택지의 카운트가 1 감소하고 새 선택지의 카운트가 1 증가하며, B의 참여 기록은 새 선택지로 갱신된다(중복 카운트 없음). +5. **Given** B가 어떤 선택지에 투표한 상태에서, **When** B가 그 선택지에 대해 "투표 취소"를 요청하면, **Then** 선택지의 카운트가 1 감소하고 B의 참여 기록이 제거된다. +6. **Given** B가 어떤 선택지에 투표하지 않은 상태에서, **When** B가 그 선택지에 대해 "투표 취소"를 요청하면, **Then** 작업이 거부된다. +7. **Given** A가 본인이 만든 투표를 수정한다, **When** 본문 내용만을 변경한다, **Then** 투표 본문이 갱신된다. 페이지·총평 여부·선택지·방·작성자는 수정 대상이 아니다. +8. **Given** B가 A가 만든 투표를 수정·삭제하려고 시도하면, **Then** 작업이 거부된다. +9. **Given** 투표가 *총평*으로 표시되어 생성될 때, **When** 작성 시 책 진행률이 80% 미만이면, **Then** 작성이 거부된다. + +--- + +### User Story 3 - 방의 게시글 목록 둘러보기 (Priority: P1) + +방 참여자는 자신이 속한 방의 게시글(기록·투표)을 한 화면에서 둘러본다. 기본은 "그룹 기록"(다른 참여자들의 게시글 포함), 별도 모드로 "내 기록"만 보기를 선택할 수 있다. 정렬·페이지 범위 필터·총평만 보기 필터·커서 페이지를 지원한다. + +**Why this priority**: 작성된 게시글이 발견되지 않으면 작성자의 동기가 사라진다. 목록은 작성 직후의 첫 소비 진입점. + +**Independent Test**: 방의 게시글이 다수 있을 때 (a) "group"으로 요청하면 자신을 포함한 참여자들의 게시글이, (b) "mine"으로 요청하면 본인 게시글만 반환된다. 정렬 옵션과 페이지 범위·총평 필터 모두 의도된 결과를 보인다. + +**Acceptance Scenarios**: + +1. **Given** 방에 다수의 기록·투표가 있을 때, **When** 사용자가 그룹 모드(`type=group`)로 목록을 요청하면, **Then** 참여자들의 게시글이 정해진 정렬 기준에 따라 커서 페이지로 반환된다. 정렬 기준은 "최신순(기본) / 좋아요 많은 순 / 댓글 많은 순" 중 하나여야 한다. +2. **Given** 사용자가 내 모드(`type=mine`)로 목록을 요청한다, **When** 어떤 요청이든, **Then** 본인의 게시글만 *페이지 높은 순* 고정 정렬로 반환된다(이 모드에서 정렬 옵션은 무시된다). +3. **Given** 사용자가 페이지 범위 필터를 활성화(`isPageFilter=true`)하고 `pageStart`·`pageEnd`를 지정한다, **When** 목록을 요청하면, **Then** 그 페이지 범위에 속하는 게시글만 반환된다. 필터가 비활성일 때는 전체가 대상이다. +4. **Given** 사용자가 총평만 보기(`isOverview=true`)를 선택한다, **When** 목록을 요청하면, **Then** 총평으로 표시된 게시글만 반환된다. +5. **Given** 사용자가 방 비참여자일 때, **When** 그 방의 게시글 목록을 요청하면, **Then** 작업이 거부된다. + +--- + +### User Story 4 - 마음에 든 내 기록을 피드로 옮겨 공유하기 (Priority: P2) + +사용자가 방에 작성한 기록 중 외부(피드)에도 공유하고 싶은 것이 있으면, *작성자 본인 한정*으로 그 기록을 피드에 핀(연결)할 수 있다. 본 PRD는 핀 가능 여부 검증과 핀에 필요한 책 정보 제공 흐름까지를 다루며, *피드 작성 자체*의 상세는 피드 도메인 PRD(#354)를 따른다. + +**Why this priority**: 방-피드를 잇는 사용자 동선. 방 안 콘텐츠가 더 넓은 커뮤니티로 흘러가는 경로. + +**Independent Test**: 사용자가 본인 기록의 핀 진입점에 진입하면 (a) 핀 가능 여부와 (b) 핀에 필요한 책 정보가 함께 응답된다. 본인이 아닌 기록·다른 방 식별자로의 핀 요청은 거부된다. + +**Acceptance Scenarios**: + +1. **Given** A가 본인이 작성한 기록 ID와 그 기록이 속한 방 ID를 지정해 핀 진입을 요청한다, **When** 시스템이 권한·소속을 검증하면, **Then** 핀에 필요한 책 정보(이후 피드 작성 단계에 사용)가 응답된다. +2. **Given** A가 본인이 아닌 사용자의 기록을 핀하려 시도한다, **Then** 작업이 거부된다. +3. **Given** A가 *다른 방*의 ID로 자신의 기록을 핀하려 시도한다, **Then** 방-기록 소속 검증 실패로 거부된다. + +--- + +### User Story 5 - AI 독후감 생성과 사용량 안내 (Priority: P2) + +사용자는 자신이 한 방에서 작성한 기록들을 바탕으로 AI 도움을 받아 독후감 형태의 콘텐츠를 생성한다. 시스템은 사용자별 AI 이용 횟수와 기록 작성 횟수를 화면에 안내해 사용자가 한도를 인지할 수 있게 한다. + +**Why this priority**: AI 보조는 종이를 채우는 마찰을 낮추는 보조 시나리오. 본질적 가치는 P1에 있지만 활용 폭을 넓힌다. + +**Independent Test**: 사용자가 (a) 자신의 AI 사용량/기록 작성 횟수를 조회할 수 있고, (b) 어떤 방의 자신의 기록을 바탕으로 AI 독후감 생성을 요청할 수 있다. 사용량이 한도를 넘으면 추가 요청이 거부된다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 어떤 방의 참여자일 때, **When** 사용자가 그 방에서의 AI 이용 횟수와 기록 작성 횟수를 조회하면, **Then** 그 두 수치가 응답된다. +2. **Given** 사용자가 AI 한도 내에 있을 때, **When** 사용자가 자신의 기록들에 기반한 AI 독후감 생성을 요청하면, **Then** 생성 결과가 응답되며 AI 사용량이 1 증가한다. +3. **Given** 사용자가 이미 AI 한도에 도달했을 때, **When** 사용자가 추가 AI 독후감 생성을 요청하면, **Then** 작업이 한도 초과로 거부된다. +4. **Given** 사용자가 방 비참여자일 때, **When** 그 방의 AI 독후감 생성 또는 사용량 조회를 요청하면, **Then** 작업이 거부된다. + +**AI 사용량 한도 정책 (확정)**: + +- **한도**: 사용자당 **평생 누적 5회**의 AI 독후감 생성을 허용한다. +- **적용 단위**: **전역**. 사용자가 참여하는 *모든 방을 합산*해 산정한다(방별 별도 한도가 아님). +- **리셋**: **없음**. 한 번 사용한 횟수는 어떤 주기로도 회복되지 않는다. +- **기록 작성 횟수와의 관계**: 기록 작성 횟수는 사용자에게 *안내 정보*로만 노출되며 AI 한도 산정에는 사용되지 않는다(게이트 아님). +- **한도 도달 후**: 추가 AI 독후감 생성 요청은 거부되며, 사용자에게는 한도 도달 사실을 인지할 수 있는 명확한 안내가 전달된다. + +--- + +### Edge Cases + +- **방 비참여자의 작성/조회**: 방에 참여하지 않은 사용자의 기록·투표 작성, 목록 조회, AI 사용은 거부된다. +- **만료/종료된 방**: 방이 진행 중이 아니면 투표 참여가 거부된다(기록 작성·조회·AI 정책은 방 라이프사이클 PRD를 따른다). +- **방-게시글 소속 불일치**: 게시글의 수정·삭제·핀 요청 시 요청에 포함된 방 ID와 실제 게시글의 방 ID가 다르면 작업이 거부된다. +- **페이지 범위 위반**: 1보다 작거나 책 전체 페이지를 초과하는 페이지에 대한 게시글 작성은 거부된다. +- **총평 조건 위반**: *기록*의 총평은 책의 마지막 페이지에서만, *투표*의 총평은 책 진행률 80% 이상일 때만 작성된다. 두 종류 사이에 의도된 조건 차이가 있다. +- **카운트 무결성**: 좋아요·댓글·투표 항목 카운트는 동시 요청 시에도 정합한다(중복 누계·누락 0건). 댓글 수가 0 미만으로 내려가는 일은 없다. +- **빠른 투표 항목 변경**: 사용자가 짧은 시간 안에 한 투표 안에서 항목을 여러 번 바꿔도, 최종 상태는 마지막 선택지에 정확히 1표가 누적된 결과가 된다(이전 선택지의 카운트는 모두 회수되어 있음). +- **존재하지 않는 참여 취소**: 참여 기록이 없는 선택지에 대한 "투표 취소" 요청은 거부된다. +- **삭제 후의 후속 작업**: 삭제된 게시글에 대한 좋아요·댓글·핀·AI 요청은 명확한 오류로 거부된다. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 공통 (Record · Vote 공히) + +- **FR-001**: 게시글 작성·수정·삭제·핀은 모두 방 참여자만 가능해야 한다. 비참여자의 시도는 거부된다. +- **FR-002**: 게시글의 수정·삭제·핀 요청에서 요청에 포함된 방 ID는 게시글이 실제 속한 방과 일치해야 한다. 불일치 시 작업이 거부된다. +- **FR-003**: 게시글 작성 시 페이지는 1 이상이며 책 전체 페이지 수 이하여야 한다. +- **FR-004**: 게시글의 수정은 본문 변경만을 허용한다. 페이지·총평 여부·방·작성자·연결 책은 수정 대상이 아니다. +- **FR-005**: 게시글의 좋아요·댓글·(투표의 경우) 항목 카운트는 어떤 동시성 시나리오에서도 실제 활성 상태와 일치해야 한다. 댓글 수는 음수가 될 수 없다. + +#### 기록(Record) + +- **FR-006**: 사용자는 방 안에서 본문·페이지·총평 여부를 지정해 기록을 작성할 수 있다. +- **FR-007**: 기록의 총평(`isOverview=true`)은 *책의 마지막 페이지*에서만 작성될 수 있다. 그 외 페이지로 총평을 작성하면 거부된다. +- **FR-008**: 기록의 작성자는 본인 기록의 본문을 수정할 수 있다. 본인이 아니면 거부된다. +- **FR-009**: 기록의 작성자는 본인 기록을 삭제할 수 있다. 본인이 아니면 거부된다. +- **FR-010**: 삭제된 기록은 어떤 사용자에게도 노출되지 않는다. + +#### 투표(Vote) + +- **FR-011**: 사용자는 방 안에서 본문·페이지·총평 여부·복수의 선택지를 지정해 투표를 생성할 수 있다. +- **FR-012**: 투표의 총평(`isOverview=true`)은 *책 진행률이 80% 이상*인 페이지에서만 생성될 수 있다. 그 외 페이지로 총평을 생성하면 거부된다. +- **FR-013**: 투표 참여는 *진행 중인 방*에서만 가능하다. 만료·종료된 방에서의 참여 요청은 거부된다. +- **FR-014**: 한 사용자는 한 투표에 대해 정확히 하나의 선택지에만 투표 상태를 가진다. 다른 선택지에 다시 "투표하기"를 요청하면 이전 선택지의 카운트가 회수되고 새 선택지의 카운트가 증가하며 사용자 참여 기록이 새 선택지로 갱신된다. +- **FR-015**: 투표 취소는 사용자가 *참여한* 선택지에 대해서만 가능하다. 참여하지 않은 선택지의 취소 요청은 거부된다. +- **FR-016**: 투표 생성자는 본인 투표의 본문을 수정·삭제할 수 있다. 본인이 아니면 거부된다. +- **FR-017**: 투표 항목별 카운트는 모든 참여자의 활성 참여 수와 일치해야 한다. 빠른 항목 변경 시에도 중복 누계·누락은 발생하지 않는다. + +#### 목록 조회 + +- **FR-018**: 사용자는 자신이 참여한 방의 게시글(기록·투표) 목록을 단일 진입점에서 조회할 수 있다. 비참여자의 요청은 거부된다. +- **FR-019**: 목록 조회는 다음 모드를 지원해야 한다: (a) `group`(기본; 참여자들의 게시글, 정렬은 최신/좋아요/댓글 중 선택), (b) `mine`(본인 게시글만, *페이지 높은 순* 고정 정렬, 정렬 옵션은 무시). +- **FR-020**: 목록 조회는 페이지 범위 필터(`isPageFilter`, `pageStart`, `pageEnd`)와 총평만 보기 필터(`isOverview`)를 독립적으로 적용할 수 있어야 한다. +- **FR-021**: 목록 조회는 커서 기반 페이지네이션을 사용한다(다른 도메인 목록과 일관). + +#### 피드 핀 + +- **FR-022**: 사용자는 본인이 작성한 기록을 피드로 핀하기 위한 진입점에서, 핀 가능 여부와 핀에 필요한 책 정보를 단일 호출로 받아야 한다. 실제 피드 작성은 피드 도메인 PRD를 따른다. + +#### AI 보조 + +- **FR-023**: 사용자는 자신이 한 방에서 작성한 기록들을 바탕으로 AI 독후감 생성을 요청할 수 있다. 사용자가 평생 누적 AI 사용 한도(5회)에 도달했거나 방 비참여자라면 거부된다. +- **FR-024**: 시스템은 사용자별로 (a) 모든 방을 합산한 *전역 AI 사용 횟수*와 (b) 방 단위 *기록 작성 횟수*를 조회 가능하게 노출해야 한다. AI 한도는 사용자당 평생 누적 5회이며 리셋되지 않는다. 기록 작성 횟수는 안내 정보이며 한도 산정에 사용되지 않는다. +- **FR-025**: 사용자가 AI 한도(5회)에 도달한 경우, 추가 생성 요청은 거부되며 사용자에게 한도 도달 사실을 인지할 수 있는 명확한 안내가 전달되어야 한다. + +### Key Entities + +- **RoomPost (방 게시글)**: 방 안에 작성되는 게시글의 추상. 기록과 투표를 통합한다. 좋아요 수·댓글 수·작성자·방·페이지·총평 여부를 가진다. +- **Record (기록)**: 책의 한 페이지(혹은 마지막 페이지)에 대한 개인 감상·일지. 본문·페이지·총평 여부를 가진다. 마지막 페이지에서만 총평이 가능하다. +- **Vote (투표)**: 방 안에서 짧은 질문에 대한 합의 도구. 본문·페이지·총평 여부·복수의 선택지를 가진다. 진행 중인 방에서만 참여가 가능하고, 진행률 80% 이상에서만 총평이 가능하다. +- **Vote Item (투표 선택지)**: 투표에 속한 선택지. 이름과 카운트를 가진다. +- **Vote Participant (투표 참여자)**: 한 사용자가 한 투표에서 선택한 선택지를 가리키는 연결. (사용자, 투표) 쌍에 대해 최대 1건 존재한다. +- **AI Usage (AI 사용량)**: 사용자의 *전역* AI 독후감 생성 누적 횟수(평생, 리셋 없음)와 *방 단위* 기록 작성 횟수의 집계. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 방 참여자가 기록 또는 투표 작성을 진입한 뒤 평균 60초 이내에 작성을 완료할 수 있다(본문·페이지·총평 여부 선택 기준). +- **SC-002**: 어떤 사용자가 동일 투표에 대해 빠르게 항목을 N회 바꿔도, 최종 항목별 카운트 합은 그 투표의 활성 참여자 수와 항상 일치한다(불일치 사건 0건). +- **SC-003**: 어떤 사용자도 자신이 참여하지 않은 방의 기록·투표 작성·조회·핀·AI 호출에 성공할 수 없다(권한 누수 0건). +- **SC-004**: 비공개 권한 정책 위반(타인 기록/투표 수정·삭제, 방-게시글 소속 불일치)이 발생한 사건이 0건이다. +- **SC-005**: "그룹 기록" 모드와 "내 기록" 모드의 결과는 서로의 정렬 정책을 침범하지 않는다(내 모드는 페이지 높은 순 고정, 그룹 모드는 사용자가 선택한 정렬을 그대로 반영). +- **SC-006**: 사용자가 자신의 *전역* AI 이용 횟수와 방별 기록 작성 횟수를 조회할 수 있는 비율이 100%이다. 한도(5회) 도달 시 사용자는 한도 사실을 명확히 인지할 수 있는 안내를 받는다. +- **SC-007**: 본인 기록을 피드로 핀하려는 사용자의 진입에서, 핀 가능 여부와 책 정보가 한 번의 응답으로 전달되는 비율이 100%이다(추가 호출 없이 화면 구성 가능). + +> 본 PRD는 사용자 경험 차원의 성공 기준만 정의하며, 백엔드 응답 시간/RPS 등 기술 임계치는 헌법(constitution)의 성능 가드 원칙과 별도의 부하 시나리오에서 정의한다. + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 요구사항은 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"의 형태로 읽힌다. +- **인증 전제**: 모든 시나리오는 인증된 사용자를 전제로 한다. 인증/인가의 상세 동작은 별도 PRD가 다룬다. +- **방 도메인은 외부 의존**: 방의 생성·만료·참여자 관리·라이프사이클 상태는 별도 방 도메인 PRD가 정의한다. 본 PRD는 방의 "참여자 여부"와 "진행 중 여부"라는 두 외부 상태만 사용한다. +- **책 도메인은 외부 의존**: 본 PRD가 사용하는 "책의 전체 페이지 수"·"진행률"은 책 도메인 PRD(#356) 및 방-책 연결을 통해 제공된다. 본 PRD는 그 값을 신뢰한다. +- **피드 도메인은 외부 의존**: "기록을 피드로 핀하기"의 핀 이후 피드 작성 상세는 피드 도메인 PRD(#354)를 따른다. +- **오늘의 한마디(AttendanceCheck)는 범위 외**: 같은 컨트롤러에 라우팅되지만 사용자 정의에 따라 본 PRD에서는 별도 도메인으로 처리한다. +- **댓글·좋아요 행위 자체는 범위 외**: 본 PRD는 *카운트 정합성*만 책임진다. 댓글 작성/삭제·좋아요 토글의 사용자 흐름은 댓글/좋아요(또는 Post) 도메인 PRD에서 다룬다. +- **AI 한도의 확장 가능성**: 본 PRD가 정의한 한도(전역 평생 누적 5회·리셋 없음)는 운영 초기 정책이다. 멤버십 등급·유료화·단기 캠페인 등으로 한도를 차등화하는 후속 결정이 발생하면 본 PRD를 개정 트리거로 본다. +- **알림 연동**: 기록/투표에 대한 댓글·좋아요로 인한 알림 발화 정책은 알림 도메인 PRD가 정의한다. 본 PRD는 발화 사실을 인정하지 않거나 정의하지 않는다. +- **신고**: 게시글에 대한 신고 흐름은 별도 신고 도메인 PRD가 정의한다(피드 PRD에서 정한 정책과 일관). From 97fd8068f14f6976d59884c397c03c0fe56d76a0 Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 17:55:37 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[docs]=20=EB=B0=A9(Room)=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20PRD=20=EC=97=AD=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(specs/005-room-domain)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 중인 방 도메인을 사용자 관점으로 정형화 (신규 기능 정의 아님) - User Story 6건 (P1: 방 생성·발견과 참여·내 방 관리, P2: 호스트 운영·방 안 컨텍스트·카테고리별 추천) - Functional Requirements 27건 (FR-001 ~ FR-027) - 측정 가능한 Success Criteria 7건, Edge Cases 9건, Assumptions 10건 - 방 라이프사이클 명문화: RECRUITING -> IN_PROGRESS -> EXPIRED IN_PROGRESS 전환은 호스트의 모집 마감, EXPIRED 전환은 종료일 기반 시간 트리거(스케줄)로 자동 수행 - 공개/비공개의 비밀번호 짝 검증, 카테고리 5종 사전 명시 - "인기 방" 산정 기준 명문화: 모집 중 방의 memberCount 내림차순 (충원율 아닌 절대 참여자 수, 개인화 없음) - 호스트 이탈 정책 명문화: 현재 정책상 호스트는 어떤 경로로도 방을 떠날 수 없음 (양도·방 폐쇄 기능 미제공) - 향후 도입 예정: 호스트 양도 + 호스트 단독 방 삭제 (도입 시 본 PRD 개정 트리거로 Assumptions에 명시) - 책 도메인은 외부 의존(#356), 방 안 활동은 RoomPost PRD(#357), 알림·신고는 별도 도메인 --- .../checklists/requirements.md | 35 +++ specs/005-room-domain/spec.md | 232 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 specs/005-room-domain/checklists/requirements.md create mode 100644 specs/005-room-domain/spec.md diff --git a/specs/005-room-domain/checklists/requirements.md b/specs/005-room-domain/checklists/requirements.md new file mode 100644 index 000000000..df9119eee --- /dev/null +++ b/specs/005-room-domain/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: 방(Room) 기능 + +**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) — BCrypt/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 — 2개 마커 해소 (Q1: memberCount 내림차순·개인화 없음 / Q2: 현재 호스트 이탈 전면 불가·자동 만료는 시간 기반 잡, 향후 양도+호스트 단독 방 삭제 도입 예정) +- [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 — 책·게시글·알림·신고는 명시적 범위 외 +- [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`으로 진행 가능. +- 향후 트리거: 호스트 양도 + 호스트 단독 방 삭제 기능 도입 시 FR-016, FR-017, Edge Cases 업데이트 필요(Assumptions에 명시됨). diff --git a/specs/005-room-domain/spec.md b/specs/005-room-domain/spec.md new file mode 100644 index 000000000..2810a0eb8 --- /dev/null +++ b/specs/005-room-domain/spec.md @@ -0,0 +1,232 @@ +# Feature Specification: 방(Room) 기능 + +**Feature Branch**: `005-room-domain` + +**Created**: 2026-05-17 + +**Status**: Reviewed (clarifications resolved 2026-05-17) + +**Input**: User description: "room 관련 기능" + +> 본 문서는 신규 기능 정의가 아닌, 이미 운영 중인 THIP 서비스의 "방(Room)" 도메인을 사용자 관점에서 역설계해 정형화한 PRD다. 구현 상세(스택·해시 알고리즘·스케줄러)는 의도적으로 배제하고, 사용자가 무엇을(WHAT) 왜(WHY) 할 수 있어야 하는지에 집중한다. + +## User Scenarios & Testing *(mandatory)* + +THIP의 **방(Room)**은 *같은 책을 함께 읽는 사용자 모임*이다. 호스트가 책을 정해 방을 만들고, 다른 사용자들이 모집 기간 안에 참여한다. 일정 시점에 모집이 마감되면 방은 *진행 중* 상태로 전환되어 기록·투표·오늘의 한마디 등 방 내부 활동이 이루어지고, 종료일 이후에는 *만료* 상태가 된다. + +### User Story 1 - 방 만들고 모집 시작하기 (Priority: P1) + +호스트가 함께 읽을 책·기간·모집 정원·공개 여부·카테고리를 정해 방을 만들고, 다른 사용자에게 노출되어 모집이 시작된다. + +**Why this priority**: 방이 없으면 THIP의 모임 가치는 존재하지 않는다. 모든 후속 활동의 출발점. + +**Independent Test**: 어떤 사용자가 책·시작일·종료일·모집 정원·공개 여부·카테고리를 지정해 방을 만들면 새 방이 *모집 중* 상태로 생성되고, 만든 사용자가 호스트로 자동 참여한 1인 상태로 시작된다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 정상적인 입력(책·시작일·종료일·모집 정원·카테고리·공개 여부)으로 방 생성을 요청할 때, **When** 생성이 성공하면, **Then** 새 방이 *모집 중* 상태로 만들어지고, 만든 사용자가 *호스트* 역할로 자동 참여한 상태(memberCount=1, roomPercentage=0)가 된다. +2. **Given** 사용자가 *비공개* 방을 비밀번호 없이 만들려 하거나, *공개* 방에 비밀번호를 지정하려 하면, **Then** 작업이 거부된다(공개 여부와 비밀번호 존재 여부는 정확히 짝을 이뤄야 한다). +3. **Given** 사용자가 시작일을 *오늘 또는 과거 날짜*로 지정하거나, 시작일을 종료일과 같거나 이후로 지정하면, **Then** 작업이 거부된다(시작일은 오늘 이후, 종료일은 시작일 이후). +4. **Given** 카테고리 값이 시스템이 인정하는 5종(과학·IT, 문학, 예술, 사회과학, 인문학) 중 어느 하나도 아닐 때, **Then** 작업이 거부된다. + +--- + +### User Story 2 - 모집 중인 방 둘러보고 참여하기 (Priority: P1) + +사용자는 키워드·카테고리로 모집 중인 방을 찾고, 정렬 옵션으로 마감 임박 또는 신청 인원순으로 둘러본다. 공개 방은 곧장 참여할 수 있고, 비공개 방은 비밀번호 검증을 거친 후 참여한다. + +**Why this priority**: 모집된 방이 발견되지 않으면 호스트의 입장에서 방을 만든 보람이 사라진다. P1으로 함께 묶임. + +**Independent Test**: 모집 중인 방이 다수 존재할 때 (a) 키워드로 검색해 결과를 받고, (b) 정렬 옵션(마감 임박/신청 인원)이 적용되며, (c) 공개/비공개 방 모두 검색 결과에 노출되되 비공개 방은 비밀번호 검증을 통과해야 참여할 수 있다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 키워드(책 이름 또는 방 이름)와 카테고리를 지정해 모집 중인 방을 검색할 때, **When** 검색이 성공하면, **Then** 매칭된 모집 중인 방의 커서 페이지 결과가 반환된다. 공개/비공개 방 모두 포함된다. +2. **Given** 사용자가 카테고리 대신 *전체 검색* 모드(`isAllCategory=true`)를 선택할 때, **Then** 모든 카테고리에 걸친 결과가 반환된다. +3. **Given** 사용자가 정렬 옵션을 *마감 임박*(`deadline`) 또는 *신청 인원*(`memberCount`)으로 지정할 때, **Then** 결과는 그 기준에 따라 정렬된다. +4. **Given** 사용자가 검색 입력을 *확정*(`isFinalized=true`)했을 때, **Then** 시스템은 그 키워드를 사용자별 *최근 검색어*로 기록한다. 입력 중(`isFinalized=false`)에는 기록하지 않는다(책 검색의 정책과 일관). +5. **Given** 사용자가 모집 중인 방 상세보기를 요청할 때, **Then** 방의 기본 정보·정원 대비 현재 참여자 수·진행 상태·책 정보 등이 함께 반환된다. +6. **Given** 사용자가 *공개* 방에 *참여*를 요청할 때, **When** 정원 미만이고 사용자가 아직 참여하지 않았다면, **Then** 사용자는 멤버로 참여하고 방의 memberCount가 1 증가한다. +7. **Given** 사용자가 *비공개* 방에 참여하기 위해 비밀번호 검증을 요청할 때, **When** 비밀번호가 일치하면, **Then** 검증 성공 응답이 반환된다(이후 별도의 참여 요청을 통해 실제 참여가 이루어진다). +8. **Given** 사용자가 *공개* 방에 비밀번호 검증을 요청할 때, **Then** 비밀번호가 필요 없다는 오류로 응답된다. +9. **Given** 사용자가 *모집 기간이 만료된* 방에 참여 또는 비밀번호 검증을 요청할 때, **Then** 작업이 거부된다. +10. **Given** 정원이 가득 찬 방에 참여를 요청할 때, **Then** 정원 초과 오류로 거부된다. + +--- + +### User Story 3 - 내가 참여한 방 관리하기 (Priority: P1) + +사용자는 자신이 참여한 방을 한 곳에서 관리한다. *홈 화면*에서는 활성(모집 중·진행 중) 방만 보고, *전용 목록*에서는 상태 필터(모집 중/진행 중/만료/혼합)로 필터링해 본다. 호스트가 아닌 멤버는 언제든 방을 나갈 수 있다. + +**Why this priority**: 가입한 사용자에게는 "내가 어디에 속해 있나"가 매일 보는 정보. 활성 방의 발견과 떠나기 모두 핵심. + +**Independent Test**: 임의 사용자가 (a) 홈에서 자신이 참여한 활성 방 목록을 보고, (b) 전용 목록에서 상태별로 필터링하며, (c) 호스트가 아닌 방은 나갈 수 있다. + +**Acceptance Scenarios**: + +1. **Given** 사용자가 다수의 방에 참여 중일 때, **When** 사용자가 홈의 *내 참여 방* 목록을 요청하면, **Then** 모집 중·진행 중인 방만 커서 페이지로 반환된다(만료된 방은 제외). +2. **Given** 사용자가 *내 모임방* 전용 목록을 요청할 때, **When** 상태 필터를 지정(`playingAndRecruiting`, `recruiting`, `playing`, `expired`)하면, **Then** 그 상태에 해당하는 자신의 방만 반환된다. 필터 미지정 시 기본값은 *모집중 + 진행중*이다. +3. **Given** 사용자가 *호스트가 아닌* 방에 대해 나가기를 요청할 때, **When** 작업이 성공하면, **Then** 사용자는 멤버에서 제외되고 방의 memberCount가 1 감소한다. +4. **Given** 사용자가 *호스트인* 방에 대해 나가기를 요청하면, **Then** 작업이 거부된다(현재 정책상 호스트는 어떤 경로로도 방을 떠날 수 없다 — 양도·방 폐쇄 기능 미제공). +5. **Given** 사용자가 자신이 참여한 방의 참여자 목록을 요청하면, **Then** 호스트 표시가 포함된 참여자 명단이 반환된다. + +--- + +### User Story 4 - 호스트로서 방 운영하기 (Priority: P2) + +호스트는 자신이 만든 방의 모집을 마감해 *진행 중* 상태로 전환한다. 진행 중인 방에서는 멤버들의 기록·투표·오늘의 한마디가 누적된다. + +**Why this priority**: 운영 권한은 일반 멤버 시나리오 이후 의미가 있다. 호스트의 의도적 액션이 라이프사이클 전환의 핵심 트리거. + +**Independent Test**: 호스트가 자신의 *모집 중* 방에 대해 모집 마감을 요청하면 방의 상태가 *진행 중*으로 바뀌고 시작일이 그 시점의 날짜로 갱신된다. 호스트가 아닌 사용자의 마감 요청은 거부된다. + +**Acceptance Scenarios**: + +1. **Given** *호스트*가 자신의 *모집 중* 방에 대해 모집 마감을 요청할 때, **When** 작업이 성공하면, **Then** 방의 상태가 *진행 중*으로 전환되고 시작일이 마감 시점의 날짜로 갱신된다. +2. **Given** *호스트가 아닌* 사용자가 임의 방의 모집 마감을 요청할 때, **Then** 작업이 거부된다. +3. **Given** *이미 진행 중이거나 만료된* 방에 대한 모집 마감 요청, **Then** 작업이 거부된다(모집 중 상태가 아니므로). + +--- + +### User Story 5 - 방 안에서 활동 컨텍스트 확인하기 (Priority: P2) + +방 안에서 기록 작성을 시작하려는 사용자는 *책 전체 페이지 수*와 *총평 작성이 가능한지 여부*를 컨텍스트로 받는다. 방의 진행 상태(진행 중/완료)에 따라 상세 화면의 구성이 달라진다. + +**Why this priority**: P1 시나리오(작성)의 *진입 직전* 정보를 제공하는 보조 흐름. 마찰을 줄이는 보조 시나리오. + +**Independent Test**: 방 참여자가 (a) 방의 책 페이지 정보와 총평 가능 여부를 단일 호출로 받고, (b) 진행 중 또는 완료된 방 상세보기에서 상태에 맞는 정보가 반환된다. + +**Acceptance Scenarios**: + +1. **Given** 방 참여자가 기록 작성 화면에 진입할 때, **When** 사용자가 책 페이지 정보를 요청하면, **Then** 책의 전체 페이지 수와 *총평 작성이 가능한 상태인지 여부*가 함께 응답된다. +2. **Given** 사용자가 *진행 중* 또는 *완료(만료)* 방의 상세를 요청할 때, **When** 작업이 성공하면, **Then** 그 상태에 맞는 상세 정보(진행률·기록·투표 요약 등 화면에 필요한 정보)가 반환된다. +3. **Given** 사용자가 방 비참여자일 때, **When** 그 방에 대한 책 페이지 정보 또는 참여자 전용 상세를 요청하면, **Then** 작업이 거부되거나 비참여자용 정보만 반환된다(상세 정책은 방 라이프사이클 규칙에 따른다). + +--- + +### User Story 6 - 카테고리별 추천 방 발견 (Priority: P2) + +사용자는 카테고리별로 "마감 임박/인기/최근 생성된" 방 묶음을 한눈에 본다. 능동 검색을 하지 않아도 카테고리 단위로 의미 있는 방을 발견한다. + +**Why this priority**: 검색 외의 발견 진입점. 사용자 활성도와 신규 방 노출의 균형을 맞춤. + +**Independent Test**: 사용자가 카테고리(기본: 문학)를 지정해 추천 목록을 요청하면 *마감 임박 방*, *인기 방*, *최근 생성된 방* 세 묶음이 응답된다. + +**Acceptance Scenarios**: + +1. **Given** 시스템에 충분한 모집 중 방이 존재할 때, **When** 사용자가 카테고리를 지정해 추천 방 목록을 요청하면, **Then** *마감 임박*·*인기*·*최근 생성* 세 묶음이 응답된다. +2. **Given** 카테고리 미지정 시, **Then** 기본 카테고리("문학")가 적용된다. +3. **Given** 어떤 카테고리에 모집 중인 방이 없을 때, **Then** 그 묶음은 빈 결과로 일관되게 응답된다. + +**"인기 방" 산정 기준 (확정)**: + +- **신호**: 모집 중 방의 **현재 참여자 수**(`memberCount`) *내림차순*. *정원 대비 충원율*이 아닌 *절대 참여자 수* 기준이다. +- **개인화**: 없음. 모든 사용자에게 동일한 결과가 노출된다. +- **대상**: *모집 중* 상태의 방으로 한정한다(진행 중·만료는 후보 아님). +- 같은 카운트인 방 사이의 결정적 순서는 본 PRD가 강제하지 않으며, 다른 순서로 노출되어도 사용자 가치에 영향이 없는 수준이어야 한다. + +--- + +### Edge Cases + +- **공개 여부와 비밀번호의 짝**: 공개 방에 비밀번호를 지정하거나 비공개 방을 비밀번호 없이 만들려는 요청은 거부된다. +- **시작일·종료일 유효성**: 시작일은 오늘 이후여야 하고, 종료일은 시작일 이후여야 한다. 같은 날짜는 허용되지 않는다. +- **카테고리 정합성**: 시스템이 인정하는 카테고리(과학·IT/문학/예술/사회과학/인문학) 외 입력은 거부된다. +- **모집 만료 후 참여 시도**: 모집 중 상태가 아닌 방에 참여 또는 비밀번호 검증을 시도하면 거부된다. +- **정원 초과 참여 시도**: memberCount가 recruitCount에 도달한 방에 참여를 요청하면 거부된다. +- **참여자 수 무결성**: memberCount는 어떤 동시성 시나리오에서도 활성 참여자 수와 일치한다. 1 미만으로 감소할 수 없다(호스트는 항상 존재). +- **호스트의 방 이탈 (확정 — 현재 정책)**: 호스트는 어떤 경로로도 방을 떠날 수 없다. *일반 나가기*는 거부되고, *호스트 권한 양도*나 *방 폐쇄(삭제)* 기능도 현재 제공되지 않는다. 따라서 한번 호스트가 된 사용자는 해당 방이 *만료* 상태에 도달할 때까지 호스트로 남는다. +- **공개 방의 비밀번호 검증 요청**: 공개 방에 대한 비밀번호 검증 요청은 "비밀번호 불필요" 오류로 응답된다. +- **방 상태 자동 전환 (확정)**: 종료일이 지난 *진행 중* 방은 시간 기반(스케줄) 트리거로 *만료* 상태로 자동 전환된다. 사용자가 어떤 액션을 하지 않아도 종료일 경계 이후의 첫 가시점부터 방은 *만료* 상태로 노출되어야 한다. 사용자 액션에 의한 명시적 만료 전환 경로는 본 PRD가 정의하지 않는다. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### 생성 + +- **FR-001**: 사용자는 책·제목·설명·시작일·종료일·모집 정원·카테고리·공개 여부(공개 시 비밀번호 없음, 비공개 시 비밀번호 필수)를 지정해 방을 만들 수 있다. +- **FR-002**: 생성된 방은 *모집 중* 상태로 시작하며, 만든 사용자는 *호스트*로 자동 참여하고 memberCount=1, roomPercentage=0으로 초기화된다. +- **FR-003**: 공개 방에 비밀번호 지정, 비공개 방에 비밀번호 미지정은 거부된다(둘은 정확히 짝을 이뤄야 한다). +- **FR-004**: 시작일은 오늘 *이후*, 종료일은 시작일 *이후*여야 한다. +- **FR-005**: 카테고리는 시스템이 인정하는 5종(과학·IT, 문학, 예술, 사회과학, 인문학) 중 하나여야 한다. + +#### 발견·검색 + +- **FR-006**: 사용자는 키워드(책 이름 또는 방 이름)·카테고리·정렬(마감 임박/신청 인원)·커서로 모집 중인 방을 검색할 수 있다. 공개/비공개 방 모두 결과에 포함된다. +- **FR-007**: 사용자가 *전체 검색* 모드를 지정하면 카테고리 제약 없이 모든 카테고리의 모집 중 방이 검색된다. +- **FR-008**: 사용자가 *검색 입력 확정*(`isFinalized=true`)으로 검색에 성공하면 시스템은 키워드를 사용자별 최근 검색어로 기록한다. 입력 중에는 기록하지 않는다(책 검색 PRD와 일관). +- **FR-009**: 사용자는 카테고리별로 *마감 임박*·*인기*·*최근 생성* 세 묶음의 추천 방 목록을 단일 호출로 받을 수 있다. *인기* 묶음은 *모집 중* 방을 대상으로 *현재 참여자 수(memberCount)* 내림차순으로 산정하며, 모든 사용자에게 동일한 결과로 노출된다(충원율 아닌 절대 참여자 수 기준). +- **FR-010**: 사용자는 *모집 중* 방의 상세 정보와 *진행 중/완료* 방의 상세 정보를 각각 별도의 진입점에서 조회할 수 있어야 하며, 상태에 맞는 정보 구성을 받는다. + +#### 참여·이탈 + +- **FR-011**: 사용자는 *공개* 방에 직접 참여할 수 있다. 정원 미달이고 본인이 미참여 상태일 때만 성공한다. +- **FR-012**: 사용자는 *비공개* 방의 비밀번호를 별도 호출로 검증할 수 있다. 공개 방에 대한 비밀번호 검증 요청은 거부된다. +- **FR-013**: 사용자는 *비공개* 방에 대해 비밀번호 검증을 통과한 뒤 참여를 요청할 수 있다. +- **FR-014**: 모집 중 상태가 아닌 방(진행 중·만료)에 대한 참여 또는 비밀번호 검증은 거부된다. +- **FR-015**: 정원에 도달한 방에 대한 참여는 거부된다. +- **FR-016**: 사용자는 *호스트가 아닌* 방에서 나갈 수 있다. 나간 사용자는 멤버 목록에서 제외되며 memberCount가 1 감소한다. +- **FR-017**: 호스트는 어떤 경로로도 방을 떠날 수 없다. 일반 "나가기"는 거부되고, *호스트 권한 양도*나 *방 폐쇄(삭제)* 기능도 현재 제공되지 않는다. 호스트는 방이 *만료* 상태에 도달할 때까지 호스트로 남는다. +- **FR-018**: memberCount는 1 미만으로 감소할 수 없다(호스트는 항상 존재). 어떤 동시성 시나리오에서도 활성 참여 관계 수와 일치해야 한다. + +#### 운영(호스트) + +- **FR-019**: 호스트는 자신의 *모집 중* 방에 대해 모집 마감을 요청할 수 있다. 작업 성공 시 방의 상태는 *진행 중*으로 전환되고 시작일이 마감 시점의 날짜로 갱신된다. +- **FR-020**: 호스트가 아닌 사용자의 모집 마감 요청은 거부된다. + +#### 라이프사이클 상태 + +- **FR-021**: 방은 정확히 다음 셋 중 하나의 상태를 가진다: *모집 중*, *진행 중*, *만료*. 상태 전이는 (a) 생성 시 *모집 중*, (b) 호스트의 모집 마감 시 *진행 중*, (c) 종료일 도달 시 *만료*로 이루어진다. +- **FR-022**: 사용자가 보는 방의 상태는 항상 그 시점의 실제 상태와 일치해야 한다. *진행 중* 방의 *만료* 전환은 종료일을 기준으로 시간 기반(스케줄) 트리거에 의해 자동으로 수행되며, 사용자 액션은 필요하지 않다. + +#### 내 방 관리 + +- **FR-023**: 사용자는 홈 화면에서 자신이 참여한 *활성*(모집 중·진행 중) 방의 커서 페이지 목록을 받을 수 있다. 만료된 방은 홈 목록에서 제외된다. +- **FR-024**: 사용자는 전용 목록에서 상태 필터(`playingAndRecruiting`/`recruiting`/`playing`/`expired`)를 지정해 자신의 방을 조회할 수 있다. 미지정 시 기본은 `playingAndRecruiting`이다. +- **FR-025**: 사용자는 자신이 참여한 방의 참여자 목록을 조회할 수 있다. 호스트 표시가 포함된다. + +#### 방 안 활동 컨텍스트 + +- **FR-026**: 방 참여자는 기록 작성 화면 진입 시점에 책 전체 페이지 수와 총평 작성 가능 여부를 단일 호출로 받을 수 있다. + +#### 방 게시물 좋아요 + +- **FR-027**: 사용자는 방 게시물(기록·투표)의 좋아요 상태를 토글할 수 있다(상세 동작은 RoomPost PRD(#357)와 좋아요 공통 정책 PRD를 따른다). 본 PRD는 방 식별자가 게시물에 일관되게 연결되어 있어야 함만 보장한다. + +### Key Entities + +- **Room (방)**: 같은 책을 함께 읽는 사용자 모임. 제목·설명·공개 여부(+ 해시된 비밀번호)·시작일·종료일·모집 정원·진행률·상태(모집 중/진행 중/만료)·연결된 책·카테고리를 가진다. +- **Room Participant (방 참여자)**: 한 사용자가 한 방에 속하는 연결. 역할(*호스트*/*멤버*)을 가진다. +- **Room Status (상태)**: 방의 라이프사이클. *모집 중* → *진행 중* → *만료* 순으로 전이한다. +- **Room Participant Role (참여자 역할)**: *호스트*(방 생성자, 운영 권한 보유)와 *멤버*(일반 참여자). +- **Category (카테고리)**: 시스템이 인정하는 5종. 과학·IT / 문학 / 예술 / 사회과학 / 인문학. +- **Book (책)**: 방이 연결되는 도서. 본 PRD는 *전체 페이지 수* 같은 외부 속성만 사용하며 상세는 책 도메인 PRD(#356)가 정의. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 사용자가 방 생성 진입부터 생성 완료까지 평균 90초 이내에 마칠 수 있다(책 선택 후 기준). +- **SC-002**: 정원 초과 사건(memberCount > recruitCount)이 운영 중 0건 발생한다. +- **SC-003**: 호스트가 일반 "나가기" 경로로 방을 떠나 호스트 없는 방이 생기는 사건이 0건이다. +- **SC-004**: 사용자가 보는 방 상태(모집 중/진행 중/만료)와 실제 상태가 일치하지 않는 사건이 0건이다. +- **SC-005**: 만료된 방이 사용자 홈의 *내 참여 방* 목록에 노출되는 사건이 0건이다. +- **SC-006**: 비공개 방의 비밀번호가 잘못된 검증 요청에 의해 통과되는 사건이 0건이다. +- **SC-007**: 사용자가 추천 묶음(마감 임박/인기/최근 생성)을 카테고리별로 단일 호출로 받는 비율이 100%이다(추가 호출 없이 화면 구성 가능). + +> 본 PRD는 사용자 경험 차원의 성공 기준만 정의하며, 백엔드 응답 시간/RPS 등 기술 임계치는 헌법(constitution)의 성능 가드 원칙과 별도의 부하 시나리오에서 정의한다. + +## Assumptions + +- **이미 운영 중인 기능의 역설계**: 본 PRD는 신규 기능 정의가 아니라 기존 구현을 사용자 관점으로 정형화한 산출물이다. 요구사항은 "구현이 보장해야 한다(혹은 보장하고 있어야 한다)"의 형태로 읽힌다. +- **인증 전제**: 모든 방 시나리오는 인증된 사용자를 전제로 한다. +- **책 도메인은 외부 의존**: 책 메타데이터(제목·전체 페이지 수 등)는 책 도메인 PRD(#356)가 제공한다. +- **방 안 활동(기록·투표·오늘의 한마디)은 별도 PRD**: 본 PRD는 방의 라이프사이클·참여·운영에 집중한다. 기록·투표 흐름은 RoomPost PRD(#357), 오늘의 한마디는 별도 PRD가 다룬다. +- **좋아요·댓글의 사용자 흐름은 공통 도메인**: 방 게시물 좋아요 자체의 토글 흐름은 좋아요 공통 도메인(Post)에서 정의한다. 본 PRD는 방 식별자 연계만 보장한다. +- **최근 검색어는 별도 도메인**: 방 검색 시 *확정* 키워드의 기록 정책은 별도 검색 이력 도메인 PRD가 정의한다(책 검색과 동일). +- **알림 연동**: 방 시작·참여·종료 등의 이벤트에 따른 알림 발화는 알림 도메인 PRD가 정의한다. +- **신고**: 방·게시물 신고 흐름은 별도 신고 도메인 PRD가 정의한다. +- **카테고리 사전**: 시스템이 인정하는 5종 카테고리는 본 PRD 작성 시점의 운영 기준이며, 향후 추가/변경은 본 PRD 개정 트리거다. +- **페이지네이션 일관성**: 방 도메인의 목록(검색·내 방·참여자)은 다른 도메인과 동일하게 커서 기반을 따른다. +- **향후 도입 예정 — 호스트 양도 및 호스트 단독 방 삭제**: 본 PRD 작성 시점에는 미제공 기능이나, 후속 단계에서 (a) 호스트가 다른 멤버에게 호스트 권한을 양도하는 기능과 (b) *방에 호스트만 남았을 때*에 한정해 호스트가 방을 폐쇄(삭제)할 수 있는 기능 도입이 예정되어 있다. 두 기능 모두 도입 시 본 PRD를 개정 트리거로 본다(FR-016, FR-017, Edge Cases 업데이트 필요). From 716835b85f31590522f5188375276de2eea713cb Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 18:06:52 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[docs]=20=EC=95=8C=EB=A6=BC(Notification)?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20PRD=20=EC=97=AD=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EC=9E=91=EC=84=B1=20(specs/006-notification-domain?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운영 중인 알림 도메인을 사용자 관점으로 정형화 (신규 기능 정의 아님) - User Story 5건 (P1: 알림 센터·읽음 라우팅·디바이스 토큰 등록·트리거 수신, P2: 디바이스별 수신 여부 토글) - Functional Requirements 21건 (FR-001 ~ FR-021) - 측정 가능한 Success Criteria 7건, Edge Cases 10건, Assumptions 11건 - 본 PRD의 본질: 트리거 수신 이후의 알림 동작 (트리거 발화 조건은 발화 도메인 PRD가 책임) - 알림 트리거 카탈로그 15종 명문화: FEED 6종(팔로우/피드 좋아요/댓글/답글/댓글 좋아요/팔로잉 새 피드), ROOM 9종(새 참여자/모집 조기 마감/활동 시작/기록·투표 시작/ 게시글 댓글·답글·좋아요/댓글 좋아요) - 디바이스 단위 수신 여부 토글 정책: 같은 사용자라도 디바이스별 독립 - 외부 푸시 채널(현재 FCM)은 벤더 중립 추상화 - 저장(알림 센터)과 전달(푸시)의 독립성 보장 (외부 장애 시에도 누적 손실 X) - 알림 보존 정책 명문화: 현재는 평생 누적·수동 삭제 미제공 향후 최근 N일 자동 삭제 도입 예정 (개정 트리거로 Assumptions에 명시) - 트리거 발화 도메인·댓글·좋아요·사용자 라이프사이클은 명시적 범위 외 --- .../checklists/requirements.md | 36 +++ specs/006-notification-domain/spec.md | 212 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 specs/006-notification-domain/checklists/requirements.md create mode 100644 specs/006-notification-domain/spec.md diff --git a/specs/006-notification-domain/checklists/requirements.md b/specs/006-notification-domain/checklists/requirements.md new file mode 100644 index 000000000..9ae588a81 --- /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 000000000..8e4799d45 --- /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를 개정 트리거로 본다. From 7c8b71df1b8ef08f5ef760e078aaea38762fa3c0 Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 18:10:44 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[docs]=20=ED=94=BC=EB=93=9C=20PRD:=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EB=B0=9C?= =?UTF-8?q?=ED=99=94=20=EB=B3=B4=EC=A6=9D=20=EB=AA=85=EB=AC=B8=ED=99=94=20?= =?UTF-8?q?(#359=20=EC=A0=95=ED=95=A9=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "팔로잉한 사용자가 새 피드를 작성함" 트리거를 본 도메인 책임으로 명시 (공개 피드 생성 성공 시 작성자의 팔로워들에게 발화, 비공개·수정·삭제는 미발화) - 좋아요·댓글로 인한 알림 트리거의 책임 분리 명확화 (좋아요 공통 도메인, 댓글 도메인 책임) - 알림 도메인 PRD(#359)의 트리거 카탈로그(FEED 6종)와 정합 --- specs/001-feed-features/spec.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specs/001-feed-features/spec.md b/specs/001-feed-features/spec.md index 5a73d5551..5e27e4100 100644 --- a/specs/001-feed-features/spec.md +++ b/specs/001-feed-features/spec.md @@ -198,6 +198,9 @@ THIP은 같은 책을 읽는 사람들이 감상을 공유하는 독서 커뮤 - **책(Book) 도메인은 외부 의존**: 피드는 ISBN을 통해 책을 참조한다. 책의 등록·수정·동기화는 별도 도메인 PRD가 정의한다. - **신고(Report)는 별도 도메인**: 신고 트리거 API와 사용자 흐름은 별도 신고 도메인 PRD가 정의한다. 본 PRD는 신고 누적 결과(노출 정책: 즉시 숨김 → 검수 큐 → 복원/영구 숨김)만을 정의한다. - **이미지 저장소**: 이미지 업로드/저장은 안전한 외부 객체 저장소를 사용한다고 가정한다. 구체적 저장소 선택은 본 PRD 범위 외다. -- **알림 연동**: 좋아요·댓글에 따른 알림 발생은 별도 알림 도메인이 다룬다. 본 PRD는 알림 발화 조건이 존재한다는 사실만 인정한다. +- **알림 연동**: 본 PRD는 *본 도메인이 직접 발화하는* 알림 트리거만을 보증한다. 좋아요·댓글로 인한 알림(피드 좋아요/피드 댓글/피드 댓글 답글/피드 댓글 좋아요)의 트리거 발화는 각각 좋아요 공통 도메인과 댓글 도메인이 책임지며, 본 도메인은 그 발화의 *대상이 피드라는 사실*만 인정한다. 알림 트리거 수신 이후의 저장·표시·푸시 전달 흐름은 알림 도메인 PRD(#359)를 따른다. + + **본 도메인이 직접 발화하는 트리거**: + - **"팔로잉한 사용자가 새 피드를 작성함"**: 사용자가 *공개* 피드를 생성하는 데 성공한 시점에, 작성자를 팔로잉 중인 사용자들에게 알림 트리거가 발화된다. 비공개 피드는 발화하지 않는다(트리거 수신자에게 노출되지 않을 콘텐츠는 알림도 보내지 않는다). 피드 수정·삭제는 트리거를 발화하지 않는다. - **카테고리/태그 사전**: 작성 화면에서 보여지는 카테고리·태그는 시스템에 의해 사전 정의되어 있으며, 본 PRD는 그 편집 흐름을 정의하지 않는다. - **페이지 크기**: 모든 커서 페이지의 기본 페이지 크기는 시스템 설정값으로 고정되며, 본 PRD는 그 값을 단정하지 않는다. From a754b8374c3eff5a533c4c29a6b6dd8662a1e2d4 Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 18:11:09 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[docs]=20RoomPost=20PRD:=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EB=B0=9C=ED=99=94=20?= =?UTF-8?q?=EB=B3=B4=EC=A6=9D=20=EB=AA=85=EB=AC=B8=ED=99=94=20(#359=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "새 기록 작성됨" 트리거: 기록 작성 성공 시 같은 방의 다른 참여자 (작성자 제외)에게 발화. 수정·삭제는 미발화 - "새 투표 시작됨" 트리거: 투표 생성 성공 시 같은 방의 다른 참여자 (작성자 제외)에게 발화. 참여·취소·항목 변경·수정·삭제는 미발화 (생성 1회당 1건) - 댓글·좋아요로 인한 알림 트리거 책임 분리 명확화 (댓글 도메인, 좋아요 공통 도메인 책임) - 알림 도메인 PRD(#359)의 트리거 카탈로그와 정합 --- specs/004-roompost-domain/spec.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/specs/004-roompost-domain/spec.md b/specs/004-roompost-domain/spec.md index 24b173ff0..5e026db88 100644 --- a/specs/004-roompost-domain/spec.md +++ b/specs/004-roompost-domain/spec.md @@ -207,5 +207,9 @@ THIP의 **방**은 같은 책을 함께 읽는 사용자 모임이며, 방 안 - **오늘의 한마디(AttendanceCheck)는 범위 외**: 같은 컨트롤러에 라우팅되지만 사용자 정의에 따라 본 PRD에서는 별도 도메인으로 처리한다. - **댓글·좋아요 행위 자체는 범위 외**: 본 PRD는 *카운트 정합성*만 책임진다. 댓글 작성/삭제·좋아요 토글의 사용자 흐름은 댓글/좋아요(또는 Post) 도메인 PRD에서 다룬다. - **AI 한도의 확장 가능성**: 본 PRD가 정의한 한도(전역 평생 누적 5회·리셋 없음)는 운영 초기 정책이다. 멤버십 등급·유료화·단기 캠페인 등으로 한도를 차등화하는 후속 결정이 발생하면 본 PRD를 개정 트리거로 본다. -- **알림 연동**: 기록/투표에 대한 댓글·좋아요로 인한 알림 발화 정책은 알림 도메인 PRD가 정의한다. 본 PRD는 발화 사실을 인정하지 않거나 정의하지 않는다. +- **알림 연동**: 본 PRD는 *본 도메인이 직접 발화하는* 알림 트리거만을 보증한다. 게시글에 대한 댓글·좋아요로 인한 알림 트리거의 발화는 각각 댓글 도메인과 좋아요 공통 도메인이 책임지며, 본 도메인은 그 발화의 *대상이 방 게시글이라는 사실*만 인정한다. 알림 트리거 수신 이후의 저장·표시·푸시 전달 흐름은 알림 도메인 PRD(#359)를 따른다. + + **본 도메인이 직접 발화하는 트리거**: + - **"내가 참여한 방에서 새 기록이 작성됨"**: 사용자가 기록을 작성하는 데 성공한 시점에, 같은 방의 다른 참여자(작성자 본인 제외)에게 알림 트리거가 발화된다. 기록 수정·삭제는 트리거를 발화하지 않는다. + - **"내가 참여한 방에서 새 투표가 시작됨"**: 사용자가 투표를 *생성*하는 데 성공한 시점에, 같은 방의 다른 참여자(작성자 본인 제외)에게 알림 트리거가 발화된다. 투표 참여·취소·항목 변경·수정·삭제는 트리거를 발화하지 않는다(생성 1회당 1건). - **신고**: 게시글에 대한 신고 흐름은 별도 신고 도메인 PRD가 정의한다(피드 PRD에서 정한 정책과 일관). From 657adc9f83b6176debeed94afd36c0a628c686dd Mon Sep 17 00:00:00 2001 From: hyunjun-jang Date: Sun, 17 May 2026 18:11:48 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[docs]=20=EB=B0=A9=20PRD:=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EB=B0=9C=ED=99=94=20?= =?UTF-8?q?=EB=B3=B4=EC=A6=9D=20=EB=AA=85=EB=AC=B8=ED=99=94=20(#359=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "새 참여자" 트리거: 방 참여 성공 시 호스트에게 발화. 참여 취소(나가기)는 미발화 (참여 1회당 1건) - "모집 조기 마감" 트리거: 호스트가 closeRoomRecruit으로 RECRUITING -> IN_PROGRESS 전환 시 호스트 제외 참여자에게 발화 - "활동 시작" 트리거: IN_PROGRESS 진입 시 참여자에게 발화 (조기 마감과 함께 발화될 수 있는 별개 트리거) - 알림 도메인 PRD(#359)의 트리거 카탈로그(ROOM 9종 중 3종)와 정합 --- specs/005-room-domain/spec.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/specs/005-room-domain/spec.md b/specs/005-room-domain/spec.md index 2810a0eb8..6f6acb9fa 100644 --- a/specs/005-room-domain/spec.md +++ b/specs/005-room-domain/spec.md @@ -225,7 +225,14 @@ THIP의 **방(Room)**은 *같은 책을 함께 읽는 사용자 모임*이다. - **방 안 활동(기록·투표·오늘의 한마디)은 별도 PRD**: 본 PRD는 방의 라이프사이클·참여·운영에 집중한다. 기록·투표 흐름은 RoomPost PRD(#357), 오늘의 한마디는 별도 PRD가 다룬다. - **좋아요·댓글의 사용자 흐름은 공통 도메인**: 방 게시물 좋아요 자체의 토글 흐름은 좋아요 공통 도메인(Post)에서 정의한다. 본 PRD는 방 식별자 연계만 보장한다. - **최근 검색어는 별도 도메인**: 방 검색 시 *확정* 키워드의 기록 정책은 별도 검색 이력 도메인 PRD가 정의한다(책 검색과 동일). -- **알림 연동**: 방 시작·참여·종료 등의 이벤트에 따른 알림 발화는 알림 도메인 PRD가 정의한다. +- **알림 연동**: 본 PRD는 *본 도메인이 직접 발화하는* 알림 트리거를 보증한다. 알림 트리거 수신 이후의 저장·표시·푸시 전달 흐름은 알림 도메인 PRD(#359)를 따른다. + + **본 도메인이 직접 발화하는 트리거**: + - **"내가 호스트인 방에 새 참여자가 들어옴"**: 사용자가 방에 참여하는 데 성공한 시점에, *그 방의 호스트*에게 알림 트리거가 발화된다. 참여 취소(나가기)는 트리거를 발화하지 않는다(참여 성공 1회당 1건). + - **"내가 참여한 방의 모집이 조기 마감됨"**: 호스트가 모집을 마감(`closeRoomRecruit`)해 방의 상태가 `RECRUITING` → `IN_PROGRESS`로 전환된 시점에, 그 방의 *호스트를 제외한* 모든 참여자에게 알림 트리거가 발화된다. 자연 시작일 도래에 의한 자동 전환과 구분된다. + - **"내가 참여한 방의 활동이 시작됨"**: 방의 상태가 `IN_PROGRESS`로 진입한 시점에 그 방의 *참여자 전원*(또는 호스트를 제외한 멤버 — 운영 구현에 따름)에게 알림 트리거가 발화된다. 본 PRD는 *발화한다는 사실*과 *대상 범위가 방 참여자라는 점*만 보장한다. + + > 위 \"조기 마감\"과 \"활동 시작\"은 모집 마감 시점에서 *함께* 발화될 수 있는 별개의 트리거다. 둘의 정확한 메시지·발화 순서·중복 방지 정책은 알림 도메인 PRD(#359)와 알림 템플릿이 다룬다. - **신고**: 방·게시물 신고 흐름은 별도 신고 도메인 PRD가 정의한다. - **카테고리 사전**: 시스템이 인정하는 5종 카테고리는 본 PRD 작성 시점의 운영 기준이며, 향후 추가/변경은 본 PRD 개정 트리거다. - **페이지네이션 일관성**: 방 도메인의 목록(검색·내 방·참여자)은 다른 도메인과 동일하게 커서 기반을 따른다.