# PRD: 무료 콘텐츠 포인트 결제 불가 ## 문서 정보 | 항목 | 내용 | |---|---| | 문서 상태 | 구현 기준 확정 | | 작성일 | 2026-07-31 | | 최종 수정일 | 2026-07-31 | | 대상 제품 | 소비자용 오디오 콘텐츠 조회 API | | 작성자·결정권자 | 사용자 | | 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 | | 관련 구현 계획 | `docs/20260731_무료_콘텐츠_포인트_결제_불가/plan-task.md` | | 관련 review | `reviews/phase-1-review.md`, `reviews/phase-2-review.md` | ## 1. Overview 무료 오디오 콘텐츠와 포인트 결제 가능 상태가 소비자 화면에서 동시에 노출되지 않도록 조회 계약을 보정한다. 저장된 포인트 결제 가능 설정은 유지하되, 소비자용 응답과 포인트 전용 목록에서는 가격을 함께 반영한 실질 상태를 사용한다. ## 2. Problem Statement - 현재 일부 조회 응답은 `price == 0`이면서 저장된 `isPointAvailable == true`인 콘텐츠를 그대로 포인트 결제 가능으로 노출한다. - 포인트 추천과 전체 탭 POINT 조회는 저장된 `isPointAvailable`만 필터링해 무료 콘텐츠가 포함될 수 있다. - 무료와 포인트 결제 가능 상태가 함께 노출되면 클라이언트의 가격 표시와 결제 진입 판단이 서로 모순될 수 있다. 문제를 해결했다는 판단은 소비자용 모든 대상 응답에서 무료 콘텐츠의 포인트 결제 가능 여부가 `false`이고, POINT 전용 목록과 개수에서 무료 콘텐츠가 제외되는 것으로 한다. ## 3. Goals - 무료 콘텐츠의 소비자용 포인트 결제 가능 여부를 항상 `false`로 응답한다. - 유료이면서 저장된 포인트 결제 가능 설정이 `true`인 콘텐츠는 기존처럼 `true`로 응답한다. - 포인트 전용 목록, 전체 개수와 페이징 판단에서 무료 콘텐츠를 제외한다. - 기존 공개 API 필드명, 응답 구조와 DB 저장값을 변경하지 않는다. ## 4. Non-Goals - 콘텐츠 생성·수정 시 `isPointAvailable` 저장값을 강제로 변경하지 않는다. - 기존 데이터의 일괄 수정이나 DB migration을 수행하지 않는다. - `/api/v2/admin/ai-characters/**/audio-contents` 관리자 목록·상세의 저장값 표현을 변경하지 않는다. - `/audio-content/{id}` 상세를 제외한 legacy 목록·추천·랭킹 API는 변경하지 않는다. - 콘텐츠 구매·대여·소장·포인트 차감 로직은 변경하지 않는다. - 공개 DTO의 필드 추가·삭제·이름 변경을 수행하지 않는다. ## 5. Target Users and Permissions | 사용자 | 목표 | 주요 작업 | 적용 범위 | |---|---|---|---| | 소비자 | 무료 콘텐츠를 포인트 결제 대상으로 오인하지 않는다 | 콘텐츠 상세·목록·추천 조회 | 대상 소비자용 API | | 관리자 | 저장된 콘텐츠 설정을 그대로 확인한다 | AI 캐릭터 콘텐츠 목록·상세 조회 | 변경 제외 | 기존 endpoint별 인증·성인 노출·차단 관계·공개 상태 정책은 변경하지 않는다. ## 6. 핵심 정책 ### 6.1 가격과 포인트 결제 가능 여부 - 무료 콘텐츠는 `price == 0`으로 정의한다. - 소비자에게 노출하는 실질 포인트 결제 가능 여부는 다음 식으로 정의한다. ```text effectivePointAvailable = storedIsPointAvailable && price > 0 ``` - `price == 0`이고 저장값이 `true`이면 소비자 응답은 `false`다. - `price > 0`이고 저장값이 `true`이면 소비자 응답은 `true`다. - 저장값이 `false`이면 가격과 관계없이 소비자 응답은 `false`다. - 이 정책은 응답 조립 시 적용하며 엔티티의 저장값은 변경하지 않는다. ### 6.2 포인트 전용 조회 - 포인트 전용 콘텐츠는 `isPointAvailable == true && price > 0`인 공개 오디오로 정의한다. - `GET /api/v2/audio/recommendations`의 `pointAudios`는 이 조건을 사용한다. - `GET /api/v2/audio/contents?type=POINT`의 목록과 `totalCount`는 동일한 조건을 사용한다. - `hasNext`는 보정된 목록 조건으로 조회한 `size + 1` 결과를 기준으로 기존 방식대로 계산한다. - 무료 콘텐츠는 `isPointAvailable == true`로 저장되어 있어도 POINT 목록, 개수와 페이징 후보에서 제외한다. ## 7. 기능 요구사항 | ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 | |---|---|---|---|---| | `POINT-001` | 확정 | 무료 기준은 `price == 0`이다 | 무료 fixture가 가격 0으로 판정된다 | `P1-T1`, `P2-T1` | | `POINT-002` | 확정 | 소비자용 실질 포인트 가능 여부는 `storedIsPointAvailable && price > 0`이다 | 무료·저장값 true 응답이 false이고 유료·저장값 true 응답이 true다 | `P1-T1` | | `POINT-003` | 확정 | legacy 콘텐츠 상세의 `isAvailableUsePoint`에 실질 상태를 적용한다 | `GET /audio-content/{id}` 응답 회귀 테스트가 통과한다 | `P1-T1` | | `POINT-004` | 확정 | 대상 v2 소비자 응답의 `isPointAvailable`에 실질 상태를 적용한다 | 각 응답 변환 테스트가 무료 true 저장값을 false로 보정한다 | `P1-T1` | | `POINT-005` | 확정 | 추천 `pointAudios`에서 무료 콘텐츠를 제외한다 | 추천 repository·E2E 테스트에서 가격 0 항목이 없다 | `P2-T1` | | `POINT-006` | 확정 | 전체 탭 POINT 목록·`totalCount`·`hasNext`가 같은 유료 포인트 조건을 사용한다 | repository·E2E 테스트의 목록과 페이징 메타데이터가 일치한다 | `P2-T1` | | `POINT-007` | 확정 | AI 캐릭터 관리자 콘텐츠 조회는 저장값을 그대로 반환한다 | 무료·저장값 true인 관리자 상세가 true를 유지한다 | `P1-T1` | | `POINT-008` | 확정 | 공개 API 스키마와 DB 저장값을 유지한다 | DTO 필드 집합과 관리자 저장값 회귀 테스트가 통과한다 | `P1-GATE`, `P2-GATE` | ## 8. API 계약 ### 8.1 응답 보정 대상 | Method | Path | 응답 경계 | 보정 필드 | |---|---|---|---| | GET | `/audio-content/{id}` | `GetAudioContentDetailResponse` | `isAvailableUsePoint` | | GET | `/api/v2/contents` | `ContentOverviewItemResponse` | `isPointAvailable` | | GET | `/api/v2/audio/contents` | `MainContentAudioResponse` | `isPointAvailable` | | GET | `/api/v2/audio/recommendations` | `AudioCardResponse` | `isPointAvailable` | | GET | `/api/v2/home/recommendations` | `HomeFirstAudioContentItem` | `isPointAvailable` | | GET | `/api/v2/creator-channels/{creatorId}/home` | `CreatorChannelAudioContentResponse` | `isPointAvailable` | | GET | `/api/v2/creator-channels/{creatorId}/audio` | `CreatorChannelAudioContentResponse` | `isPointAvailable` | | GET | `/api/v2/creator-channels/{creatorId}/live` | `CreatorChannelAudioContentResponse` | `isPointAvailable` | - `GET /api/v2/creator-channels/{creatorId}/series`는 콘텐츠 가격과 포인트 가능 필드를 반환하지 않아 코드 변경 대상이 아니다. - 현재 v2 소비자용 API에 동일 필드를 반환하는 새 경로가 발견되면 같은 식을 적용하고 계획 범위를 먼저 갱신한다. ### 8.2 포인트 전용 조회 조건 보정 대상 | Method | Path/section | 변경 전 | 변경 후 | |---|---|---|---| | GET | `/api/v2/audio/recommendations`의 `pointAudios` | `isPointAvailable == true` | `isPointAvailable == true && price > 0` | | GET | `/api/v2/audio/contents?type=POINT` | `isPointAvailable == true` | `isPointAvailable == true && price > 0` | ### 8.3 변경 제외 관리자 계약 | Method | Path | 정책 | |---|---|---| | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 저장된 `isPointAvailable`을 그대로 반환 | | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | 저장된 값을 `isAvailableUsePoint`에 그대로 반환 | ## 9. 기술적 제약 - Kotlin, Java 17, Spring Boot 2.7.14와 현재 QueryDSL/JPA 구조를 유지한다. - 새 dependency, DB schema, API endpoint와 DTO를 추가하지 않는다. - 응답 변환 경계에서는 `configured && price > 0` 식을 직접 사용해 현재 파일 책임 안에서 최소 변경한다. - POINT 조회 조건은 기존 repository 조건 함수에 `price > 0`을 결합해 목록과 count가 같은 조건을 공유하게 한다. - 관리자 mapper와 관리자 조회 service는 변경하지 않는다. - 관련 없는 콘텐츠 가격·결제·추천 점수·정렬·성인·차단 정책은 변경하지 않는다. ## 10. 테스트와 품질 요구사항 - TDD 순서로 무료·저장값 true fixture의 실패 테스트를 먼저 작성하고 실패 원인이 기존 원본 전달임을 확인한다. - 소비자 응답 경계별로 무료 true → false와 유료 true → true를 검증한다. - 추천 POINT와 전체 탭 POINT에 무료 true fixture를 추가해 목록 제외를 검증한다. - 전체 탭은 POINT `totalCount`와 `hasNext`가 목록 조건과 일치하는지 검증한다. - 관리자 상세는 무료 true 저장값을 그대로 true로 응답하는 회귀 테스트를 유지한다. - focused test 후 직접 영향받는 v2 콘텐츠·홈·크리에이터 채널 회귀와 `ktlintCheck`를 실행한다. - 여러 API 경계를 변경하므로 최종 Gate에서 전체 `test`를 실행한다. ## 11. 성공 기준 - [ ] `price == 0`, 저장값 `true`인 콘텐츠가 모든 대상 소비자 응답에서 `false`다. (`POINT-002~004`) - [ ] `price > 0`, 저장값 `true`인 콘텐츠가 대상 소비자 응답에서 `true`다. (`POINT-002`) - [ ] 저장값 `false`인 콘텐츠는 가격과 관계없이 `false`다. (`POINT-002`) - [ ] 무료·저장값 true 콘텐츠가 추천 `pointAudios`에서 제외된다. (`POINT-005`) - [ ] 무료·저장값 true 콘텐츠가 전체 탭 POINT 목록·`totalCount`·`hasNext` 후보에서 제외된다. (`POINT-006`) - [ ] 관리자 목록·상세와 DB 저장값은 변경되지 않는다. (`POINT-007~008`) - [ ] 공개 응답 필드명과 구조가 변경되지 않는다. (`POINT-008`) ## 12. Open Questions 없음. ## 13. 요구사항 추적표 | 요구사항 | 계획 Phase | Goal | 자동 검증 | |---|---:|---|---| | `POINT-001~004`, `POINT-007~008` | 1 | `P1-T1`, `P1-GATE` | legacy 상세·v2 응답 mapper·관리자 회귀 테스트 | | `POINT-005~006`, `POINT-008` | 2 | `P2-T1`, `P2-GATE` | 추천/전체 탭 repository·E2E 테스트 | ## 14. Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·Goal | |---|---|---|---|---|---| | 2026-07-31 | `DEC-001` | 확정 | 무료 기준을 `price == 0`으로 고정하고 소비자용 포인트 가능 여부를 `storedIsPointAvailable && price > 0`으로 계산한다 | 사용자 인터뷰 | `POINT-001~004`, `P1-T1` | | 2026-07-31 | `DEC-002` | 확정 | 추천과 전체 탭 POINT 조회에서 무료 콘텐츠를 제외한다 | 사용자 선택 A | `POINT-005~006`, `P2-T1` | | 2026-07-31 | `DEC-003` | 확정 | AI 캐릭터 관리자 조회와 DB 저장값은 변경하지 않는다 | 사용자 선택 A | `POINT-007~008`, `P1-T1` | | 2026-07-31 | `DEC-004` | 확정 | 새 통합 문서를 기준으로 만들고 충돌하는 기존 추천·전체 탭 문서에는 정정 기록을 누적한다 | 사용자 승인 | 관련 문서 전체 |