diff --git a/specs/003-book-domain/checklists/requirements.md b/specs/003-book-domain/checklists/requirements.md new file mode 100644 index 00000000..4107cdbc --- /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 00000000..58e6709e --- /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가 단정하지 않는다. +- **페이지네이션 일관성**: 책 도메인의 목록(저장한 책·선택가능 책·모집중인 방)은 다른 도메인(피드·팔로우)과 동일하게 커서 기반을 따른다.