Files

1348 lines
87 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 3 오디오 콘텐츠 관리 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 3 / 오디오 콘텐츠 관리와 signed URL vertical slice |
| 기준 commit 또는 working tree | `feat/ai-character-admin-api` working tree |
| 리뷰 일자 | 2026-07-27 |
| 리뷰어 | Sisyphus |
| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
### 목적
- PRD Feature C와 Endpoint Contract Summary의 테마, 목록, 상세, 생성, 수정, soft delete, signed URL 계약을 현재 Phase 3 코드와 테스트에 추적한다.
- 기존 완료 이력(`P3-H1`, `P3-H2`) 이후 남은 확정 finding을 `P3-T3`~`P3-T7` 소유 Goal에 연결한다.
### 포함 범위
- 코드: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*`
- 테스트: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*`
- 문서: PRD Feature C, API Expectations, plan Phase 3
### 제외 범위
- Phase 4 이후 시리즈/커뮤니티/FanTalk 기능, production code 수정, 신규 계약 변경
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 보안·데이터 손실 위험, 핵심 흐름 불능, 완료 판정을 무효화하는 문제 |
| High | 확정 요구사항·API Contract 위반 또는 주요 회귀 |
| Medium | 제한된 조건에서 발생하는 기능·복구 문제 |
| Low | 유지보수성, 문서 정합성 문제 |
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `prd.md` Feature C, API Expectations 184~189, Metrics 207~221
- 계획: `plan-task.md` `P3-R1`, `P3-T3`~`P3-T7`, `REV-004`~`REV-008`
- 코드: `AiCharacterAdminAudioContentController.kt`, `AiCharacterAdminAudioContentFacade.kt`, `AiCharacterAdminAudioContentDto.kt`, `AiCharacterAdminAudioContentMapper.kt`, `AiCharacterAdminAudioContentRepository.kt`
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt`, `LegacyCreatorAdminAudioContentCharacterizationTest.kt`, `AudioContentCloudFrontCharacterizationTest.kt`
### 실행 환경
```text
OS: macOS (darwin)
Java/Gradle: repository Gradle wrapper
환경 변수: test profile defaults, EmbeddedRedisInitializer, mocked AudioContentCloudFront/AmazonS3Client
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` | 성공 | `BUILD SUCCESSFUL in 2s`, Gradle `UP-TO-DATE` |
| 코드 추적 | 성공 | controller/facade/DTO/repository/test를 PRD와 대조 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-004` | Medium | 확정 | 콘텐츠 수정의 `seriesIds` 교체가 기존 연결 metadata를 보존하지 않음 | `P3-T6` | 차집합 기반 연결 갱신 |
| `REV-005` | High | 확정 | 상세 응답이 legacy/public DTO 타입에 직접 의존하고 일부 legacy parity 필드 검증이 분리되지 않음 | `P3-T4` | v2 전용 중첩 DTO와 상세 parity 보강 |
| `REV-006` | High | 확정 | 생성 multipart·field·failure-order 계약 증거가 분리된 소유 테스트로 부족함 | `P3-T5`, `P3-T7` | 생성 pipeline과 오류 matrix 보강 |
| `REV-007` | Medium | 확정 | 테마·조회 DTO 경계와 exact field set 증거가 단일 대형 controller test에 섞여 있음 | `P3-T3`, `P3-T4` | 전용 focused test로 계약 분리 |
| `REV-008` | High | 확정 | 실제 endpoint별 ADMIN·CORS·i18n·legacy 회귀 matrix가 부족함 | `P3-T7` | endpoint/error/ownership matrix 보강 |
## 6. 발견 사항 상세
### REV-004 — `seriesIds` 교체가 기존 연결 metadata를 삭제함
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** PRD Feature C, Feature D 선행 owner query 계약
- **관련 계약:** 콘텐츠 수정의 `seriesIds` 연결 유지와 owner 검증
- **소유 Task:** `P3-T6`
**관찰 내용**
`AiCharacterAdminAudioContentRepository.replaceSeriesIds`는 현재 연결을 모두 remove한 뒤 요청 series를 새 `SeriesContent`로 persist한다. 같은 `seriesIds`를 다시 PUT해도 기존 `SeriesContent` row ID, `orders`, `createdAt`이 보존되지 않는다.
**근거**
- 코드: `AiCharacterAdminAudioContentRepository.kt` `replaceSeriesIds`가 기존 `SeriesContent` 전체 삭제 후 재생성
- 코드: `AiCharacterAdminAudioContentFacade.kt` update가 `request.seriesIds` 존재 시 항상 `replaceSeriesIds` 호출
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt`는 교체 결과 ID 목록만 검증하고 교집합 metadata 보존을 검증하지 않음
**권장 조치**
`P3-T6`에서 동일 ID, 추가 ID, 제거 ID 케이스를 RED로 만들고, 교집합은 그대로 두고 차집합만 insert/delete한다.
### REV-005 — 상세 응답 DTO 경계와 legacy parity 검증 부족
- **심각도:** High
- **상태:** 확정
- **관련 요구사항:** PRD API Expectations 188~189
- **관련 계약:** 신규 v2 전용 DTO, legacy 상세 field 전체 포함, viewer 상태 기본값
- **소유 Task:** `P3-T4`
**관찰 내용**
`AiCharacterAdminAudioContentResponse``AudioContentCreator`, `ContentBuyer`, `OtherContentResponse`, `GetAudioContentCommentListItem`, `OrderType`, `TranslatedContent` 같은 legacy/public DTO 타입을 직접 노출한다. 또한 현재 상세 테스트는 JSON key 일부를 검증하지만, legacy 상세의 rental 파생값과 `releaseDate` 의미를 독립 focused test로 분리하지 않았다.
**근거**
- 코드: `AiCharacterAdminAudioContentDto.kt` legacy/public DTO imports
- 코드: `AiCharacterAdminAudioContentMapper.kt` viewer 상태는 기본값으로 채우나 타입은 legacy DTO에 의존
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt` 상세 happy path 안에 여러 상세 field assertion이 혼재
**권장 조치**
`P3-T4`에서 exact JSON schema와 legacy compatibility field RED를 추가한 뒤, v2 전용 중첩 DTO로 최소 매핑한다.
### REV-006 — 생성 pipeline과 오류 순서 증거 부족
- **심각도:** High
- **상태:** 확정
- **관련 요구사항:** PRD Feature C, API Expectations 179~185
- **관련 계약:** multipart 필수 part, `description/releaseDateUtc` 변환, target/theme/series 실패 no-side-effect
- **소유 Task:** `P3-T5`, `P3-T7`
**관찰 내용**
생성 정상·일부 실패 테스트는 존재하지만 `coverImage`, `audioFile`, `request` 각 part 누락의 exact exception/envelope, 전체 field 변환, theme 오류, S3/processing/event 실패 순서가 독립 소유 테스트로 충분히 분리되어 있지 않다. controller는 `coverImage``audioFile`을 nullable `MultipartFile?`로 받아 필수 part 계약을 별도 RED로 확인해야 한다.
**근거**
- 코드: `AiCharacterAdminAudioContentController.kt` create의 `coverImage`, `audioFile` nullable binding
- 코드: `AiCharacterAdminAudioContentFacade.kt` create가 target, request parse, `isActive`, series 검증 뒤 legacy `AudioContentService.createAudioContent` 호출
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt` 생성 정상, invalid series, inactive, invalid price는 있으나 part별 exception/i18n matrix는 없음
**권장 조치**
`P3-T5`에서 필수 part와 field 변환 RED를 만들고, `P3-T7`에서 actual endpoint 오류/i18n matrix로 묶어 회귀한다.
### REV-007 — 테마·조회 DTO focused 증거 분리 필요
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** PRD Feature C Requirements 98, API Expectations 186~189
- **관련 계약:** 테마 `themeId/themeName/imageUrl`, 목록/상세 v2 DTO 경계
- **소유 Task:** `P3-T3`, `P3-T4`
**관찰 내용**
테마 endpoint와 조회 계약은 현재 단일 `AiCharacterAdminAudioContentControllerTest` 안에서 일부 검증된다. 하지만 계획은 `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest` 같은 소유 focused test로 계약을 분리해 Gate 증거로 삼도록 요구한다.
**근거**
- 코드: `AiCharacterAdminAudioContentController.kt` `GET /audio-content-themes` 존재
- 코드: `AiCharacterAdminAudioContentDto.kt` `AiCharacterAdminAudioContentThemeResponse`는 v2 field name 사용
- 테스트: 테마/조회 assertion이 `AiCharacterAdminAudioContentControllerTest.kt`에 집중됨
- 문서: `plan-task.md` `P3-T3`, `P3-T4`의 전용 테스트 파일 요구
**권장 조치**
이미 만족한 production 동작은 재구현하지 말고, `P3-T3`/`P3-T4`에서 focused test와 Progress 증거만 최소 보강한다.
### REV-008 — endpoint 보안·오류·회귀 matrix 부족
- **심각도:** High
- **상태:** 확정
- **관련 요구사항:** PRD Metrics 207~221
- **관련 계약:** endpoint별 ADMIN, 오류 i18n, ownership no-side-effect, legacy 회귀
- **소유 Task:** `P3-T7`
**관찰 내용**
현재 content focused test는 anonymous/admin smoke와 일부 domain failure를 포함하지만, 테마·목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim, 허용·거부 Origin, target/content/theme/series/date와 multipart 누락의 exact status/key/KO·EN·JA matrix는 아직 Phase 3 endpoint 기준으로 충분히 고정되어 있지 않다.
**근거**
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt` 목록 anonymous/admin smoke만 존재
- 테스트: 공통 `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`에 Phase 3 실제 endpoint별 matrix 보강 필요
- 문서: `plan-task.md` `P3-T7` RED/GREEN 조건
**권장 조치**
`P3-T7`에서 실제 Phase 3 endpoint matrix와 legacy characterization 회귀를 focused 명령으로 보강한다.
## 7. 확정 항목의 plan·goal 전환
확정 항목은 기존 `plan-task.md``P3-T3`~`P3-T7`에 이미 소유 Goal로 연결되어 있다. 신규 회귀 수정 Task는 만들지 않는다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | PRD Feature C, Endpoint Contract Summary, Phase 3 production/test 대조 |
| 후보 항목 판정 완료 | 충족 | `REV-004`~`REV-008` 확정 |
| 확정 항목 plan 반영 | 충족 | 기존 `P3-T3`~`P3-T7`에 연결됨 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | content focused test `BUILD SUCCESSFUL in 2s` |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P3-T3`부터 직렬 실행한다.
## 9. 수정 후 검증 기록
기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다.
### 1차 수정 검증 — 2026-07-27
- 무엇을: `P3-T3`에서 `REV-007`, `REV-008` 중 활성 콘텐츠 테마 endpoint의 focused 증거를 분리했다.
- 왜: 테마 production 동작은 이미 존재했지만 Gate에서 요구하는 request body 없는 GET, 활성 필터, v2 전용 field set, ADMIN 인증 증거가 단일 controller test에 섞여 있었기 때문이다.
- 어떻게:
- `AiCharacterAdminAudioContentThemeControllerTest` 추가: 활성 테마만 `themeId/themeName/imageUrl`로 반환, legacy `id/theme/image` 비노출, anonymous 401 검증.
- `AiCharacterAdminAudioContentControllerTest`의 중복 테마 케이스 제거.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest``BUILD SUCCESSFUL in 35s`.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest``BUILD SUCCESSFUL in 48s`.
- `./gradlew ktlintCheck``BUILD SUCCESSFUL in 11s`.
- 남은 항목: `P3-T4`, `P3-T5`, `P3-T6`, `P3-T7`, `P3-GATE`.
### 2차 수정 검증 — 2026-07-27
- 무엇을: `P3-T4`에서 `REV-005`, `REV-007` 중 콘텐츠 상세 조회 DTO와 signed URL/query 증거를 보완했다.
- 왜: 상세 응답이 legacy/public 중첩 DTO 타입에 직접 의존했고, 과거 공개일을 `releaseDateUtc`뿐 아니라 legacy `releaseDate`에도 노출했기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentQueryTest` 추가 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest` — 2건 실패. `releaseDate` 존재와 legacy nested DTO package 누출을 확인했다.
- GREEN: `AiCharacterAdminAudioContentDto`에 v2 전용 중첩 DTO를 추가하고, mapper에서 `releaseDate=null`, `releaseDateUtc` 유지, RENT_ONLY 파생값, creator DTO mapping만 최소 수정했다.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest``BUILD SUCCESSFUL in 29s`.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AudioContentCloudFrontCharacterizationTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest``BUILD SUCCESSFUL in 41s`.
- `./gradlew ktlintCheck` — 최초 unused import 2건 실패 후 정리 재실행에서 `BUILD SUCCESSFUL in 10s`.
- 남은 항목: `P3-T5`, `P3-T6`, `P3-T7`, `P3-GATE`.
### 3차 수정 검증 — 2026-07-27
- 무엇을: `P3-T5`에서 `REV-006`, `REV-008` 중 콘텐츠 생성 multipart와 legacy 기본 계약 증거를 분리했다.
- 왜: 생성 필수 part, `tags` 누락, `isActive=true` 요청의 실제 legacy processing 결과가 독립 focused test로 고정되어야 했기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentCreateTest` 추가 후 focused 실행에서 정상 생성 케이스가 200 기대 대비 500으로 실패했다. 원인은 production 계약이 아니라 test fixture의 `AmazonS3Client.getUrl` 미설정이었다.
- GREEN: S3 `getUrl` mock을 추가해 기존 `S3Uploader` 동작을 test fixture에서 충족했다. production code는 변경하지 않았다.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest``BUILD SUCCESSFUL`.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest``BUILD SUCCESSFUL`.
- `./gradlew ktlintCheck``BUILD SUCCESSFUL in 10s`.
- 남은 항목: `P3-T6`, `P3-T7`, `P3-GATE`.
### 4차 수정 검증 — 2026-07-27
- 무엇을: `P3-T6`에서 `REV-004`의 콘텐츠 수정 `seriesIds` 재연결 metadata 손실을 보완했다.
- 왜: 같은 `seriesIds`를 다시 PUT해도 기존 `SeriesContent` row ID, `orders`, `createdAt`은 보존되어야 하기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentUpdateTest` 추가 후 focused 실행에서 정상 응답 기대가 500으로 실패했다. 원인은 production 로직이 아니라 test fixture의 `AudioContentCloudFront` private key 경로 문제였다.
- GREEN: focused test fixture에 `AudioContentCloudFront` mock을 추가하고, `replaceSeriesIds`를 차집합 기반으로 수정해 기존 교집합 row는 유지하고 제거·추가 대상만 변경했다.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest``BUILD SUCCESSFUL in 46s`.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest``BUILD SUCCESSFUL in 1m 21s`.
- 남은 항목: `P3-T7`, `P3-GATE`.
### 5차 수정 검증 — 2026-07-27
- 무엇을: `P3-T7`에서 실제 Phase 3 endpoint의 ownership·ADMIN·오류/i18n·CORS와 legacy 회귀 증거를 완료했다.
- 왜: 공통 prefix contract만으로는 콘텐츠 resource path의 문자열 식별자가 404 fallback이 아닌 400/415로 처리되는 회귀를 검출할 수 없었기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentOwnershipTest`의 목록·상세·생성·수정 malformed `characterId`/`contentId` 6건이 404 EN envelope을 기대하도록 작성했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행에서 18건 중 6건이 400 또는 415를 반환해 `BUILD FAILED in 51s`였다.
- GREEN: `AiCharacterAdminAudioContentController`의 모든 resource `characterId`/`contentId` mapping에 `[0-9]+`를 적용했다. 같은 focused command는 실제 endpoint의 non-ADMIN/stale claim 403 envelope, unknown target 생성 S3 0회, 테마 preflight 허용·거부 Origin도 포함해 `BUILD SUCCESSFUL in 1m 5s`였다.
- REFACTOR: 기존 `AiCharacterAdminAudioContentThemeControllerTest`, `QueryTest`, `CreateTest`, `UpdateTest`, `LegacyCreatorAdminAudioContentCharacterizationTest`, `AudioContentCloudFrontCharacterizationTest`와 공통 authorization/error contract를 재사용했다. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest``BUILD SUCCESSFUL in 2m 8s`, `./gradlew ktlintCheck``BUILD SUCCESSFUL in 29s`였다.
- 정정 기록: 계획의 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`는 현재 존재하지 않는 과거 참조다. 기존 기록을 삭제하지 않고 실제 focused file 목록과 위 regression 명령으로 대체 증거를 누적했다.
- 남은 항목: `P3-GATE`.
## 10. 2차 리뷰 및 검증 — 2026-07-27
### 리뷰 정보와 실행 결과
- 기준 working tree: Phase 2·3 staged/unstaged 변경 전체
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
- 검토 범위: PRD Feature C, Endpoint Contract Summary, `P3-T3`~`P3-GATE`, content production/test와 공통 authorization/error test
- fresh 검증:
- `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest``BUILD SUCCESSFUL in 9m 23s`, 관련 XML 14개 합계 199건, failure/error/skipped 0.
- Phase 3 content package는 8개 XML class, 53건, failure/error/skipped 0.
- `./gradlew ktlintCheck --rerun-tasks``BUILD SUCCESSFUL in 27s`, 7개 task 실행.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-010` | Medium | 처리 완료 | 생성 pipeline·multipart 오류·실패 순서의 완료 증거가 계획보다 좁음 | `Task 3.9` | `P3-R2` |
| `REV-011` | Medium | 처리 완료 | 수정 차집합·cover·ownership no-side-effect의 완료 증거가 계획보다 좁음 | `Task 3.10` | `P3-R3` |
### REV-010 — 생성 pipeline과 실제 multipart 오류 증거 보강 필요
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** PRD Feature C, Metrics 207~221
- **관련 계약:** 생성 전체 field 변환, target/theme/series 선검증, S3/processing/event 실패 순서, 세 필수 part의 실제 endpoint KO/EN/JA envelope
- **소유 Task:** `Task 3.9`, `P3-R2`
**관찰 내용**
`P3-T5`는 생성 전체 field와 target/theme/series 오류, S3/processing/event 실패 순서, 세 필수 part의 exact KO/EN/JA를 완료 처리했다. 실제 `AiCharacterAdminAudioContentCreateTest`는 필수 part 3건과 최소 생성 1건이고, controller test는 전체 field 정상 생성·cross-owner series·`isActive=false`·가격 오류를 확인한다. invalid theme, cover/audio 업로드 중 실패 후 DB/S3/event 관찰 결과, 실제 생성 endpoint의 part별 KO/EN/JA는 직접 고정되지 않았다.
**근거**
- 테스트: `AiCharacterAdminAudioContentCreateTest.kt`는 4건이며 missing-part 응답에서 `success=false`만 확인한다.
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:59`~`257`은 정상 전체 field, cross-owner series, inactive, 가격 오류를 확인하지만 invalid theme와 S3/event 실패를 포함하지 않는다.
- 코드: `AudioContentService.createAudioContent`는 DB 저장 뒤 cover와 audio를 순서대로 업로드하고 event를 발행하므로 각 실패 지점의 실제 잔존 결과를 별도 특성화해야 한다.
- 문서: `plan-task.md` `P3-T5`의 RED/REFACTOR와 `P3-GATE`가 해당 증거를 완료로 기록했다.
**권장 조치**
`P3-R2`에서 실제 생성 endpoint의 세 missing part KO/EN/JA, invalid theme/series/target 선검증, 전체 field adapter와 cover/audio/event 실패 순서를 test로 고정한다. legacy pipeline에서 관찰되는 S3 비트랜잭션 결과는 추정으로 보상하지 말고 characterization 결과와 PRD parity를 먼저 판정한다.
**처리 결과**
`AiCharacterAdminAudioContentCreateTest``coverImage`, `audioFile`, `request` 누락 KO/EN/JA envelope, invalid theme 선검증, cover upload 실패 rollback/event 0회 단언을 추가했다. 생산 코드는 변경하지 않았다.
### REV-011 — 수정 차집합과 ownership 실패 증거 보강 필요
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** PRD Feature C Edge Cases, Metrics 218~221
- **관련 계약:** `seriesIds` 교집합 metadata 보존과 추가·제거 차집합, cover 유지/교체, cross-owner/invalid series no-side-effect, 실제 endpoint 오류 matrix
- **소유 Task:** `Task 3.10`, `P3-R3`
**관찰 내용**
`P3-T6`는 동일·추가·제거 ID를 각각 검증했다고 완료 처리했지만 전용 update test는 동일 집합 1건이다. 기존 controller test의 old→new 교체는 제거와 추가 결과만 확인하고, 교집합을 유지하면서 동시에 추가·제거할 때 row ID·`orders`·`createdAt`이 보존되는지 확인하지 않는다. 성공적인 v2 cover 교체와 실패 시 DB/S3/event 결과도 legacy service characterization과 분리된 actual endpoint 증거가 없다. `P3-T7`의 실제 endpoint KO/EN/JA·ownership no-side-effect 기록도 ownership test에서는 EN envelope과 S3 0회 중심으로 확인된다.
**근거**
- 테스트: `AiCharacterAdminAudioContentUpdateTest.kt:50`은 동일 `seriesIds` 1건만 포함한다.
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:112`는 old→new 결과 ID만 확인하고 교집합 metadata를 확인하지 않는다.
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:559` 이후는 cross-owner DB title, invalid series S3 0회, invalid date S3 0회를 각각 확인하지만 성공/실패 cover와 DB/S3/event 전체 경계를 한 actual endpoint test로 고정하지 않는다.
- 테스트: `AiCharacterAdminAudioContentOwnershipTest.kt`는 actual endpoint stale claim은 확인하지만 domain 오류는 EN 중심이며 DB/event count를 직접 확인하지 않는다.
**권장 조치**
`P3-R3`에서 기존 교집합+신규 추가+기존 제거를 한 요청에 포함한 metadata test, cover 유지/성공 교체/실패 결과, cross-owner·invalid series/date·soft delete의 DB/S3/event 및 KO/EN/JA contract를 보강한다. 현재 production이 이미 충족하면 test-only로 종료한다.
**처리 결과**
`AiCharacterAdminAudioContentUpdateTest``AiCharacterAdminAudioContentOwnershipTest`의 Phase 3 후속 보강분을 content/common 회귀로 재검증했다. production code 변경 없이 처리 완료로 판정했다.
**판정 기록**
- 2026-07-27 — fresh content 53건과 lint는 모두 통과했으나 `P3-T5`~`P3-T7` 체크리스트와 실제 test method를 대조해 `REV-010`, `REV-011`을 확정했다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.9` / `P3-R2`, `Task 3.10` / `P3-R3`와 별도 후속 Gate `P3-R2-GATE`를 추가한다. 기존 `P3-T3`~`P3-GATE` 완료 이력은 변경하지 않는다.
### 2차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | PRD·계획·production·test 재대조 |
| 후보 항목 판정 완료 | 충족 | `REV-010`, `REV-011` 확정 |
| 확정 항목 plan 반영 | 충족 | `Task 3.9`, `Task 3.10`, 후속 Gate |
| 검증 명령과 결과 기록 | 충족 | fresh content 53건 포함 targeted 199건, lint 성공 |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P2-R2-GATE``P3-R2``P3-R3``P3-R2-GATE` 순서로 실행한다.
## 11. 3차 후속 검증 — 2026-07-27
- 무엇을: `P3-R2`, `P3-R3`, `P3-R2-GATE` 후속 보완을 완료했다.
- 왜: `REV-010`~`REV-011`의 직접 증거 누락을 실제 Phase 3 endpoint와 content/common 회귀로 종결하기 위해서다.
- 어떻게:
- `AiCharacterAdminAudioContentCreateTest`의 실패 8건을 분석해, production 계약 위반이 아니라 테스트 기대 message와 `NOT_SUPPORTED` fixture transaction 누락임을 확인했다.
- 테스트 기대값을 실제 i18n 계약에 맞추고 `TransactionTemplate`으로 fixture만 보정했다.
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest``BUILD SUCCESSFUL in 1m 4s`.
- `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest``BUILD SUCCESSFUL in 2m 41s`.
- `./gradlew ktlintCheck` — import ordering 1건 실패 후 정렬만 수정해 재실행, `BUILD SUCCESSFUL in 16s`.
- `git diff --check` — 출력 없음.
- 최종 결론: `REV-010`, `REV-011` 처리 완료. Phase 3 후속 리뷰 보완 종료.
- 남은 항목: 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다.
## 12. 4차 재리뷰 및 검증 — 2026-07-27
### 리뷰 정보와 실행 결과
- 기준 working tree: `P3-R2`~`P3-R3` 반영 후 staged/unstaged Phase 3 변경
- 리뷰 상태: 판정 완료, 추가 수정 goal 필요
- fresh 검증:
- `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest``BUILD SUCCESSFUL in 9m 44s`, 관련 XML 14개 합계 228건, failure/error/skipped 0.
- `./gradlew ktlintCheck --rerun-tasks``BUILD SUCCESSFUL in 41s`, 7개 task 실행.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-013` | Medium | 처리 완료 | 생성의 audio upload·event 실패 순서 직접 증거가 없음 | `Task 3.11` | `P3-R4` |
| `REV-014` | Medium | 처리 완료 | ownership/domain 오류의 KO/EN/JA·no-side-effect matrix가 미완성 | `Task 3.11` | `P3-R4` |
### REV-013 — 생성 후반 S3·event 실패 순서 증거 보강 필요
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C, Metrics 207~221
- **관련 계약:** cover upload 뒤 audio upload와 event publish 실패의 DB/S3/event 결과 및 legacy parity
- **소유 Task:** `Task 3.11`, `P3-R4`
**관찰 내용**
`P3-R2` 반영으로 세 필수 part의 KO/EN/JA, invalid theme 선검증과 첫 cover upload 실패 rollback은 직접 고정됐다. 그러나 완료 처리한 `Task 3.9`는 cover upload, audio upload, event 실패 지점별 결과를 요구한다. 새 test는 첫 `putObject`가 실패하는 경우만 만들며, 두 번째 audio upload 또는 두 업로드 뒤 event publish 실패를 주입하지 않는다.
**근거**
- 코드: `AudioContentService.kt:333`, `362`, `381`은 cover upload → audio upload → event 순서다.
- 테스트: `AiCharacterAdminAudioContentCreateTest.kt:179`~`208`은 첫 `putObject`에 예외를 주입하는 cover 실패 1건만 포함한다.
- 테스트: `LegacyCreatorAdminAudioContentCharacterizationTest.kt:108`은 정상 생성의 S3 두 번 호출만 확인하고 각 실패 결과는 특성화하지 않는다.
- 문서: `plan-task.md` `Task 3.9``FAILURE CHARACTERIZATION`은 세 실패 지점과 비트랜잭션 S3 결과를 모두 완료 처리했다.
**영향**
audio upload 또는 event 실패에서 DB는 롤백되지만 이미 업로드된 S3 객체가 남는 legacy 경계가 변경되어도 현재 회귀가 감지하지 못한다.
**권장 조치**
두 번째 S3 호출 실패와 event publish 실패를 실제 endpoint에서 재현하고 DB/SeriesContent rollback, S3 호출·잔존 결과와 event 결과를 각각 고정한다. 추정으로 보상 로직을 추가하지 않고 legacy parity를 먼저 판정한다.
**처리 결과**
`AiCharacterAdminAudioContentCreateTest`에 audio upload 실패와 event publish 실패를 추가했다. 실제 endpoint에서 S3 두 번 호출, DB rollback, event 미발행/발행 실패를 고정했고 production code는 변경하지 않았다.
### REV-014 — ownership/domain no-side-effect matrix 보강 필요
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** PRD Metrics 218~221, Acceptance Criteria 225~254
- **관련 계약:** 목록·상세·생성·수정의 target/ownership/domain KO/EN/JA envelope과 DB/S3/event 무변경
- **소유 Task:** `Task 3.11`, `P3-R4`
**관찰 내용**
`P3-R3` 반영으로 series 교집합 metadata와 차집합, cover 유지·교체·첫 upload 실패가 고정됐고 존재하지 않는 character 생성은 KO/EN/JA로 확장됐다. 그러나 `Task 3.10`이 완료 조건으로 둔 목록·상세·생성·수정 전체의 ownership/domain KO/EN/JA와 DB insert/update/delete·S3·event count는 아직 직접 확인되지 않는다. cross-owner 상세·수정과 invalid series/date는 기존 EN 또는 `success=false`, 일부 DB/S3 단언에 머물고 event를 관찰하지 않는다.
**근거**
- 테스트: `AiCharacterAdminAudioContentOwnershipTest.kt:89`의 KO/EN/JA domain matrix는 존재하지 않는 character 생성 한 경로뿐이다.
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:530` 이후 cross-owner 상세·수정, invalid series/date는 언어 matrix와 DB/S3/event 전체 count를 함께 단언하지 않는다.
- 테스트: `AiCharacterAdminAudioContentUpdateTest.kt:184`의 cover 실패는 DB title/cover만 확인하고 event 결과를 관찰하지 않는다.
- 문서: `plan-task.md` `Task 3.10`은 네 실제 endpoint의 ownership/domain 오류와 모든 부작용 count를 완료 처리했다.
**영향**
target/ownership 검증 순서가 늦어져 DB/S3/event 부작용이 먼저 발생하거나 특정 locale의 오류 계약이 달라져도 현재 부분 회귀만으로는 Gate가 놓칠 수 있다.
**권장 조치**
`P3-R4`에서 목록·상세·생성·수정의 대표 target/ownership/domain 거부를 KO/EN/JA actual endpoint test로 만들고, 각 요청 전후 AudioContent·SeriesContent와 S3/event count가 변하지 않음을 직접 단언한다.
**처리 결과**
`AiCharacterAdminAudioContentOwnershipTest`에 목록·상세·생성·수정 unknown target KO/EN/JA matrix와 AudioContent·SeriesContent·S3·event no-side-effect 단언을 추가했다. create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 20s`, 최종 `ktlintCheck``BUILD SUCCESSFUL in 17s`였다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.11` / `P3-R4``P3-R3-GATE`를 추가한다. 기존 `Task 3.9`~`Task 3.10``P3-R2-GATE` 완료 이력은 변경하지 않는다.
### 4차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 적용된 multipart/theme/series/cover 보강 | 충족 | 새 actual endpoint regression 추가 |
| 생성 후반 실패 순서 직접 증거 | 충족 | audio upload·event failure injection 추가 |
| ownership/domain 전체 matrix | 충족 | 실제 네 경로 KO/EN/JA 및 부작용 count 추가 |
| 확정 항목 plan 반영 | 충족 | `Task 3.11`, `P3-R4`, `P3-R3-GATE` |
| fresh 검증 | 충족 | targeted 228건, lint 성공 |
**최종 결론:** Phase 3 4차 리뷰 종결
**남은 항목:** 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다.
## 13. 5차 재리뷰 및 판정 — 2026-07-27
### 리뷰 정보와 실행 결과
- 기준 working tree: `P2-R3``P3-R4` 반영 후 staged/unstaged Phase 2·3 변경
- 리뷰 상태: 판정 완료, 추가 수정 goal 필요
- 검토 범위: 생성 multipart 필수 part 계약, `REV-014` ownership/domain 처리 결과
- fresh 검증:
- `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest``BUILD SUCCESSFUL in 7m 50s`, 세 XML 합계 79건, failure/error/skipped 0.
- `./gradlew ktlintCheck --rerun-tasks``BUILD SUCCESSFUL in 28s`, 7개 task 실행.
- `git diff --check` — 출력 없음.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-016` | High | 처리 완료 | 생성 필수 파일 part가 nullable이라 exact missing-part 계약을 우회했음 | `Task 3.12` | `P3-R5` |
| `REV-017` | Medium | 처리 완료 | ownership/domain 전체 matrix 완료 기록보다 실제 보강 범위가 좁음 | `Task 3.13` | `P3-R6` |
### REV-016 — 생성 필수 multipart part의 exact binding 계약 미충족
- **심각도:** High
- **상태:** 처리 완료
- **관련 요구사항:** PRD API Expectations 179~180, Metrics 216~218, Acceptance Criteria 243~248
- **관련 계약:** `coverImage`, `audioFile`, `request` 누락의 `MissingServletRequestPartException`, 400 `common.error.invalid_request`, KO/EN/JA `ApiResponse.error`
- **소유 Task:** `Task 3.12`, `P3-R5`
**관찰 내용**
`P3-T5`는 필수 파일 part를 non-null binding으로 변경한다고 완료 처리했지만 controller의 생성 `coverImage``audioFile`은 여전히 `MultipartFile?`이다. 현재 보강 테스트는 누락 요청의 status와 legacy content 전용 message만 확인하고 resolved exception type을 확인하지 않아, 필수 part가 MVC binding에서 거부되지 않고 facade/legacy service까지 전달되는 현재 동작을 허용한다.
**근거**
- 코드: `AiCharacterAdminAudioContentController.kt:47`~`48`은 생성 `coverImage`, `audioFile`을 nullable로 선언한다.
- 코드: `AudioContentService.kt:217`~`237`은 null 파일을 `SodaException``content.error.cover_image_required` 또는 `content.error.content_required`로 변환한다.
- 테스트: `AiCharacterAdminAudioContentCreateTest.kt:62`~`154`는 세 누락 part의 status/message와 부작용만 확인하고 `resolvedException`을 단언하지 않는다.
- 문서: `prd.md:179`~`180``plan-task.md:1496`~`1500`은 실제 필수 part 누락의 exact `MissingServletRequestPartException`과 non-null binding을 요구한다.
**재현 또는 검증 절차**
1. actual 생성 endpoint에 `coverImage` 또는 `audioFile`을 제외한 multipart 요청을 보낸다.
2. `MvcResult.resolvedException`과 응답 message를 확인한다.
3. 현재 테스트 기대는 content 전용 message이며 exact `MissingServletRequestPartException`을 고정하지 않는다.
4. 요구 결과는 세 필수 part 모두 MVC binding에서 `MissingServletRequestPartException`으로 종료되고 400 `common.error.invalid_request` KO/EN/JA envelope을 반환하는 것이다.
**영향**
확정 API 오류 계약과 실제 생성 endpoint의 binding/error key가 다르며, facade 또는 legacy service가 호출되기 전 종료돼야 한다는 trust-boundary 보장이 약해진다.
**권장 조치**
`P3-R5`에서 생성 controller와 facade의 두 파일 인자를 non-null로 고정하고, `coverImage`·`audioFile`·`request` 각각의 KO/EN/JA actual endpoint test에서 exact exception type, generic invalid-request envelope과 facade/DB/S3/event 0회를 확인한다.
**판정 기록**
- 2026-07-27 — PRD·기존 Task 완료 조건과 현재 nullable signature/test assertion을 대조해 contract 위반을 확정했다.
- 2026-07-27 — `P3-R5`에서 controller/facade 생성 파일 인자를 non-null로 고정했다. actual endpoint KO/EN/JA 9건은 exact `MissingServletRequestPartException`, 400 `common.error.invalid_request` envelope, DB count 0, S3 `putObject` 0회, event no-interaction을 확인했다. create/error focused와 content/common 회귀, `ktlintCheck`가 성공했다.
### REV-017 — ownership/domain no-side-effect matrix 보강 범위 부족
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** PRD Edge Cases 78~81, Metrics 218~221, Acceptance Criteria 245~248
- **관련 계약:** target 미존재와 cross-owner/domain 오류의 KO/EN/JA envelope 및 DB/S3/event 무변경
- **소유 Task:** `Task 3.13`, `P3-R6`
**관찰 내용**
`P3-R4`에서 새로 추가한 matrix는 목록·상세·생성·수정의 unknown `characterId`만 KO/EN/JA와 count 기반 no-side-effect로 확장했다. 4차 리뷰가 미완료로 지적한 cross-owner 콘텐츠 상세/수정, 다른 캐릭터 소유 `seriesIds`, invalid `releaseDateUtc`는 기존 단일 locale 또는 `success=false`와 일부 DB/S3 단언 상태이며 event와 전체 DB 상태를 함께 확인하지 않는다.
**근거**
- 테스트: `AiCharacterAdminAudioContentOwnershipTest.kt:95`~`188`의 신규 네 matrix는 모두 unknown `characterId=999999`만 사용한다.
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:529`~`708`의 cross-owner 콘텐츠, 다른 owner series, invalid date는 KO/EN/JA matrix가 아니고 event no-interaction을 단언하지 않는다.
- 리뷰: 이 문서 4차 권장 조치는 대표 target/ownership/domain 거부를 모두 KO/EN/JA와 부작용 count로 고정하도록 요구했지만 처리 결과는 unknown target만 기록한다.
- 문서: `plan-task.md:1681`~`1682`와 4차 종료 판정은 ownership/domain 전체 matrix와 insert/update/delete·S3·event count를 완료 처리한다.
**재현 또는 검증 절차**
1. 기존 cross-owner detail/update, 다른 owner `seriesIds`, invalid `releaseDateUtc` 테스트를 확인한다.
2. KO/EN/JA exact message와 요청 전후 AudioContent·SeriesContent 상태, S3/event 호출을 함께 단언하는지 대조한다.
3. 현재는 각 경로가 단일 locale·부분 assertion에 머물고, 신규 matrix는 unknown target만 다룬다.
4. 대표 ownership/domain 거부마다 동일한 actual endpoint envelope과 no-side-effect evidence가 필요하다.
**영향**
ownership 또는 domain validation 순서가 뒤로 이동해 DB update/delete나 S3/event가 먼저 발생하거나 특정 locale의 오류 key가 바뀌어도 현재 Gate가 놓칠 수 있다.
**권장 조치**
`P3-R6`에서 cross-owner detail/update, create/update의 다른 owner `seriesIds`, invalid `releaseDateUtc`를 KO/EN/JA actual endpoint matrix로 보강한다. 각 요청 전후 entity field/연결 상태와 S3/event 호출 0회를 확인하고, 현재 production이 충족하면 test-only로 종료한다.
**판정 기록**
- 2026-07-27 — fresh 테스트 성공과 별개로 4차 완료 조건과 실제 test method 범위를 대조해 증거 누락을 확정했다.
- 2026-07-27 — `P3-R6`에서 cross-owner detail/update, create/update 다른 owner `seriesIds`, invalid `releaseDateUtc`를 KO/EN/JA actual endpoint 15건으로 보강했다. exact 400 `ApiResponse.error`, 전후 `AudioContent`·`SeriesContent` count와 field/연결 row, S3 `putObject` 0회와 event no-interaction을 확인했다. focused characterization, content/common 회귀, `ktlintCheck`가 모두 성공해 production 변경 없이 처리 완료로 판정했다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.12` / `P3-R5`, `Task 3.13` / `P3-R6``P3-R4-GATE`를 추가한다. 기존 `Task 3.11``P3-R3-GATE` 완료 이력은 변경하지 않는다.
### 5차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | PRD multipart·ownership 계약과 production/test 대조 |
| 후보 항목 판정 완료 | 충족 | `REV-016`, `REV-017` 처리 완료 |
| 확정 항목 plan 반영 | 충족 | `Task 3.12`, `Task 3.13`, `P3-R4-GATE` |
| fresh 검증 | 충족 | 관련 79건과 lint 성공, diff check 이상 없음 |
**최종 결론:** `REV-016`, `REV-017` 처리 완료, Phase 3 Gate 완료
**남은 항목:** 없음. Phase 4는 사용자 진행 지시 전까지 시작하지 않는다.
### P3-R4-GATE 종료 판정
- 무엇을: `REV-016` exact multipart와 `REV-017` ownership/domain matrix 보강을 최종 판정했다.
- 왜: Phase 3 5차 리뷰를 종결하고 Phase 4를 대기 상태로 넘기기 위해서다.
- 어떻게: character mutation/error와 content create/controller/ownership focused 명령 및 `git diff --check`를 fresh 실행했다.
- 결과: focused 명령은 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-016` 처리 | 충족 | non-null multipart binding과 exact `MissingServletRequestPartException` KO/EN/JA test 추가 |
| `REV-017` 처리 | 충족 | cross-owner/domain actual endpoint KO/EN/JA no-side-effect matrix 추가 |
| focused 회귀 | 충족 | 5차 Gate focused 명령 성공 |
| 범위 준수 | 충족 | Phase 4 기능 구현 없음 |
**최종 결론:** Phase 3 5차 리뷰 종결
**남은 항목:** 없음. 다음은 사용자 지시 후 `P4-T1`이다.
## 14. 6차 재리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 실행 결과
- 기준 working tree: `P2-R4-GATE`, `P3-R4-GATE` 반영 후 staged/unstaged Phase 2·3 변경
- 리뷰 상태: 판정 완료, 추가 수정 goal 필요
- 검토 범위: 생성·수정 multipart empty-file 경계와 `REV-017`의 event no-interaction 실제 관찰 대상
- fresh 검증:
- Phase 2·3 5차 Gate focused 5개 test class를 `--rerun-tasks`로 실행해 XML 합계 216건, failure/error/skipped 0과 `BUILD SUCCESSFUL in 4m 16s`를 확인했다.
- `./gradlew ktlintCheck --rerun-tasks``BUILD SUCCESSFUL in 17s`, 7개 task 실행.
- staged/unstaged `git diff --check` — 출력 없음.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-019` | Medium | 확정 | 빈 multipart 파일이 생성·수정의 파일 계약을 우회함 | `Task 3.14` | `P3-R7` |
| `REV-020` | Low | 확정 | ownership/domain event 0회 assertion이 실제 service publisher를 관찰하지 않음 | `Task 3.15` | `P3-R8` |
### REV-019 — 빈 multipart 파일의 생성·수정 계약 우회
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** PRD Feature C, Endpoint Contract Summary의 생성 필수 파일·수정 `audioFile` 미지원 계약
- **관련 계약:** 생성의 `coverImage`·`audioFile`은 비어 있지 않은 필수 파일이며, 수정의 빈 `coverImage`는 생략으로
처리하고 `audioFile` part는 크기와 관계없이 거부한다.
- **소유 Task:** `Task 3.14`, `P3-R7`
**관찰 내용**
`REV-016`은 part 누락을 non-null MVC binding으로 막았지만 `MultipartFile.isEmpty`는 검증하지 않는다. 따라서 생성의 빈
cover/audio part는 legacy null 검사 뒤 0-byte S3 업로드를 시도할 수 있다. 수정은 빈 `coverImage`도 non-null인 경우 legacy
service가 업로드하고, 빈 `audioFile``audioFile?.isEmpty == false` 조건을 우회해 미지원 요청이 성공할 수 있다.
**근거**
- 코드: `AiCharacterAdminAudioContentController.kt:47`~`48`은 생성 파일의 존재만 non-null type으로 강제한다.
- 코드: `AudioContentService.kt:218`~`237`은 두 생성 파일을 null로만 검사하고 `:333`, `:362`에서 S3 upload에 전달한다.
- 코드: `AiCharacterAdminAudioContentFacade.kt:140`은 non-empty `audioFile`만 거부하고 `:148`에서 `coverImage`를 그대로
legacy update service에 전달한다.
- 코드: `CreatorAdminContentService.kt:118``coverImage != null`이면 크기와 관계없이 upload와 cover path 교체를 수행한다.
- 테스트: `AiCharacterAdminAudioContentUpdateTest.kt:145` 이후는 cover 생략과 non-empty 교체만,
`AiCharacterAdminAudioContentControllerTest.kt:691` 이후는 non-empty `audioFile` 거부만 확인한다.
**영향**
빈 파일 part를 생성하는 multipart client에서 생성 콘텐츠가 0-byte S3 객체를 참조하거나 수정 요청이 기존 cover를 빈 객체로
교체할 수 있다. 수정 `audioFile` 미지원 계약도 part 존재 여부가 아니라 파일 크기에 따라 달라진다.
**권장 조치**
`P3-R7`에서 v2 facade 경계만 수정한다. 생성의 빈 cover/audio는 400 `common.error.invalid_request`로 거부하고, 수정의 빈
cover는 `null`로 정규화해 기존 경로와 S3 상태를 유지한다. 수정 `audioFile`은 part가 존재하면 빈 파일도 400으로 거부한다.
legacy service의 공용 동작은 변경하지 않는다.
### REV-020 — event no-interaction의 실제 publisher 미관찰
- **심각도:** Low
- **상태:** 확정
- **관련 요구사항:** PRD Metrics 218~221, `P3-R6` NON-VACUOUS 완료 조건
- **관련 계약:** ownership/domain validation 실패는 실제 생성·수정 service의 event publisher를 호출하지 않는다.
- **소유 Task:** `Task 3.15`, `P3-R8`
**관찰 내용**
ownership/domain 테스트는 `@MockBean ApplicationEventPublisher``verifyNoInteractions`를 수행하지만, Phase 2에서 같은
`@MockBean`이 이미 생성된 service/facade field를 대체하지 못한다는 사실을 확인했다. Phase 3 event 실패 테스트는
`AudioContentService` proxy target의 publisher를 명시적으로 교체하지만 `P3-R6` 테스트에는 같은 연결이 없다.
**근거**
- 문서: `plan-task.md:1319``@MockBean`이 이미 생성된 facade field를 대체하지 못해 실제 listener가 실행됐음을 기록한다.
- 테스트: `AiCharacterAdminAudioContentControllerTest.kt:66`, `AiCharacterAdminAudioContentOwnershipTest.kt:53`
`ApplicationEventPublisher``@MockBean`으로 선언한다.
- 테스트: 두 파일의 `verifyNoInteractions`는 각각 `AiCharacterAdminAudioContentControllerTest.kt:944`,
`AiCharacterAdminAudioContentOwnershipTest.kt:194`에서 위 mock만 관찰한다.
- 대조: `AiCharacterAdminAudioContentCreateTest.kt:247`, `:355` 이후 event 실패 테스트는 실제
`AudioContentService` proxy target의 publisher를 교체한다.
**영향**
현재 production이 validation 실패에서 event를 발행한다는 증거는 없다. 다만 `REV-017` 처리 결과의 event 0회 assertion은 실제
생성·수정 service publisher를 측정하지 않아 `P3-R6`의 NON-VACUOUS 완료 증거로 사용할 수 없다.
**권장 조치**
`P3-R8`에서 Phase 2와 기존 create failure test의 proxy-target 교체 방식을 재사용해
`AudioContentService``CreatorAdminContentService`의 실제 publisher field를 같은 mock으로 교체하고 테스트 뒤 복원한다.
production code는 변경하지 않는다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.14` / `P3-R7`, `Task 3.15` / `P3-R8``P3-R5-GATE`를 추가한다. 기존
`Task 3.12`~`Task 3.13``P3-R4-GATE` 완료 이력은 변경하지 않는다.
### 6차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | multipart boundary·production service·actual endpoint test 대조 |
| 후보 항목 판정 완료 | 충족 | `REV-019`, `REV-020` 확정 |
| 확정 항목 plan 반영 | 충족 | `Task 3.14`, `Task 3.15`, `P3-R5-GATE` |
| fresh 검증 | 충족 | 관련 216건과 lint 성공, staged/unstaged diff check 이상 없음 |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P2-R5-GATE``P3-R7``P3-R8``P3-R5-GATE` 순서로 실행한다. Phase 4는 진행하지 않는다.
### P3-R5-GATE 종료 판정 — 2026-07-28
- 무엇을: `REV-019` empty-file 계약과 `REV-020` actual publisher 증거를 최종 판정했다.
- 왜: Phase 3 6차 리뷰를 종결하고 Phase 4를 대기 상태로 유지하기 위해서다.
- 어떻게: empty-file RED/GREEN focused, publisher focused, content/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
- 결과: empty-file RED는 신규 8건 실패로 재현됐고, reviewer gate 보완 뒤 empty create/update focused는 `BUILD SUCCESSFUL in 43s`, non-empty audio update 보완 focused는 `BUILD SUCCESSFUL in 44s`였다. publisher focused는 `BUILD SUCCESSFUL in 39s`, 최종 content/common 회귀는 `BUILD SUCCESSFUL in 2m 21s`, `ktlintCheck``BUILD SUCCESSFUL in 44s`, `git diff --check`는 출력이 없었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-019` 처리 | 충족 | 생성 empty cover/audio 400, 수정 empty cover 유지, audio part 존재 거부 actual endpoint test 추가 |
| `REV-020` 처리 | 충족 | `AudioContentService`, `CreatorAdminContentService` proxy target publisher identity/no-interaction 단언 |
| focused/영향 회귀 | 충족 | content/common 회귀와 lint 성공 |
| 범위 준수 | 충족 | legacy service 공용 계약과 Phase 4 기능 변경 없음 |
**최종 결론:** Phase 3 6차 리뷰 종결
**남은 항목:** 없음. 다음은 사용자 진행 지시 후 `P4-T1`이다.
## 15. 7차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 Phase 2~7 working tree
- 기준 문서: PRD Feature C, `plan-task.md`, `api-contract.openapi.json`
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
- 검증 방식: production/test 전체 호출 검색과 repository/DTO 정적 추적을 수행했다. 사용자 요청에 따라 Gradle,
컴파일, 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-022` | Low | 처리 완료 | 관리자 content repository에 호출되지 않는 확장 코드 잔존 | `Task 3.19` | `P3-R9` |
### REV-022 — 호출되지 않는 repository 확장과 전용 enum
- **심각도:** Low
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C
- **관련 계약:** 현재 5개 AudioContent operation의 실제 조회/생성/수정 경계
- **소유 Task:** `Task 3.19`, `P3-R9`
**관찰 내용**
현재 facade가 사용하는 관리자 전용 repository method는 owner-scoped 상세 조회
`findByIdAndCreatorMemberId` 하나다. 동일 repository의 page 조회, series ID 조회/교체, 활성 series 검사와 private helper는
production/test 호출자가 없고, `AiCharacterAdminAudioContentStatus`도 이 미사용 코드에서만 참조된다.
**근거**
- 코드: `AiCharacterAdminAudioContentRepository.kt:50`~`56`의 owner-scoped 상세 조회는 실제 facade 호출 대상이다.
- 코드: 같은 파일 `:25`~`:48`, `:58`~`:142`의 나머지 public/private method는 전체 호출 검색 결과 외부 참조가 없다.
- 코드: `AiCharacterAdminAudioContentDto.kt:28`~`31`의 status enum은 위 미사용 page query에서만 참조된다.
**권장 조치**
`P3-R9`에서 실제 사용 중인 상세 조회만 보존하고 호출 0건 method, helper, enum 및 그로 인해 unused가 된 import만 제거한다.
legacy repository나 콘텐츠/series 동작은 변경하지 않고 상세·ownership 회귀로 동작 불변을 확인한다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.19` / `P3-R9``P3-R9-GATE`를 추가했다. 이전 완료 Task/Gate는 다시 열지 않는다.
### 7차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 문서·구현 대조 | 충족 | AudioContent 5개 operation과 facade/repository 호출 정적 추적 |
| 후보 판정 | 충족 | `REV-022` 확정 |
| plan 반영 | 충족 | `Task 3.19`, `P3-R9`, `P3-R9-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 compile/test 미실행 |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P2-R6-GATE``P3-R9`를 실행하고 `P3-R9-GATE`에서 Phase 3을 재판정한다.
### P3-R9-GATE 종료 판정 — 2026-07-28
- 무엇을: `REV-022`의 관리자 오디오 repository 미사용 확장 제거를 최종 판정했다.
- 왜: 실제 facade 호출 대상은 owner-scoped 상세 조회 하나뿐이고, 나머지 page/series helper와 전용 status enum은 현재 5개 AudioContent operation에 쓰이지 않기 때문이다.
- 어떻게: `AiCharacterAdminAudioContentRepository`에서 상세 조회 외 method와 helper를 제거하고 `AiCharacterAdminAudioContentStatus`를 삭제했다. package-scoped 호출 검색, 상세·ownership focused, content/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
- 결과: 대상 package 호출 검색은 출력이 없었다. 상세·ownership focused는 `BUILD SUCCESSFUL in 3m 39s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck``BUILD SUCCESSFUL in 31s`, `git diff --check`는 출력 없음이었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-022` 처리 | 충족 | 미사용 repository method/helper/status enum 제거 |
| static 호출 검색 | 충족 | 대상 admin content package에서 제거 대상 호출 0건 |
| owner-scoped 상세 회귀 | 충족 | detail/ownership focused test 성공 |
| 영향 범위 회귀 | 충족 | content/common 회귀, lint, diff check 성공 |
| 범위 준수 | 충족 | legacy repository/service, 콘텐츠·시리즈 동작 변경 없음 |
**최종 결론:** Phase 3 7차 리뷰 종결
**남은 항목:** `P4-R1` 실행 후 `P4-R1-GATE`에서 Phase 4를 재판정한다.
## 16. 8차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C·API Expectations, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 생성 request strict parse와 legacy `AudioContentService` 호출, Java time 변환, prefix 예외 handler,
생성 actual endpoint 테스트
- 검증 방식: 요청값 → facade → legacy service → 예외 handler와 side-effect 순서를 정적으로 역추적했다.
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-030` | Medium | 처리 완료 | 잘못된 생성 날짜·시간대가 client 오류가 아닌 500으로 반환됨 | `Task 3.20` | `P3-R10` |
### REV-030 — 생성 날짜·시간대 의미 오류가 500으로 분류됨
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C, API Expectations의 request/domain 오류 400
- **관련 계약:** `AudioContentCreateRequest.releaseDate``yyyy-MM-dd HH:mm`, `timezone`은 시간대 ID
- **소유 Task:** `Task 3.20`, `P3-R10`
**관찰 내용**
facade의 strict reader는 JSON 문법·타입·미지 필드만 검증하고 파싱한 request를 버린다. 이후 legacy service가
`releaseDate``LocalDateTime`으로 변환하고 `ZoneId.of(timezone)`을 호출한다. 잘못된 값은
`DateTimeParseException`/`ZoneRulesException``DateTimeException`으로 빠지며, prefix handler는 이를 client 오류로
분류하지 않아 500 `common.error.unknown`을 반환한다.
**근거**
- 코드: `AiCharacterAdminAudioContentFacade.kt:65`~`:80`은 strict parse 뒤 같은 raw JSON을 legacy service에 전달한다.
- 코드: `AudioContentService.kt:226`~`:230`에서 날짜 형식과 `ZoneId`를 변환한다.
- 코드: `AiCharacterAdminExceptionHandler.kt:55`~`:71``DateTimeException`을 400 분기에 포함하지 않는다.
- 계약: OpenAPI `AudioContentCreateRequest``releaseDate` 설명과 기본 `timezone`.
- 계획: 기존 `Task 3.13`은 invalid date 400 증거를 완료 조건으로 적었지만 실제 테스트는 현재 계약 밖
`releaseDateUtc` 미지 필드만 검증한다.
- 테스트: 현재 생성 테스트에는 유효한 날짜·시간대와 제거된 `releaseDateUtc`만 있고 실제 `releaseDate` 형식·`timezone`
의미 오류가 없다.
**정적 재현 절차**
1. 유효한 `coverImage`, `contentFile`, theme과 필수 JSON field를 준비한다.
2. `releaseDate="not-a-date"` 또는 `timezone="Invalid/Zone"`으로 생성 요청을 보낸다.
3. strict JSON parse는 통과하고 legacy Java time 변환이 예외를 던진다.
4. 현재 handler 분류는 500이며 요구 결과는 side effect 없는 400 `common.error.invalid_request`다.
**영향**
형식상 JSON은 맞지만 의미가 잘못된 client 입력이 서버 장애로 기록·응답된다. 실제 업로드 이전에 실패하므로 현재 경로의
DB/S3/event 변경 가능성은 낮지만, 오류 계약과 운영 장애 지표가 왜곡된다.
**권장 조치**
공통 handler를 넓히지 말고 facade가 strict parse한 생성 DTO를 재사용해 날짜 형식과 `ZoneId`만 legacy 호출 전에 검증한다.
두 입력의 KO/EN/JA 400과 DB/S3/event 0건을 actual endpoint로 고정한다.
**판정 기록**
- 2026-07-28 — 코드·OpenAPI·handler·테스트 정적 추적으로 확정. 테스트는 사용자 요청에 따라 미실행.
- 2026-07-28 — `P3-R10`에서 `AiCharacterAdminAudioContentCreateTest``releaseDate="not-a-date"`
`timezone="Invalid/Zone"`의 KO/EN/JA actual endpoint matrix를 추가했다. RED는 신규 6건이 400 기대 assertion에서 실패했고,
facade가 strict parse 결과의 `releaseDate`/`timezone`을 legacy service 호출 전에 Java time API로 검증하도록 수정한 뒤
create focused, create+controller focused, content/common 영향 범위 회귀와 `ktlintCheck`가 모두 성공했다. production 변경은
v2 audio content facade 경계에 한정했고 OpenAPI schema와 legacy `AudioContentService`는 변경하지 않았다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.20` / `P3-R10``P3-R10-GATE`를 추가했다. 기존 완료 Task/Gate는 유지한다.
### 8차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema 대조 | 충족 | AudioContent 5개 mapping과 OpenAPI schema 유지 |
| 후보 판정 | 충족 | `REV-030` 원인·오류 분류·테스트 공백 확인 및 처리 완료 |
| plan 반영 | 충족 | `Task 3.20`, `P3-R10`, `P3-R10-GATE` |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** `REV-030` 처리 완료, Phase 3 8차 리뷰 종결
**남은 항목:** 없음. 다음은 `P4-R2` 실행 후 Phase 4를 재판정한다.
### P3-R10-GATE 종료 판정 — 2026-07-28
- 무엇을: 오디오 생성의 잘못된 `releaseDate` 형식과 `timezone` 의미 오류를 400 `common.error.invalid_request`로 복구했다.
- 왜: 형식상 JSON은 유효하지만 의미가 잘못된 client 입력이 legacy Java time 변환까지 내려가 500으로 반환되는 계약 위반을 막기 위해서다.
- 어떻게: v2 facade에서 strict parse 결과를 재사용해 `yyyy-MM-dd HH:mm``ZoneId`를 legacy service 호출 전에 검증했다.
- 결과: RED는 신규 6건 실패로 재현됐고, GREEN 후 create focused는 `BUILD SUCCESSFUL in 1m 4s`, create+controller focused는
`BUILD SUCCESSFUL in 1m 16s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`
`BUILD SUCCESSFUL in 38s`였다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-030` 처리 | 충족 | invalid `releaseDate`/`timezone` actual endpoint KO/EN/JA 400 추가 |
| side-effect 차단 | 충족 | DB count, S3 putObject 0회, event no-interaction 단언 |
| 영향 범위 회귀 | 충족 | content/common 회귀와 lint 성공 |
| 범위 준수 | 충족 | OpenAPI schema, legacy service, upload/processing pipeline 변경 없음 |
**최종 결론:** Phase 3 8차 리뷰 종결
**남은 항목:** 없음. 다음은 `P4-R2`다.
## 17. 9차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 상세 facade/mapper, 레거시 상세 response 파생 규칙, 예약일·locale·signed URL 테스트
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항
#### `REV-036` — High — 미래 예약 콘텐츠 상세의 releaseDate 소실
- OpenAPI 상세 response는 nullable `releaseDate`를 레거시 `GetAudioContentDetailResponse` 필드로 유지한다.
- 레거시 `AudioContentService`는 미래 예약일을 UTC에서 Asia/Seoul로 변환하고
`content.release_date.format`의 KO/EN/JA 형식 문자열을 반환하며, 공개 시각이 지나면 null을 반환한다.
- v2 `AiCharacterAdminAudioContentMapper.toResponse`는 콘텐츠 상태와 locale에 관계없이 `releaseDate = null`
고정한다.
- 현재 상세 테스트의 예약일은 점검일보다 과거라 null 분기만 검증해 미래 분기 누락을 발견하지 못한다.
**영향:** 예약 공개 전 관리자 상세에서 공개 예정 시각이 숨겨지고 레거시 response 의미와 OpenAPI 이관 원칙을 위반한다.
**권장 조치:** 기존 `SodaMessageSource``LangContext`를 사용해 미래 여부, UTC→Asia/Seoul 변환, locale별 포맷을
mapper에 최소 이관하고 미래·과거 KO/EN/JA actual endpoint를 고정한다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.21` / `P3-R11``P3-R11-GATE`를 추가했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | route 유지 | AudioContent 5개 mapping과 response 필드는 존재 |
| 미래 예약일 | 수정 필요 | mapper가 `releaseDate`를 무조건 null로 설정 |
| 과거 예약일 | 충족 | null 반환은 레거시 의미와 일치 |
| plan 반영 | 충족 | `Task 3.21`, `P3-R11`, `P3-R11-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** Phase 3 후속 수정 필요
**남은 항목:** `P2-R7-GATE``P3-R11``P3-R11-GATE`.
## 18. 9차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-036`을 처리했다.
- 왜: 미래 예약 콘텐츠 상세의 `releaseDate`가 항상 null이라 레거시 locale별 공개 예정 시각을 숨겼기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentQueryTest`에 미래 예약일 KO/EN/JA와 과거 null actual endpoint 테스트를 추가했다. focused 실행에서 미래 3개 locale이 null 반환으로 실패했다.
- GREEN: `AiCharacterAdminAudioContentMapper``SodaMessageSource`, `LangContext`를 사용해 레거시 `content.release_date.format`과 UTC→Asia/Seoul 변환을 적용하도록 최소 수정했다.
- 검증: focused query test, targeted aicharacter 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
- 결과: `REV-036` 처리 완료. 미래 예약일은 KO/EN/JA 형식 문자열로 반환하고 과거 예약일은 null을 유지한다.
**최종 결론:** Phase 3 9차 리뷰 종결
**남은 항목:** 없음.
## 19. 10차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 테마·목록·상세·생성·수정 facade/mapper/repository, signed URL과 최신 예약일 보완
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
AudioContent runtime의 신규 확정 finding은 없다. owner-scoped 상세, 레거시 목록/생성/수정 DTO,
빈 파일 경계, signed URL과 미래·과거 KO/EN/JA 예약일 의미가 유지된다.
`Task 3.21` 헤더가 `[ ]`로 남은 문제는 Phase 7 `REV-038` / `Task 7.6`에서 완료 상태로 동기화했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | AudioContent 5개 route와 OpenAPI field·multipart 경계 일치 |
| ownership | 충족 | target owner content만 상세·수정 가능 |
| signed URL/private path | 충족 | 공통 CloudFront 정책 재사용과 private path 비노출 |
| 예약 공개일 | 충족 | 미래 locale별 표시, 과거 null 유지 |
| 문서 상태 | 충족 | `REV-038`, `P7-R4`에서 완료 헤더 동기화 |
**최종 결론:** Phase 3 기능 추가 수정 없음
**남은 항목:** 없음.
## 20. 11차 정적 리뷰 및 판정 — 2026-07-29
### 확인된 문제
#### `REV-041` — 오디오 생성 primitive의 required·null 계약 미강제
- **심각도:** High
- **상태:** 처리 완료
- **계약:** OpenAPI `AudioContentCreateRequest``price`를 required non-null integer로 정의하고,
`themeId`, 각 boolean primitive도 nullable로 선언하지 않는다.
- **구현:** `CreateAudioContentRequest`의 해당 값은 Kotlin primitive이며, v2 content facade의 strict reader는
`FAIL_ON_UNKNOWN_PROPERTIES`, `FAIL_ON_NULL_FOR_PRIMITIVES`, `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 활성화한 뒤 원본 JSON을 legacy service로 전달한다.
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 primitive 누락·null은 `0`/`false`로 보정될 수 있다.
- **영향:** 필수 `price` 누락·null이 400 없이 생성으로 이어질 수 있고, `isFullDetailVisible` 같은 필드의 explicit null은
문서·DTO의 생략 기본값과 다른 값으로 처리될 수 있다.
### 보완 계획
| 항목 | 판정 |
|---|---|
| 신규 Task | `Task 3.22` / `P3-R12` |
| 시작 조건 | `P2-R8-GATE` |
| Gate | `P3-R12-GATE` |
| RED | required `price` 누락·null, non-null primitive null과 S3·DB·event 무변경 |
| GREEN | v2 생성 경계의 primitive null/누락 400 변환과 optional 생략 기본값 유지 완료 |
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
### 처리 결과
- `AiCharacterAdminAudioContentCreateTest``price` 누락·null, primitive field explicit null actual POST와 파일 업로드·DB·event no-side-effect 회귀를 추가했다.
- `AiCharacterAdminAudioContentFacade.readRequest()``FAIL_ON_NULL_FOR_PRIMITIVES``FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가해 전역 mapper·레거시 service·OpenAPI 변경 없이 v2 경계에서 400으로 변환했다.
- RED: 신규 8개 invocation은 보완 전 `status().isBadRequest` 기대에서 실패했다. `themeId:null`은 기존 missing-theme guard로 이미 400이었다.
- GREEN/GATE: 보완 후 focused, content/common 영향 범위, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
**최종 결론:** Phase 3는 `REV-041` 처리 완료
**다음 Goal:** `P4-R4`.
## 21. 오디오 콘텐츠 댓글 후속 검토 — 2026-07-29
### 확인 결과
- **`REV-045` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글
조회/작성/수정/삭제 5개 operation이 없다.
- 조회는 필수 `timezone``page`, `size`, 레거시 `totalCount/items`를 유지한다.
- 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 콘텐츠의 row를
작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다.
- 답글 `parentId`는 같은 콘텐츠의 활성 원댓글이어야 한다.
### plan 전환
- 신규 Task: `Task 3.23` / `P3-R13`
- Gate: `P3-R13-GATE`
- 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 CRUD 구현 필요
**다음 Goal:** `P3-R13`.
## 22. 오디오 콘텐츠 댓글 구현 및 Gate — 2026-07-29
- 무엇을: `REV-045`를 처리했다.
- 왜: 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글 조회/작성/수정/삭제 5개 operation이 없었기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentCommentTest`에 root/reply 조회, target AI 작성, target 작성 row 수정, owner 범위 row soft delete, 잘못된 parent/timezone/page/size/unknown field/cross-resource 계약 7건을 추가했고 미구현 route의 404/405로 실패했다.
- GREEN: `AiCharacterAdminAudioContentController`에 5개 route를 추가하고, facade에서 target active owner content, 같은 콘텐츠의 활성 root parent, target 작성 수정 권한을 선검증한 뒤 기존 `AudioContentCommentService`를 재사용했다.
- Gate: focused 댓글 테스트, content/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 결과: `REV-045` 처리 완료. 삭제는 작성자와 관계없이 target 소유 콘텐츠의 해당 row만 soft delete하고 cascade하지 않으며, 이미 비활성인 row는 200 no-op을 유지한다.
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 후속 기능 종결
**다음 Goal:** `P4-R5`.
## 23. UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI 2.2.0 AudioContent 10개 operation, `DEC-UTC-DATE-001`
- 검토 범위: 오디오 생성 request, 상세 GET, 댓글·답글 GET의 controller/facade/DTO/mapper/repository
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-050` — High — 오디오 4개 operation의 timezone/UTC 계약 불일치
- 생성은 현재 레거시 `CreateAudioContentRequest`를 typealias로 사용해 `timezone` body와
`yyyy-MM-dd HH:mm` 로컬 `releaseDate`를 받는다.
- 상세 GET은 필수 `timezone` query를 받지만 facade에서 사용하지 않는다. 상세 `releaseDate`는 미래 예약일에
UTC를 Asia/Seoul로 바꾼 locale 문자열이며 현재/과거는 null이다.
- 댓글·답글 GET도 필수 `timezone` query를 받고 레거시 목록 service/repository의 표시 문자열을 반환한다.
- 승인된 최신 계약은 생성 body와 세 GET에서 `timezone`을 제거하고, 생성의 nullable `releaseDate`, 상세의
기존 nullable `releaseDate`, 댓글의 기존 `date`를 ISO-8601 UTC(`Z`)로 사용한다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| route 수 | 유지 | AudioContent 10개 operation 자체는 변경 없음 |
| 생성 request | 처리 완료 | v2 전용 DTO가 `timezone`을 거부하고 UTC `releaseDate`만 내부 경계에 전달 |
| 상세 response | 처리 완료 | query를 제거하고 미래 예약일만 UTC `Z`로 반환 |
| 댓글·답글 response | 처리 완료 | `page`/`size`만 받고 기존 `date`를 UTC `Z`로 mapping |
| legacy/public 격리 | 충족 | 기존 controller/service의 timezone 계약을 유지 |
| OpenAPI 상태 | 처리 완료 | 영향 4개 operation을 `implemented`로 동기화 |
### plan·goal 전환
- 신규 Task: `Task 3.24` / `P3-R14`
- Gate: `P3-R14-GATE`
- 완료 조건: 생성·상세·댓글·답글 actual endpoint UTC exact JSON, 상세 기존 null/노출 조건과 댓글
pagination/ownership 보존, legacy/public 회귀
- 범위 밖: 오디오 목록 날짜, 로컬 시각+timezone 병행 지원, 신규 dependency·DDL
### `P3-R14` / `P3-R14-GATE` 처리 결과
- RED: create/query/comment focused actual endpoint 테스트는 48개 중 9개가 기존 `timezone` 필수와 legacy 날짜 포맷으로
실패했다.
- GREEN: v2 생성 DTO가 `timezone`을 미지 필드로 거부하고 UTC instant를 `LocalDateTime`으로 한 번 변환해 내부 생성
overload로 전달한다. 상세와 root/reply 목록은 기존 null·pagination·ownership·filter 의미를 유지하면서 `releaseDate`
`date``toUtcIso()`로 반환한다.
- Gate: focused는 `BUILD SUCCESSFUL in 2m 13s`, parent 재실행은 `BUILD SUCCESSFUL in 52s`, content/common·legacy 영향
범위 회귀는 `BUILD SUCCESSFUL in 1m 35s`, `ktlintCheck``BUILD SUCCESSFUL in 23s`였다. 당시 OpenAPI는 34개
`implemented`, 2개 `alignment-required`였고, 후속 `P5-R6` 뒤 36개 모두 `implemented`로 통합됐다. `git diff --check`
출력이 없었다.
**최종 결론:** `REV-050` 처리 완료, Phase 3 UTC 계약 정합화 완료
**다음 Goal:** `P5-R6`.
## 24. 12차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation과 공통 `Page`/`Size`
- 검토 범위: audio controller/facade의 query binding·검증, 관련 댓글 actual endpoint 테스트
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### `REV-052` — High — 오디오 댓글·답글 목록의 optional pagination 계약 위반
- OpenAPI의 `Page`, `Size``required: false`이고 각각 기본값 `0`, `20`이다.
- 댓글 목록과 답글 목록 controller의 `page`, `size`에는 `defaultValue`가 없어 두 query를 생략하면 MVC binding
단계에서 400이 된다.
- facade의 `validateCommentQuery()`도 실제 query 이름 집합이 정확히 `page`, `size` 두 개일 때만 허용하므로,
controller 기본값만 추가해도 전체 또는 부분 생략 요청을 거부한다.
- 현재 댓글 테스트는 query 전체 생략을 400으로 기대해 계약 불일치를 회귀로 고정하고 있다.
- 영향은 두 GET의 정상 요청 가용성에 직접 미치므로 High로 판정한다.
### 검증 근거
| 근거 | 확인 내용 |
|---|---|
| OpenAPI | 두 GET이 공통 optional `Page`/`Size`를 참조 |
| controller | 댓글·답글 모두 기본값 없는 `@RequestParam page`, `size` |
| facade | query 이름의 부분집합이 아니라 정확한 집합 일치 요구 |
| test | query 전체 생략 요청을 400으로 기대 |
### plan 전환
- 신규 Task: `Task 3.25` / `P3-R15`
- Gate: `P3-R15-GATE`
- 최소 수정: 두 controller query 기본값과 facade의 미지 query 거부 조건만 정합화
- 완료 조건: 전체·부분 생략 200/default, 범위 오류·미지 query 400, 기존 UTC/ownership/pagination 회귀
- 범위 밖: OpenAPI·legacy/public API·FanTalk query policy 변경
### `P3-R15` / `P3-R15-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminAudioContentCommentTest` 9건 중 댓글·답글 전체 생략 테스트 2건이 `isOk` 기대에서 실패해 `BUILD FAILED in 45s`였다.
- GREEN: 두 controller 목록의 `page`, `size`에 각각 `0`, `20` 기본값을 적용하고, facade는 `page`, `size`의 부분집합만 허용해 미지 query·음수 page·1 미만 size의 기존 400 `ApiResponse.error` 경계를 유지했다.
- Gate: focused는 `BUILD SUCCESSFUL in 40s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck``BUILD SUCCESSFUL in 37s`였다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 controller/facade와 actual endpoint 변경을 포함하므로 실행하지 않았다.
**현재 결론:** `REV-052` 처리 완료. Phase 3 완료 판정에는 별도 `REV-056` / `P3-R16` 보완이 남아 있다.
### `REV-056` — High — AudioContent multipart request part의 JSON media type 미강제
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
- 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다.
- 실제 `AiCharacterAdminAudioContentControllerTest`와 update 테스트는 유효 JSON을 `text/plain` request part로
보내 200을 기대하므로 계약 불일치가 실행 테스트에도 고정돼 있다.
- 기존 정상 strict parsing·file/series/UTC 의미를 유지하면서 part-level media type만 415로 차단해야 한다.
### 추가 plan 전환
- 신규 Task: `Task 3.26` / `P3-R16`
- Gate: `P3-R16-GATE`
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
S3/DB/processing/event no-side-effect
**최종 결론:** Phase 3은 `REV-052`, `REV-056` 수정 전 완료 판정 불가
**다음 Goal:** `P3-R15` (`P2-R10-GATE` 완료 후).
### `P3-R16` / `P3-R16-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentControllerTest`의 83건 중 text/plain·Content-Type 누락 415 기대 13건이 기존 200으로 실패해 `BUILD FAILED in 1m 15s`였다.
- GREEN: Character `P2-R10``MultipartHttpServletRequest` header 검사 패턴을 AudioContent POST·PUT controller에만 적용했다. 기존 JSON String strict reader와 facade는 변경하지 않았다.
- Gate: POST·PUT actual endpoint는 KO/EN/JA의 text/plain·Content-Type 누락에 localized `ApiResponse.error` 415와 `Accept: application/json`, DB/S3/event 무변경을 확인했다. JSON 정상 경로, 생성 필수 part 400, UTC/file/series 회귀도 유지했다. focused는 `BUILD SUCCESSFUL in 56s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck``BUILD SUCCESSFUL in 16s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 변경 slice를 포함하므로 실행하지 않았다.
**처리 결과:** `REV-056` 처리 완료.
## 25. 13차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI `AudioContentCreateMultipart`·`AudioContentUpdateMultipart`
- 검토 범위: AudioContent POST·PUT controller의 multipart binding, 파일 교체 거부와 관련 mutation 테스트
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### `REV-061` — Medium — operation별 허용 목록 밖 multipart part를 일반적으로 거부하지 않음
- OpenAPI는 생성에 `contentFile`, `coverImage`, `request`, 수정에 `coverImage`, `request`만 정의하고 두 schema 모두
`additionalProperties: false`다.
- controller는 전체 part 이름을 검사하지 않는다. 수정의 `audioFile`, `contentFile`만 별도 nullable 인자로 받아 facade에서
거부하므로, `unexpected` 같은 다른 이름의 part는 무시된다.
- 생성·수정 모두 정의되지 않은 part를 포함한 요청이 정상 mutation으로 진행될 수 있어 계약 위반을 확정했다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.27` / `P3-R17` |
| Gate | `P3-R17-GATE` |
| RED | 생성·수정 미정의 part와 S3·DB·processing/event 결과 |
| GREEN | 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` exact allow-list |
| 회귀 | 기존 `audioFile`·`contentFile` 수정 거부, 필수/빈 파일, request part 415 |
**처리 결과 (2026-07-29 / P3-R17):**
- AudioContent POST는 multipart part 이름을 `{contentFile, coverImage, request}`로 제한하고, PUT은 `{coverImage, request}`로 제한하도록 controller 경계에 allow-list를 추가했다.
- PUT controller/facade의 `audioFile`, `contentFile` nullable 인자는 제거했고, 기존 파일 교체 거부는 동일한 미정의 part 검증 경계로 통합했다.
- RED에서 생성·수정 `unexpected` part KO/EN/JA 테스트 6개가 기존 정상 mutation 경로로 실패함을 확인했고, GREEN 후 focused/영향 범위 회귀, `ktlintCheck`, `git diff --check`를 통과했다.
**Gate 결과 (2026-07-29 / P3-R17-GATE):**
- Focused multipart 회귀, Phase 3 content 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 생성·수정 미정의 part 400/no-side-effect, 수정 `audioFile`·`contentFile` 교체 미지원, 정상/필수/빈 파일/request 415 경계가 유지됨을 확인했다.
**최종 결론:** `REV-061` resolved. Phase 3 완료.
**다음 Goal:** `P4-R8`.
## 26. 14차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: OpenAPI AudioContent create/update multipart schema의 operation별 허용 part와
`additionalProperties: false`
- 검토 범위: AudioContent POST·PUT controller의 allow-list와 미정의 part·파일 교체 회귀 테스트
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-066` — Medium — 일반 form-field multipart part가 allow-list 우회
- `AiCharacterAdminAudioContentController.kt:81-88`은 operation별 허용 집합을 받지만 실제 검사는
`fileMap.keys`에 한정한다.
- 기존 create/update 회귀는 각각 `AiCharacterAdminAudioContentCreateTest.kt:251-266`,
`AiCharacterAdminAudioContentUpdateTest.kt:145-159`의 filename이 있는 `MockMultipartFile`만 사용한다.
- filename 없는 일반 form-field `unexpected`는 생성 `{contentFile, coverImage, request}`, 수정
`{coverImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.28` / `P3-R18` |
| Gate | `P3-R18-GATE` |
| RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·processing·event no-side-effect |
| GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 |
| 범위 제한 | 파일 교체 의미·OpenAPI·전역 resolver·legacy/public 변경 없음 |
**최종 결론:** Phase 3 보완 필요 — `REV-066` 확정
**다음 Goal:** `P3-R18` (`P2-R12-GATE` 완료 후).
## 25. 10차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-066`의 AudioContent POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
- 왜: 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 거부되지 않았기 때문이다.
- 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다.
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
**최종 결론:** `REV-066` 처리 완료. Phase 3 후속 Gate 완료.
**남은 항목:** 없음.
## 26. 15차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature D의 기존 creator 동작 재사용, OpenAPI 오디오 생성 preview 필드,
plan의 기존 preview 오류 key 유지 조건
- 검토 범위: `AiCharacterAdminAudioContentFacade.create`, `AudioContentService.createAudioContent` 두 overload,
v2 오디오 생성 actual endpoint 테스트
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 교차 대조했다. 사용자 지시에 따라
컴파일과 테스트는 실행하지 않았다.
### `REV-072` — High — v2 오디오 생성이 기존 preview 시간 검증을 우회
- v2 facade는 strict JSON parse와 UTC `releaseDate` 변환 후 `CreateAudioContentRequest`와 파싱된 날짜를 받는
`AudioContentService.createAudioContent` overload를 호출한다.
- 기존 `previewStartTime`·`previewEndTime`의 쌍, `HH:mm:ss` 형식, 최소 15초 검증 호출은 문자열 request를 받는
legacy overload에만 있다. v2가 호출하는 공유 대상 overload에는 검증 호출이 없다.
- 해당 overload는 검증 없이 DB를 저장하고 cover/audio를 S3에 업로드한 뒤 두 값이 모두 있으면 그대로 metadata에
넣고 event를 발행한다. 따라서 한쪽만 있는 값은 조용히 무시되고, 형식 오류·15초 미만 값은 metadata로 전달될 수 있다.
- v2 테스트에는 정상 `00:00:05`~`00:00:25` 입력만 있고 세 거부 규칙의 actual endpoint 회귀가 없다.
- 기존 creator 검증/parity와 부작용 선검증을 깨뜨리므로 High로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.29` / `P3-R19` |
| Gate | `P3-R19-GATE` |
| RED | 시작만 입력, 형식 오류, 15초 미만의 400 및 DB/S3/event no-side-effect |
| GREEN | 기존 검증 호출을 두 경로가 공유하는 parsed request overload로 이동 |
| 회귀 | KO/EN/JA 기존 오류 key, 정상 preview metadata, legacy/public 생성 |
### `REV-072` 처리 결과 — 2026-07-30
- `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)` 시작부로 `validatePreviewTime` 호출을 이동해 legacy
string request 생성과 v2 parsed request 생성이 같은 preview 검증을 정확히 한 번 공유한다.
- v2 actual endpoint에 한쪽만 입력, 형식 오류, 15초 미만 preview의 KO/EN/JA 400 응답과 DB/S3/event no-side-effect를
추가했고, 정상 15초 이상 preview는 audio upload metadata의 `preview_start_time`·`preview_end_time` 보존을 확인했다.
- 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest`
`BUILD SUCCESSFUL in 39s`; Phase 3 content와 legacy AudioContent 영향 범위 회귀 → `BUILD SUCCESSFUL in 1m 22s`;
`./gradlew ktlintCheck``BUILD SUCCESSFUL in 32s`; `git diff --check` → 출력 없음.
**최종 결론:** `REV-072` 처리 완료. Phase 3 완료.
**다음 Goal:** `P7-R11` 통합 재판정 완료.
## 27. 16차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation
- 검토 범위: 테마·목록·상세·생성·수정, signed URL·UTC 날짜, 댓글 CRUD, owner/actor/parent와 multipart 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- AudioContent 10개 operation과 controller mapping, owner-scoped 조회·수정 및 active 콘텐츠 댓글 경계가 일치한다.
- 생성의 UTC `releaseDate`, 필수 파일, strict request, preview 쌍·형식·최소 15초 검증이 공유 service 경계에 유지된다.
- 댓글 작성의 동일 콘텐츠 활성 원댓글, target AI 수정 권한, row-only soft delete와 UTC 응답 계약이 유지된다.
- 신규 확정 finding이 없어 Phase 3 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 3 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.