fix(content): 무료 콘텐츠 포인트 사용 조건을 보정한다

This commit is contained in:
2026-07-31 20:00:58 +09:00
parent eb0ff7537e
commit 033044b4de
27 changed files with 1396 additions and 41 deletions

View File

@@ -0,0 +1,410 @@
# 무료 콘텐츠 포인트 결제 불가 구현 계획
| 문서 항목 | 내용 |
|---|---|
| 상태 | 리뷰 후속 작업 완료 |
| 작성일 | 2026-07-31 |
| 요구사항 기준 | `docs/20260731_무료_콘텐츠_포인트_결제_불가/prd.md` |
| API 기준 | PRD `8. API 계약` |
| 현재 Phase | Phase 1 회귀 수정 완료 |
| 현재 활성 Goal | 없음 |
## 목표
무료 오디오 콘텐츠가 소비자 조회에서 포인트 결제 가능으로 노출되거나 POINT 전용 목록에 포함되지 않게 한다.
## 구현 방식
- 응답 DTO의 공개 필드와 내부 조회 record는 변경하지 않는다.
- 각 소비자 응답 조립 지점에서 `isPointAvailable && price > 0`을 적용한다.
- 추천과 전체 탭 POINT repository 조건에 `price > 0`을 추가한다.
- 관리자 mapper와 저장 로직은 수정하지 않는다.
- 단일 식 적용을 위한 새 공통 abstraction이나 dependency는 만들지 않는다.
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1. 소비자 응답 정규화 | 완료 | `3/3` | 없음 | 없음 |
| 2. POINT 조회 조건 보정 | 완료 | `1/1` | 없음 | 없음 |
- 동시에 하나의 미완료 goal만 운용한다.
- 완료된 Task와 검증 기록은 되돌리거나 삭제하지 않는다.
- goal에는 token budget을 설정하지 않는다.
## 범위
### 포함
- legacy 콘텐츠 상세 `isAvailableUsePoint` 보정
- v2 콘텐츠 overview·전체 탭·추천 탭 응답 보정
- v2 홈 추천 첫 오디오 응답 보정
- v2 크리에이터 채널 홈·오디오·라이브의 공통 오디오 응답 보정
- 추천 `pointAudios`와 전체 탭 `type=POINT`의 유료 조건 보강
- 전체 탭 POINT count·pagination 회귀 검증
- AI 캐릭터 관리자 조회 원본값 유지 회귀 검증
### 제외
- DB 저장값과 기존 데이터 변경
- 콘텐츠 생성·수정 validation 변경
- 관리자 콘텐츠 mapper·service 변경
- legacy 콘텐츠 상세 이외의 legacy 조회 API 변경
- 결제·주문·포인트 차감 로직 변경
- API schema와 dependency 변경
## 기술적 제약
- 기술 스택: Kotlin, Java 17, Spring Boot 2.7.14, JUnit 5, QueryDSL/JPA.
- 기존 package와 mapper/repository 책임을 유지한다.
- 공개 API 필드명 `isAvailableUsePoint`, `isPointAvailable`을 유지한다.
- `price == 0`은 무료, 실질 포인트 가능 여부는 `configured && price > 0`으로 고정한다.
- POINT 조회의 목록과 count는 같은 repository 조건을 사용한다.
- 모든 구현 Task는 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR` 순서로 실행한다.
- focused test부터 실행하고 최종 Gate에서 영향 범위 회귀와 전체 `test`를 실행한다.
## Phase 1: 소비자 응답 정규화
**Phase 결과:** 무료·저장값 true인 콘텐츠가 대상 소비자 응답에서는 false로 보이지만 관리자 조회에서는 true를 유지한다.
**선행조건:** PRD `POINT-001~004`, `POINT-007~008` 확정.
**Phase 완료 조건:** `P1-T1``P1-GATE` 완료, 검증 기록 누적.
**후속 리뷰 완료 조건:** `P1-R1` 완료, `P1-GATE` 재검증과 수정 후 검증 기록 누적.
### Task 1.1 소비자 응답의 실질 포인트 가능 여부 적용
**Goal 실행 `P1-T1`:** legacy 상세와 모든 대상 v2 응답 조립 경계에서 가격을 반영한 포인트 가능 여부를 반환한다.
- **시작 조건:** PRD `DEC-001`, `DEC-003` 확인.
- **완료 증거:** RED/GREEN 체크박스, 대상 mapper 테스트, 관리자 원본값 회귀 테스트와 Progress 기록.
- **범위 밖:** POINT 전용 repository 필터와 콘텐츠 저장값 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponse.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponse.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/dto/AudioRecommendationsResponse.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponse.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponseTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
**Interfaces:**
- Consumes: 각 mapper가 이미 받는 `price: Int`와 저장된 Boolean 필드.
- Produces: schema를 바꾸지 않고 `configured && price > 0`으로 보정된 소비자 응답 Boolean.
- Preserves: AI 캐릭터 관리자 목록·상세의 원본 Boolean.
- [x] **RED:** 각 응답 경계에 무료·저장값 true fixture를 추가하고 소비자 응답은 false, 유료·저장값 true 응답은 true로 기대한다. 관리자 상세에는 무료·저장값 true가 true로 유지되는 회귀 assertion을 추가한다.
- [x] **RED 확인:** 아래 focused test를 실행해 소비자 응답이 현재 true를 전달하여 발생하는 assertion 실패를 확인하고, 관리자 회귀 assertion은 기존 동작으로 통과하는지 구분해 기록한다.
```bash
./gradlew test \
--tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponseTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponseTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacadeTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponseTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacadeTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest'
```
- [x] **GREEN:** 소비자 응답 조립 지점의 Boolean 대입을 `storedValue && price > 0`으로 바꾼다. `AiCharacterAdminAudioContentMapper`와 관리자 service는 수정하지 않는다.
- [x] **GREEN 확인:** 같은 focused test 명령을 다시 실행해 무료 true → false, 유료 true → true, 관리자 무료 true → true가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** 동일한 한 줄 식을 유지하고 새 helper나 구조 변경을 추가하지 않는다. 변경 파일에 `./gradlew ktlintCheck`를 실행하고 결과를 Progress에 기록한다.
### Phase 1 Gate
**Goal 실행 `P1-GATE`:** 대상 소비자 응답 계약과 관리자 제외 계약을 최종 판정한다.
- **시작 조건:** `P1-T1` 완료.
- **완료 증거:** 아래 명령 성공, 공개 필드명 유지 확인과 Progress 기록.
- **범위 밖:** POINT repository 필터 구현과 관련 없는 응답 리팩터링.
```bash
./gradlew test \
--tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.home.*'
./gradlew ktlintCheck
```
**Expected:** 대상 소비자 응답에서 무료 콘텐츠가 포인트 가능으로 노출되지 않고 기존 JSON 필드 집합이 유지된다.
### Task 1.2 소비자·관리자 포인트 가능 계약의 회귀 증거 보강
**Goal 실행 `P1-R1`:** 소비자 응답의 저장값 false 조건과 무료 관리자 응답의 원본값 유지 조건을 자동 회귀 테스트로 증명한다.
- **시작 조건:** `REV-P1-001`, `REV-P1-002` 확정과 기존 `P1-GATE` 완료.
- **완료 증거:** 대상 소비자 경계의 `price > 0, storedIsPointAvailable == false` assertion, 관리자 목록·상세의
`price == 0, storedIsPointAvailable == true` assertion, focused test와 `P1-GATE` 재검증 기록.
- **범위 밖:** production 코드 변경, 관리자 응답 보정, 공개 API schema 변경.
- **TDD 예외 사유:** production 구현은 정적 검토상 계약을 이미 충족하며, 누락된 것은 완료 근거인 회귀 assertion이다.
의도적인 production 결함을 만들어 RED를 재현하지 않는다.
- **대체 검증 방법:** 기존 fixture가 누락한 진리표 조건을 추가하고 focused test와 `P1-GATE`를 통과시킨 뒤 mapper 식과
관리자 원본 전달 식을 다시 대조한다.
**Files:**
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponseTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponseTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Modify: `docs/20260731_무료_콘텐츠_포인트_결제_불가/plan-task.md`
- [x] 대상 소비자 응답 경계마다 `price > 0, storedIsPointAvailable == false`가 false로 유지되는 assertion을 추가한다.
- [x] AI 캐릭터 관리자 목록·상세 fixture를 `price == 0, storedIsPointAvailable == true`로 구성하고 저장값 true가 그대로
반환되는지 확인한다.
- [x] Task 1.1 focused test와 `P1-GATE`를 재실행하고 결과를 Progress에 누적한다.
- [x] `REV-P1-001`, `REV-P1-002`의 수정 후 검증 기록과 이 문서의 현재 상태를 갱신한다.
### Task 1.3 legacy 상세의 유료 포인트 가능 positive 회귀 증거 보강
**Goal 실행 `P1-R2`:** legacy 상세에서 `price > 0, storedIsPointAvailable == true`가 true를 반환하는 계약을 자동 회귀 테스트로 증명한다.
- **시작 조건:** `REV-P1-003` 확정과 `P1-R1` 완료.
- **완료 증거:** legacy 상세의 무료·저장값 true → false, 유료·저장값 false → false,
유료·저장값 true → true assertion, focused test와 `P1-GATE` 재검증 기록.
- **범위 밖:** production 코드, v2 응답 test, 관리자 mapper/service 변경.
- **TDD 예외 사유:** production 구현은 정적 검토상 positive 계약을 이미 충족하며, 누락된 것은 회귀 assertion이다.
의도적인 production 결함을 만들어 RED를 재현하지 않는다.
- **대체 검증 방법:** 기존 legacy 상세 test의 진리표를 완성하고 focused test와 `P1-GATE`를 통과시킨다.
**Files:**
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
- Modify: `docs/20260731_무료_콘텐츠_포인트_결제_불가/plan-task.md`
- [x] legacy 상세 test에 `price > 0, storedIsPointAvailable == true` 응답이 true인 assertion을 추가한다.
- [x] 하나의 test가 세 계약 조건을 드러내도록 `DisplayName`과 test 함수명을 맞춘다.
- [x] `AudioContentServiceTest`, `P1-GATE`, `ktlintCheck`를 실행하고 결과를 Progress에 누적한다.
- [x] `REV-P1-003`의 수정 후 검증 기록과 이 문서의 현재 상태를 갱신한다.
## Phase 2: POINT 조회 조건 보정
**Phase 결과:** 추천과 전체 탭 POINT 목록, count와 pagination 후보에서 무료 콘텐츠가 제외된다.
**선행조건:** `P1-GATE` 완료와 PRD `POINT-005~006` 확정.
**Phase 완료 조건:** `P2-T1``P2-GATE` 완료, 검증 기록 누적.
### Task 2.1 추천·전체 탭 POINT 조회에 유료 조건 적용
**Goal 실행 `P2-T1`:** 두 POINT 조회가 `isPointAvailable == true && price > 0` 조건을 공통으로 사용한다.
- **시작 조건:** `P1-GATE` 완료, PRD `DEC-002` 확인.
- **완료 증거:** repository RED/GREEN, endpoint E2E와 Progress 기록.
- **범위 밖:** FREE/AUDIO/추천 점수·랜덤 정렬·공개/성인/차단 조건 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepositoryTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt`
**Interfaces:**
- Consumes: 기존 `audioContent.isPointAvailable``audioContent.price` QueryDSL 필드.
- Produces: 추천 `pointAudios`, 전체 탭 POINT 목록과 count에 공통 적용되는 `isPointAvailable.isTrue.and(price.gt(0))` 조건.
- Preserves: FREE 목록은 `price.eq(0)`, AUDIO 목록은 `price.gt(0)`인 기존 조건.
- [x] **RED:** 두 repository fixture에 `price = 0, isPointAvailable = true``price > 0, isPointAvailable = true`를 함께 두고 POINT 결과에는 유료 항목만 포함되도록 기대한다. 전체 탭 E2E는 `totalCount`, `audios`, `hasNext`가 같은 후보 집합을 반영하도록 기대한다.
- [x] **RED 확인:** 아래 focused test를 실행해 무료 true 콘텐츠가 POINT 결과에 포함되어 발생하는 목록 또는 count assertion 실패를 확인한다.
```bash
./gradlew test \
--tests 'kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest' \
--tests 'kr.co.vividnext.sodalive.v2.content.all.adapter.out.persistence.DefaultMainContentAllQueryRepositoryTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest'
```
- [x] **GREEN:** `findPointAudios` 조건과 `optionalAudioPointCondition``price.gt(0)`을 결합한다. 전체 탭 count와 목록은 기존 `audioCondition`을 계속 공유한다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 POINT 결과에서 무료가 제외되고 유료 true는 유지되며 `totalCount``hasNext`가 일치하는지 확인한다.
- [x] **REFACTOR:** FREE/AUDIO 조건, 랜덤/가격/인기 정렬과 공통 visibility 조건이 바뀌지 않았는지 직접 영향 회귀와 `ktlintCheck`로 확인한다.
### Phase 2 Gate
**Goal 실행 `P2-GATE`:** 소비자 응답과 POINT 조회의 전체 요구사항을 최종 판정한다.
- **시작 조건:** `P2-T1` 완료.
- **완료 증거:** focused·전체 회귀·lint 성공, 문서 Progress와 최종 검증 기록.
- **범위 밖:** 실패를 숨기기 위한 test 삭제·skip·완화와 관련 없는 코드 수정.
```bash
./gradlew test \
--tests 'kr.co.vividnext.sodalive.v2.content.recommendation.*' \
--tests 'kr.co.vividnext.sodalive.v2.content.all.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.content.*'
./gradlew test
./gradlew ktlintCheck
```
**Expected:** 모든 테스트와 lint가 성공하고 무료 콘텐츠가 어떤 대상 소비자 응답이나 POINT 전용 결과에서도 포인트 결제 가능으로 취급되지 않는다.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | 없음 | 아니요 | 실패 응답 경계와 fixture를 다시 대조 |
| 2 | `P1-GATE` | `P1-T1` | 아니요 | 실패 소유 mapper의 회귀 수정 goal 추가 |
| 3 | `P2-T1` | `P1-GATE` | 아니요 | 목록·count 조건 공유 여부 확인 |
| 4 | `P2-GATE` | `P2-T1` | 아니요 | 실패 소유 repository 또는 mapper로 되돌림 |
| 5 | `P1-R1` | Phase별 review 판정 완료 | 아니요 | 누락된 계약 fixture와 assertion 범위를 다시 대조 |
| 6 | `P1-R2` | `P1-R1` | 아니요 | legacy 상세의 세 계약 조건을 다시 대조 |
```text
P1-T1 → P1-GATE → P2-T1 → P2-GATE
```
리뷰 후속 실행 순서:
```text
P1-R1 → P1-R2
```
## 변경 금지 항목
- 관리자 조회와 DB 저장값을 보정하지 않는다.
- 공개 DTO 필드를 추가·삭제·이름 변경하지 않는다.
- POINT 조건 보강을 FREE/AUDIO/추천 점수·정렬 변경으로 확장하지 않는다.
- 새 dependency, schema migration과 공통 abstraction을 추가하지 않는다.
- 기존 완료 문서의 체크박스와 검증 기록을 삭제하거나 덮어쓰지 않는다.
- test를 삭제·skip·완화해 Gate를 통과시키지 않는다.
## Progress
실제 구현 시 기존 기록을 삭제하거나 덮어쓰지 않고 Goal 실행 결과를 차수별로 누적한다.
### 문서 작성 검증 — 2026-07-31
- 상태: 완료
- 무엇을: 확정 요구사항을 새 PRD와 goal 실행형 계획에 기록하고 기존 추천·전체 탭 문서의 충돌 계약을 정정했다.
- 왜: 구현 전에 단일 기준 문서, 범위, 제외 조건과 완료 증거를 확정하기 위해서다.
- 어떻게:
- `rg` placeholder·요구사항/Goal 추적 검색 — placeholder 없음, `POINT-001~008``P1-T1`·`P2-T1` 연결 확인.
- 계획에 기록된 production 파일 존재 확인 — 누락 없음.
- `git diff --check` — 출력 없음.
- `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL`, exit code 0.
- 구현 test: 문서만 변경했으므로 실행하지 않았다.
- 남은 항목: `P1-T1`부터 구현 실행.
- 다음 행동: 사용자가 구현을 요청하면 `P1-T1`의 RED부터 시작한다.
### 구현 검증 — 2026-07-31
- 상태: 완료
- 무엇을: 무료 콘텐츠 포인트 결제 불가 정책을 대상 소비자 응답과 POINT 전용 조회 조건에 적용했다.
- 왜: `price == 0` 콘텐츠가 포인트 결제 가능 상태와 동시에 노출되거나 POINT 목록·개수·페이징 후보에 포함되지 않도록 하기 위해서다.
- 어떻게:
- P1 RED focused test — 소비자 응답 경계에서 무료·저장값 true가 기존 true로 전달되어 assertion 실패 확인.
- P1 GREEN focused test — `BUILD SUCCESSFUL`, 무료 true → false, 유료 true → true, 관리자 상세 원본 true 유지 확인.
- P1-GATE — 원 wildcard 명령은 600초 제한 초과로 분할 실행했고, content 범위 `BUILD SUCCESSFUL`(9m31s), creator/home 범위 `BUILD SUCCESSFUL`(4m27s), `./gradlew ktlintCheck` `BUILD SUCCESSFUL`(1m02s).
- P2 RED focused test — recommendation/main-all repository와 E2E에서 무료·저장값 true가 POINT 후보에 포함되어 4개 assertion 실패 확인.
- P2 GREEN focused test — `BUILD SUCCESSFUL`, 추천 `pointAudios`와 전체 탭 POINT 목록·`totalCount`·`hasNext`가 유료 포인트 후보만 반영함을 확인.
- P2-GATE 영향 범위 — `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.recommendation.*' --tests 'kr.co.vividnext.sodalive.v2.content.all.*' --tests 'kr.co.vividnext.sodalive.v2.api.content.*'` `BUILD SUCCESSFUL`(1m48s).
- 최종 Gate — `./gradlew test` `BUILD SUCCESSFUL`(13m27s), `./gradlew ktlintCheck` `BUILD SUCCESSFUL`(40s).
- 결정: 관리자 mapper/service, DB 저장값, 공개 DTO 필드명과 구조는 변경하지 않았다.
- 남은 항목: 없음.
### Phase별 리뷰 — 2026-07-31
- 상태: Phase 1 수정 goal 필요, Phase 2 확정 발견 사항 없음.
- 무엇을: PRD, 구현 계획, staged production/test diff와 관련 호출 경계를 Phase별로 대조했다.
- 왜: 완료 체크와 실제 계약 증거가 일치하는지 판정하기 위해서다.
- 어떻게:
- `git diff --cached --check` — 출력 없음.
- `rg`로 대상 응답 mapper, POINT repository 조건과 관련 test assertion을 대조했다.
- `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL`, exit code 0.
- 사용자 지시에 따라 compile과 test는 다시 실행하지 않았으며 기존 구현 검증 기록을 근거로만 확인했다.
- 후속: `REV-P1-001`, `REV-P1-002``P1-R1`로 전환했다.
- 리뷰 문서:
- `docs/20260731_무료_콘텐츠_포인트_결제_불가/reviews/phase-1-review.md`
- `docs/20260731_무료_콘텐츠_포인트_결제_불가/reviews/phase-2-review.md`
### 리뷰 후속 검증 — 2026-07-31
- 상태: 완료
- 무엇을: `REV-P1-001`, `REV-P1-002`의 누락된 회귀 증거를 `P1-R1`로 보강했다.
- 왜: 소비자 응답의 저장값 false 조건과 AI 캐릭터 관리자 무료 원본값 유지 조건을 자동 테스트로 고정하기 위해서다.
- 어떻게:
- P1-R1 focused test — `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponseTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponseTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacadeTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponseTest' --tests 'kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacadeTest' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest'` `BUILD SUCCESSFUL`(3m11s).
- P1-GATE — `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` `BUILD SUCCESSFUL`(12m15s).
- `./gradlew ktlintCheck``BUILD SUCCESSFUL`(47s).
- 결정: production 코드, 관리자 mapper/service, 공개 DTO schema는 변경하지 않았다.
- 남은 항목: 없음.
### 2차 Phase 1 리뷰 — 2026-07-31
- 상태: `REV-P1-001`, `REV-P1-002` 수정 확인, 추가 수정 goal 필요.
- 무엇을: `P1-R1` test diff, Phase 1 production mapper, 관리자 제외 경계와 완료 기록을 다시 대조했다.
- 왜: 기존 확정 발견 사항이 실제로 수정됐는지, 계약 진리표에 다른 누락은 없는지 판정하기 위해서다.
- 어떻게:
- staged diff와 `rg`로 6개 소비자 조립 경계의 유료·저장값 false assertion을 확인했다.
- 관리자 목록·상세의 `price == 0, stored == true` fixture와 true assertion을 확인했다.
- `git diff --check`, `git diff --cached --check` — 출력 없음.
- `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL`, exit code 0.
- 사용자 지시에 따라 compile과 test는 다시 실행하지 않았다.
- 후속: legacy 상세의 유료·저장값 true positive assertion 누락을 `REV-P1-003`, `P1-R2`로 전환했다.
### 리뷰 후속 검증 2차 — 2026-07-31
- 상태: 완료
- 무엇을: `REV-P1-003`의 누락된 legacy 상세 positive 회귀 증거를 `P1-R2`로 보강했다.
- 왜: legacy 상세에서 무료·저장값 true, 유료·저장값 false, 유료·저장값 true의 포인트 사용 가능 계약을 하나의 test로 고정하기 위해서다.
- 어떻게:
- P1-R2 focused test — `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest'` `BUILD SUCCESSFUL`(14s).
- P1-GATE 단위 범위 — `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` `BUILD SUCCESSFUL`(2m 26s).
- `./gradlew ktlintCheck``BUILD SUCCESSFUL`(14s).
- 결정: production 코드, 관리자 mapper/service, 공개 DTO schema는 변경하지 않았다.
- 전체 테스트: 사용자 지시에 따라 실행하지 않았다.
- 남은 항목: 없음.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-07-31 | `PLAN-DEC-001` | 확정 | 응답 조립 지점에서 `configured && price > 0`을 직접 적용하고 새 abstraction을 만들지 않는다 | 기존 mapper 책임과 최소 변경 원칙 | `P1-T1` |
| 2026-07-31 | `PLAN-DEC-002` | 확정 | POINT repository 조건에 `price > 0`을 결합하고 전체 탭 목록과 count의 기존 조건 공유 구조를 유지한다 | 목록·count 일관성 | `P2-T1` |
| 2026-07-31 | `PLAN-DEC-003` | 확정 | 여러 소비자 API 경계를 변경하므로 최종 Gate에서 전체 `test`를 실행한다 | 영향 범위 회귀 증거 필요 | `P2-GATE` |
## 발견된 문제
초기 구현 시 발견된 문제 없음.
### 2026-07-31 Phase별 리뷰
- `REV-P1-001` — 소비자 응답 테스트에 `price > 0, storedIsPointAvailable == false` 계약 증거가 없다.
- `REV-P1-002` — 관리자 회귀 테스트가 무료 fixture로 저장값 유지 계약을 검증하지 않는다.
- `REV-P1-003` — legacy 상세 test가 유료·저장값 true의 positive 계약을 검증하지 않는다.
- 후속 Goal: `P1-R1`
- 처리: 2026-07-31 `P1-R1` 완료.
- 추가 후속 Goal: `P1-R2`
- 처리: 2026-07-31 `P1-R2` 완료.
- Phase 2: 확정 발견 사항 없음.
## 최종 보고 형식
```markdown
구현 결과: 무료 콘텐츠 포인트 결제 불가 정책을 소비자 응답과 POINT 조회에 적용
- 변경: 대상 응답 mapper와 추천·전체 탭 POINT 조회 조건
- 결정: 관리자·DB 원본 유지, 공개 schema 유지
- 검증: focused test, 영향 범위 회귀, 전체 test, ktlintCheck 결과
- 남은 항목: 없음 또는 실패·외부 조건
- 문서: PRD와 이 plan-task.md의 Progress·검증 기록
```

View File

@@ -0,0 +1,171 @@
# 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` | 확정 | 새 통합 문서를 기준으로 만들고 충돌하는 기존 추천·전체 탭 문서에는 정정 기록을 누적한다 | 사용자 승인 | 관련 문서 전체 |

View File

@@ -0,0 +1,229 @@
# Phase 1 코드 리뷰 보고서
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / `P1-T1`, `P1-GATE` |
| 기준 commit 또는 working tree | `eb0ff7537e5fa6b083be21df3319be0ff2ecda51` + staged working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `docs/20260731_무료_콘텐츠_포인트_결제_불가/prd.md`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `POINT-001~004`, `POINT-007~008` 구현과 완료 증거가 일치하는지 확인한다.
- 대상 소비자 응답과 변경 제외 관리자 응답이 각각 확정 계약을 유지하는지 확인한다.
### 포함 범위
- 코드: Phase 1에서 변경한 consumer response mapper와 `AudioContentService`
- 테스트: Phase 1의 unit/facade test와 AI 캐릭터 관리자 controller test
- 문서: PRD Phase 1 요구사항, `P1-T1`, `P1-GATE`, Progress
### 제외 범위
- Phase 2 POINT 전용 repository 조건
- legacy 상세 외 legacy 목록·추천·랭킹 API
- compile과 test 재실행
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
| High | 확정 요구사항 또는 공개 API 계약 위반 |
| Medium | 제한된 조건의 기능 회귀 또는 핵심 계약의 자동 검증 누락 |
| Low | 문서 정합성 또는 비핵심 회귀 증거 누락 |
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `POINT-001~004`, `POINT-007~008`
- 계획: `P1-T1`, `P1-GATE`
- 코드:
- `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt:970`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/dto/ContentOverviewPageResponse.kt:49`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/dto/MainContentAllTabResponse.kt:63`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/dto/AudioRecommendationsResponse.kt:72`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponse.kt:35`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt:270`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt:69`
- 테스트:
- `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt:286`
- `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt:495`
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `git diff --cached --check` | 성공 | 출력 없음 |
| staged diff와 `rg` 기반 호출·assertion 대조 | 성공 | 6개 소비자 조립 경계는 모두 `stored && price > 0` 적용 |
| `./gradlew --no-daemon tasks --all` | 성공 | `BUILD SUCCESSFUL`, exit code 0 |
| compile/test | 미실행 | 사용자 지시에 따라 기존 성공 기록만 확인 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-001` | Medium | 수정 완료 | 소비자 테스트가 저장값 false 조건을 증명하지 않음 | `Task 1.2` | `P1-R1` |
| `REV-P1-002` | Low | 수정 완료 | 관리자 회귀 테스트가 무료 원본값 유지 조건을 증명하지 않음 | `Task 1.2` | `P1-R1` |
| `REV-P1-003` | Medium | 수정 완료 | legacy 상세 test가 유료·저장값 true positive 계약을 증명하지 않음 | `Task 1.3` | `P1-R2` |
## 6. 발견 사항 상세
### REV-P1-001 — 소비자 테스트가 저장값 false 조건을 증명하지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `POINT-002`, `POINT-004`
- **소유 Task:** `Task 1.2`, `P1-R1`
**관찰 내용**
변경된 소비자 테스트는 주로 `price == 0, stored == true → false`
`price > 0, stored == true → true`만 검증한다. 따라서 구현이 실수로 `price > 0`만 반환해도 해당 두 조건은 통과한다.
PRD 성공 기준인 `stored == false → false`를 자동으로 구분할 수 없다.
**근거**
- 코드: 각 소비자 조립 경계는 현재 `stored && price > 0`으로 올바르게 구현되어 있다.
- 테스트: Phase 1 변경 test에는 각 변경 경계의 `price > 0, stored == false` assertion이 없다.
- 문서: PRD `POINT-002`와 성공 기준은 저장값 false가 가격과 무관하게 false일 것을 요구한다.
**영향**
현재 production 동작 결함은 확인되지 않았다. 다만 저장값 조건이 제거되는 회귀가 발생해도 Phase 1 test가 탐지하지 못한다.
**권장 조치**
각 변경 경계의 기존 test fixture에 `price > 0, stored == false` 사례를 최소 추가하고 `P1-GATE`를 재검증한다.
**판정 기록**
- 2026-07-31 — staged 구현식은 정상이나 확정 계약의 자동 검증 누락으로 판정해 `P1-R1`로 전환했다.
- 2026-07-31 — `P1-R1`에서 대상 소비자 경계에 `price > 0, stored == false` assertion을 추가하고 focused test, `P1-GATE`, `ktlintCheck` 성공을 확인했다.
### REV-P1-002 — 관리자 회귀 테스트가 무료 원본값 유지 조건을 증명하지 않음
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `POINT-007`, `POINT-008`
- **소유 Task:** `Task 1.2`, `P1-R1`
**관찰 내용**
계획과 Progress는 관리자 상세의 “무료·저장값 true → true” 확인을 완료 증거로 기록했지만,
`AiCharacterAdminAudioContentControllerTest`의 공통 fixture는 `price = 100`이다. 상세 assertion은 저장값 true 전달만 검증하며
무료 조건에서 소비자 보정이 관리자 경계로 번지지 않았는지는 증명하지 않는다. 관리자 목록의 동일 조건 assertion도 없다.
**근거**
- 코드: `AiCharacterAdminAudioContentMapper`는 현재 `content.isPointAvailable`을 그대로 전달한다.
- 테스트: 관리자 helper의 `price = 100`, 상세의 `isAvailableUsePoint == true` assertion.
- 문서: `P1-T1`과 구현 Progress는 무료 관리자 원본값 유지 확인을 완료 증거로 기록한다.
**영향**
현재 관리자 mapper의 기능 결함은 확인되지 않았다. 그러나 완료 기록과 실제 test fixture가 불일치하며 관리자 제외 계약의
핵심 경계가 회귀 test로 고정되지 않았다.
**권장 조치**
관리자 목록·상세 fixture를 `price == 0, stored == true`로 구성해 목록 `isPointAvailable`과 상세
`isAvailableUsePoint`가 true를 유지하는지 검증한다.
**판정 기록**
- 2026-07-31 — 구현은 정상이나 완료 증거 불일치가 확인되어 `P1-R1`로 전환했다.
- 2026-07-31 — `P1-R1`에서 관리자 목록·상세 fixture를 `price == 0, stored == true`로 보강하고 원본 true 유지 assertion 통과를 확인했다.
### REV-P1-003 — legacy 상세 test가 유료·저장값 true positive 계약을 증명하지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `POINT-002`, `POINT-003`
- **소유 Task:** `Task 1.3`, `P1-R2`
**관찰 내용**
`AudioContentServiceTest` 상세 회귀 test는 무료·저장값 true와 유료·저장값 false가 false인 것만 검증한다.
`price > 0, stored == true → true` assertion이 없어 legacy 상세 구현이 항상 false로 회귀해도 해당 test가 통과한다.
**근거**
- 코드: `AudioContentService` 상세 응답은 현재 `audioContent.isPointAvailable && audioContent.price > 0`으로 올바르게 구현되어 있다.
- 테스트: `AudioContentServiceTest` 상세 포인트 assertion은 false 사례 2건만 포함한다.
- 문서: `P1-T1`은 각 응답 경계의 유료·저장값 true 유지를 완료 증거로 요구한다.
**영향**
현재 production 동작 결함은 확인되지 않았다. 다만 legacy 상세의 positive 계약이 회귀 test로 고정되지 않았다.
**권장 조치**
기존 상세 test에 유료·저장값 true 응답 assertion을 추가해 세 계약 조건을 완성한다.
**판정 기록**
- 2026-07-31 — staged production 구현은 정상이나 legacy 상세 positive 회귀 증거 누락으로 판정해 `P1-R2`로 전환했다.
- 2026-07-31 — `P1-R2`에서 legacy 상세 test에 유료·저장값 true assertion을 추가해 세 계약 조건을 고정하고 focused test, P1-GATE 단위 범위, `ktlintCheck` 성공을 확인했다.
## 7. 확정 항목의 plan·goal 전환
- 신규 회귀 수정 Task: `plan-task.md``Task 1.2`
- 후속 goal: `P1-R1`
- objective: 소비자 응답의 저장값 false 조건과 무료 관리자 응답의 원본값 유지 조건을 자동 회귀 테스트로 증명한다.
- 추가 회귀 수정 Task: `plan-task.md``Task 1.3`
- 추가 후속 goal: `P1-R2`
- objective: legacy 상세의 유료·저장값 true positive 계약을 자동 회귀 테스트로 증명한다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Phase 1 production/test/document diff와 관련 mapper 확인 |
| 후보 항목 판정 완료 | 충족 | 3건 모두 판정 완료 |
| 확정 항목 plan 반영 | 충족 | `Task 1.2`, `P1-R1`, `Task 1.3`, `P1-R2` |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | 정적 검증 기록, test 미실행 사유 명시 |
**최종 결론:** 수정 완료
**남은 항목:** 없음
## 9. 수정 후 검증 기록
기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다.
### 1차 수정 검증 — 2026-07-31
- 무엇을: `REV-P1-001`, `REV-P1-002`의 수정 내용과 회귀 증거를 재검토했다.
- 왜: 소비자 저장값 false 계약과 관리자 무료 원본값 유지 계약의 자동 검증 누락을 해소했는지 확인하기 위해서다.
- 어떻게:
- staged diff와 `rg` — 6개 소비자 조립 경계의 유료·저장값 false assertion 확인.
- staged diff — 관리자 목록·상세의 무료·저장값 true fixture와 true assertion 확인.
- `git diff --check`, `git diff --cached --check` — 출력 없음.
- `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL`, exit code 0.
- compile/test — 사용자 지시에 따라 재실행하지 않고 `plan-task.md`의 기존 성공 기록만 확인.
- 판정: `REV-P1-001`, `REV-P1-002` 수정 완료.
- 남은 항목: `REV-P1-003`, `P1-R2`.
### 2차 수정 검증 — 2026-07-31
- 무엇을: `REV-P1-003`의 수정 내용과 회귀 증거를 재검토했다.
- 왜: legacy 상세의 유료·저장값 true positive 계약이 자동 검증으로 고정됐는지 확인하기 위해서다.
- 어떻게:
- `AudioContentServiceTest` — 무료·저장값 true → false, 유료·저장값 false → false, 유료·저장값 true → true assertion 확인.
- `AudioContentService` — 응답식 `audioContent.isPointAvailable && audioContent.price > 0`과 세 assertion 대조.
- `plan-task.md`의 기존 완료 기록 — focused test `BUILD SUCCESSFUL`(14s), P1-GATE 단위 범위
`BUILD SUCCESSFUL`(2m 26s), `ktlintCheck` `BUILD SUCCESSFUL`(14s) 확인.
- `git diff --check`, `git diff --cached --check` — 출력 없음.
- `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL`, exit code 0.
- compile/test — 사용자 지시에 따라 이번 재검토에서는 실행하지 않음.
- 판정: `REV-P1-003` 수정 완료.
- 남은 항목: 없음.

View File

@@ -0,0 +1,111 @@
# Phase 2 코드 리뷰 보고서
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 2 / `P2-T1`, `P2-GATE` |
| 기준 commit 또는 working tree | `eb0ff7537e5fa6b083be21df3319be0ff2ecda51` + staged working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `docs/20260731_무료_콘텐츠_포인트_결제_불가/prd.md`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `POINT-005~006`, `POINT-008` 구현과 완료 증거가 일치하는지 확인한다.
- 추천과 전체 탭 POINT 조회가 무료 콘텐츠를 목록·count·pagination 후보에서 제외하는지 확인한다.
### 포함 범위
- 코드: 추천·전체 탭 QueryDSL repository 변경
- 테스트: 두 repository test와 추천·전체 탭 E2E test
- 문서: 관련 PRD, 기존 추천·전체 탭 후속 정정, `P2-T1`, `P2-GATE`, Progress
### 제외 범위
- Phase 1 응답 mapper
- FREE/AUDIO/정렬 정책의 신규 변경
- compile과 test 재실행
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
| High | 확정 요구사항 또는 공개 API 계약 위반 |
| Medium | 제한된 조건의 목록·count·pagination 불일치 |
| Low | 문서 정합성 또는 비핵심 회귀 증거 누락 |
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `POINT-005~006`, `POINT-008`
- 계획: `P2-T1`, `P2-GATE`
- 코드:
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt:135`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt:35`
- `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt:409`
- 테스트:
- `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt:87`
- `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepositoryTest.kt:43`
- `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt:42`
- `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt:97`
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `git diff --cached --check` | 성공 | 출력 없음 |
| staged diff와 `rg` 기반 조건·호출 대조 | 성공 | 추천은 유료 POINT 조건, 전체 탭 list/count는 같은 `audioCondition` 공유 |
| `./gradlew --no-daemon tasks --all` | 성공 | `BUILD SUCCESSFUL`, exit code 0 |
| compile/test | 미실행 | 사용자 지시에 따라 기존 성공 기록만 확인 |
## 5. 발견 사항 요약
확정 발견 사항 없음.
## 6. 검토 결과
- 추천 `findPointAudios``isPointAvailable.isTrue.and(price.gt(0))`을 조회 전에 적용해 limit 후보에서도 무료 콘텐츠를 제외한다.
- 전체 탭 `countAudios``findAudios`는 동일한 `audioCondition``optionalAudioPointCondition`을 사용한다.
- 전체 탭 E2E는 유료 1건과 무료·저장값 true 1건에서 `size=1`로 조회해 `totalCount=1`, 목록 1건,
`hasNext=false`를 함께 검증한다.
- repository test는 유료·저장값 false, 유료·저장값 true, 무료·저장값 true를 구분한다.
- 기존 추천·전체 탭 문서는 2026-07-31 후속 요구사항 정정을 누적해 현재 PRD와 일치한다.
## 7. 확정 항목의 plan·goal 전환
전환 항목 없음.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Phase 2 production/test/document diff와 관련 호출 경계 확인 |
| 후보 항목 판정 완료 | 충족 | 후보 없음 |
| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | 정적 검증 기록, test 미실행 사유 명시 |
**최종 결론:** 확정 발견 사항 없음
**남은 항목:** 없음
## 9. 후속 상태 확인
### 1차 재확인 — 2026-07-31
- 무엇을: Phase 1의 `P1-R2` 수정이 Phase 2 POINT 조회 계약에 영향을 주지 않았는지 재검토했다.
- 왜: 후속 test 변경 뒤에도 POINT 목록·count·pagination 조건과 기존 Phase 2 판정이 유효한지 확인하기 위해서다.
- 어떻게:
- production diff — `P1-R2`에 따른 Phase 2 repository 변경 없음 확인.
- repository와 E2E test 정적 대조 — 추천 유료 POINT 조건과 전체 탭 list/count 공통 조건 유지 확인.
- `git diff --check`, `git diff --cached --check` — 출력 없음.
- `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL`, exit code 0.
- compile/test — 사용자 지시에 따라 이번 재검토에서는 실행하지 않음.
- 판정: 기존 Phase 2 판정 유지.
- 남은 항목: 없음.