Files

362 lines
23 KiB
Markdown

# Phase 4 Audio Content 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 4 / Audio 목록·상세·생성·수정·soft delete·player |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 판정 완료, 회귀 수정 완료 및 외부 수동 QA 대기 |
## 2. 리뷰 목적과 범위
### 목적
- Audio multipart DTO, 예약 UTC, 파일 정책, CRUD와 soft delete 불변식을 검증한다.
- schema·API adapter·contract test가 동일한 request 경계를 강제하는지 확인한다.
### 포함 범위
- 코드: `src/features/audio-contents`
- 테스트: Audio unit/contract/UI/E2E
- 문서: `AUDIO-001`~`AUDIO-033`, 관련 OpenAPI operation, `P4-*`
- 수동 검증: update/deactivate payload schema 정적 대조
### 제외 범위
- 실제 대용량 upload와 외부 CDN 재생 품질
- Comments 동작은 Phase 8에서 판정
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 데이터 mutation 계약 위반과 경계 test의 잘못된 허용을 우선한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `AUDIO-006`, `AUDIO-007`, `AUDIO-012`, `AUDIO-019`, `AUDIO-023`~`AUDIO-033`
- 계약: Audio POST/PUT multipart request schema
- 계획: `P4-T1`~`P4-GATE`
- 코드: `src/features/audio-contents/schemas/audio-content-schema.ts:35-44`
- 테스트: `src/features/audio-contents/tests/audio-contract.test.ts:159-202`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Vitest + Playwright 4 projects
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run e2e:mock` | 부분 실패 | Audio E2E 흐름은 통과; 전체 origin 실패는 `REV-P0-004` |
| `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 |
| Audio update schema·test 정적 대조 | 실패 | 일반 update에서 nullable boolean `isActive`, test는 `isActive:true`를 허용·기대 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-007` | Medium | 수정 완료 | 일반 Audio update schema와 contract test가 `isActive=true`를 허용한다 | `P4-R4` | `P4-R4` |
## 6. 발견 사항 상세
### REV-P4-007 — 일반 Audio update schema와 contract test가 `isActive=true`를 허용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-007`
- **관련 계약:** Audio PUT request의 optional `isActive`를 제품 정책상 soft delete로 제한
- **소유 Task:** 신규 `P4-R4`
**관찰 내용**
`audioContentUpdateRequestSchema``isActive`가 nullable boolean이라 `true`, `false`, `null`을 모두 허용한다. contract test는 일반 수정 request에 `isActive:true`를 넣고 그대로 전송되는 것을 성공 조건으로 고정한다. PRD는 일반 수정에서는 필드를 생략하고 soft delete 전용 request에서만 `false`를 보내며 `true`는 전송하지 않도록 명시한다.
**근거**
- 코드: `src/features/audio-contents/schemas/audio-content-schema.ts:35-44`
- 테스트: `src/features/audio-contents/tests/audio-contract.test.ts:159-202`
- 문서: `prd.md:225``AUDIO-007`
- 계약 해석: OpenAPI의 optional boolean 범위보다 제품 mutation 정책이 더 좁으며 별도 deactivate adapter가 이미 존재
**재현 또는 검증 절차**
1. `audioContentUpdateRequestSchema.parse({ isActive: true })`를 호출한다.
2. parsing이 성공하는 것을 확인한다.
3. 기존 contract test가 일반 update body의 `isActive:true`를 기대하는 것을 확인한다.
4. 요구 결과는 일반 update에서 `isActive` 자체를 거부하고 deactivate request만 `{ isActive:false }`를 허용하는 것이다.
**영향**
후속 UI·adapter 변경이 활성 복원 request를 잘못 보내도 schema와 contract test가 차단하지 못해 확정된 soft-delete-only 정책이 회귀할 수 있다.
**권장 조치**
일반 update schema에서 `isActive`를 제거하고 `{isActive:false}` strict schema를 deactivate 전용으로 분리한다. `true`, `null`, unknown field 거부와 두 payload의 정확한 직렬화를 test한다.
**판정 기록**
- 2026-07-30 — PRD, schema, adapter contract test의 상반된 허용 범위를 대조해 확정.
- 2026-07-30 — `P4-R4`에서 normal update schema/API/mock parser와 deactivate schema/API/mock parser를 분리해 수정 완료. 일반 update는 `isActive=true/null/false`를 거부하고 deactivate만 `{ isActive:false }`를 허용한다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P4-007``plan-task.md` 신규 `P4-R4`
- goal objective: `[P4-R4] Audio 일반 update와 soft delete schema를 분리해 isActive 전송 정책을 강제한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Audio code·test·contract 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P4-R4` |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 수정 완료
**남은 항목:** Audio 실제 server integration은 기존 Gate 정책에 따라 mock UI 증거와 분리해 추적한다.
## 9. 수정 후 검증 기록
- 2026-07-30 — RED: `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts`에서 normal update의 `isActive:false`가 거부되지 않아 실패하는 것을 확인했다.
- 2026-07-30 — GREEN/focused: `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts` 결과 1 file / 10 tests passed.
- 2026-07-30 — 회귀: `npm run test:run -- src/features/audio-contents src/shared/mocks` 결과 14 files / 67 tests passed. 첫 병렬 실행에서 `audio-list.test.tsx`가 loading 상태로 timeout됐으나 단독 재현은 2 passed였고, 같은 전체 focused 명령 재실행은 14 files / 67 tests passed로 통과했다.
- 2026-07-30 — E2E/static: `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 결과 21 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다.
- 2026-07-30 — LSP/size: `audio-content-schema.ts`, `audio-content-api.ts`, `audio-content-mock-store.ts`, `audio-content-handlers.ts`, `handlers.ts`, `audio-contract.test.ts` diagnostics는 오류 0건이었다. Audio handler 추출 후 `src/shared/mocks/handlers.ts`는 238 lines, `src/shared/mocks/audio-content-handlers.ts`는 133 lines다.
## 10. 2차 점검 결과 — 2026-07-30
- Phase 4 request/schema 자체의 신규 계약 위반은 없고 `REV-P4-007` 수정 완료 상태가 유지된다.
- Audio 비활성화 pending·실패 복구의 공통 결함은 중복 등록하지 않고 `REV-P9-004`/`P9-R4`에서 추적한다.
## 11. 2026-07-31 재점검
### 실행·판정 요약
- **범위:** Audio 목록 URL·API query, create/edit cover crop lifecycle, upload·deactivate를 PRD `AUDIO-001~033`, `FILE-006~009`와 OpenAPI 2.3.0에 재대조했다.
- **검증:** 전체 unit 72 files / 360 tests, server allowlist 36 tests, typecheck·lint·build가 통과했다.
- **신규 발견:** Medium 2건. 기존 `REV-P4-007`은 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-008` | Medium | 수정 완료 | Audio URL이 계약 query `search_word` 대신 `search`를 사용한다 | `P4-R5` | `P4-R5` |
| `REV-P4-009` | Medium | 수정 완료 | Audio cover crop 준비·연속 선택 상태가 저장 경계에 없다 | `P4-R6` | `P4-R6` |
### REV-P4-008 — Audio URL이 계약 query `search_word` 대신 `search`를 사용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §7.1
- **관련 계약:** Audio 목록 query `search_word`, `page`, `size`
- **소유 Task:** 신규 `P4-R5`
**관찰 내용과 근거**
브라우저 URL은 `search`를 읽고 쓰고 API 호출 직전에만 `search_word`로 이름을 바꾼다. `?search_word=루나` 직접 링크는 UI 검색 상태로 복원되지 않는다.
- 코드: `src/features/audio-contents/pages/AudioContentListPage.tsx:25-43,55-58`
- 테스트: `src/features/audio-contents/tests/audio-list.test.tsx:87-128``?search=루`를 정상 URL로 사용한다.
- 문서: `prd.md:147-150`
**재현 또는 검증 절차**
1. `/ai-characters/101/audio-contents?search_word=루나&page=1&size=20`에 직접 진입한다.
2. 검색 input과 request가 URL 검색어를 복원하지 않는지 확인한다.
3. UI 검색 뒤 URL이 `search=...`가 되는지 확인한다.
**영향 및 권장 수정 방향**
계약 이름으로 공유한 deep link가 무시되고 URL·API 추적성이 달라진다. URL parser/serializer key만 `search_word`로 맞추고 직접 진입·검색·pagination 왕복 test를 추가한다.
**판정 기록:**
- 2026-07-31 — PRD·OpenAPI·page/test 대조로 확정.
- 2026-07-31 — `P4-R5`에서 Audio list URL read/write key를 `search_word`로 교체했다. `?search_word=루나&page=1&size=20` 직접 진입은 input과 API request를 복원하고, 검색 변경 URL에는 `search`가 남지 않는다.
### REV-P4-009 — Audio cover crop 준비·연속 선택 상태가 저장 경계에 없다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-006~009`, PRD §10.5
- **소유 Task:** 신규 `P4-R6`
**관찰 내용과 근거**
`selectCoverImage``createCropSource`를 바로 await하지만 준비 상태, selection token, reject 오류를 관리하지 않는다. submit button은 audio upload 중에만 비활성화되며 source 준비 중에는 저장할 수 있다. create는 필수 cover validation으로 일부 차단되지만 edit는 새 선택 의도와 무관하게 기존 cover 유지 payload를 먼저 저장할 수 있고, 연속 선택은 느린 이전 Promise가 최신 dialog를 덮을 수 있다.
- 코드: `src/features/audio-contents/components/AudioContentForm.tsx:65-96,116-126,233-244`
- 문서: `prd.md:353-359,524-538`
- 비교 근거: Community form/sheet는 selection ID와 `isImagePreparing`을 사용해 동일 경계를 처리한다.
**재현 또는 검증 절차**
1. edit form에서 pending `createCropSource`를 주입하고 새 cover를 선택한다.
2. source가 resolve되기 전에 저장하면 새 cover 선택이 확정되지 않은 채 update가 진행된다.
3. 느린 첫 파일과 빠른 둘째 파일을 선택해 첫 Promise를 나중에 resolve하면 오래된 dialog가 열릴 수 있다.
4. Promise reject 시 사용자 오류 없이 unhandled rejection이 될 수 있다.
**영향 및 권장 수정 방향**
운영자가 선택한 새 cover가 누락되거나 이전 선택 dialog가 열려 잘못된 파일을 적용할 수 있다. committed crop result와 준비 상태를 분리하고 마지막 선택만 허용하며 준비/crop 중 submit을 막고 오류를 표시한다.
**판정 기록:**
- 2026-07-31 — 비동기 state 전이와 파일 정책을 대조해 확정.
- 2026-07-31 — `P4-R6`에서 Audio cover source 준비 상태와 selection token을 추가하고 준비/crop 중 submit을 차단했다. Promise reject는 inline 오류로 표시하고, crop 적용 결과만 `coverImage`에 commit하며 수정 취소는 기존 서버 cover 유지 계약을 보존한다.
### plan·goal 전환 및 종료 판정
- `REV-P4-008``P4-R5`
- `REV-P4-009``P4-R6`
- **최종 결론:** 신규 Medium 2건 수정 완료. Blocker/High 없음.
- **교차 QA:** 당시 Mobile Safari Audio 접근성 시나리오의 로그인 input 유실은 focused `--repeat-each=5`에서 5 passed였고, 이후 지원 project 축소로 현재 Task에서 제외했다.
## 12. 최종 Phase별 점검 — 2026-07-31
### 실행 결과
| 명령 또는 검증 | 결과 | 판정 |
|---|---|---|
| `npm run test:run -- src/features/audio-contents` | 8 files / 53 tests passed | 성공 |
| `npm run e2e:mock -- --project=chromium` | 전체 57 tests passed, Audio 6 scenarios 포함 | 성공 |
| `npm run typecheck` / `npm run lint` / `npm run build` | 모두 exit 0 | 성공 |
| PRD file flow와 Audio cover selection 정적 대조 | crop 전 원본 validator 호출 없음 | 실패, 신규 finding 확정 |
병렬로 네 개 Vitest process를 실행한 첫 Audio focused run에서는 `audio-list.test.tsx` 1건이 로딩 대기에서 timeout됐으나, 해당 파일 단독 2/2와 Audio 전체 단독 53/53이 통과했다. 같은 양상이 기존 §9·`P4-R5` 기록에도 있어 이번 계약 finding과 분리했고 신규 제품 결함으로 등록하지 않았다.
### 신규 발견 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-010` | Medium | 수정 완료 | Audio cover가 원본 파일 정책 검증 전에 crop 준비로 진입한다 | `P4-R7` | `P4-R7` |
### REV-P4-010 — Audio cover가 원본 파일 정책 검증 전에 crop 준비로 진입한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `FILE-001~003`, `FILE-006~007`, PRD `Image crop UI 흐름` 1~3단계
- **관련 계약:** cover JPEG/PNG, exact max `10,485,760 bytes`, extension/MIME pair
- **소유 Task:** 신규 `P4-R7`
**관찰 내용**
`selectCoverImage`는 새 파일을 선택하면 `validateAudioCoverFile`을 호출하지 않고 바로 `createCropSource(file)`을 실행한다. 파일 정책 검증은 crop 적용 결과가 `coverImage`에 저장된 뒤 form submit validation에서만 일어난다. 따라서 `10,485,761 bytes` 원본, GIF, 확장자/MIME 불일치 파일도 먼저 crop source 생성과 Dialog 경로로 들어간다.
**근거**
- 문서: `prd.md:350-356,382-389`
- 코드: `src/features/audio-contents/components/AudioContentForm.tsx:83-109,112-117`
- validator: `src/features/audio-contents/validation/audio-cover-policy.ts:7-27`
- 테스트 공백: `audio-contract.test.ts:216-227`은 validator 단위 계약만 확인하고, Audio form tests에는 invalid 원본이 `createCropSource`를 호출하지 않는 경계가 없다.
- 비교 근거: `SeriesForm`은 동일 단계에서 `validateSeriesImageFile` 실패 시 crop source 호출 전에 반환한다.
**재현 또는 검증 절차**
1. Audio 생성·수정 form에 호출 횟수를 기록하는 `createCropSource`를 주입한다.
2. `10,485,761 bytes` PNG 또는 `.png`/`image/jpeg` 파일을 cover로 선택한다.
3. 현재 구현에서 `createCropSource`가 1회 호출되는 것을 확인한다.
4. 요구 결과는 호출 0회, crop Dialog 0개, 즉시 inline 정책 오류다.
**영향과 권장 조치**
부적합 원본이 불필요한 decode/canvas 작업에 들어가며, 큰 원본이 crop 결과 크기만 작아져 제출 검증을 통과하면 “선택 직후 원본 상한 검증” 계약을 우회한다. 기존 validator와 오류 메시지를 재사용해 crop source 생성 전에 반환하고 create/edit 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — PRD의 명시적 처리 순서, Audio component와 validator·form tests를 대조해 확정.
- 2026-07-31 — `plan-task.md`에 미완료 신규 Task `P4-R7`로 전환. 애플리케이션 코드는 이번 리뷰 범위에서 수정하지 않았다.
- 2026-07-31 — `P4-R7`에서 `selectCoverImage`가 기존 cover validator를 crop source 준비 전에 재사용하도록 수정했다. RED focused는 2 failed / 18 passed, GREEN focused는 2 files / 20 tests passed였다. `npm run typecheck`, `npm run lint`, `npm run build`, Audio directory LSP diagnostics는 통과했다. 개발 중 E2E는 사용자 지시에 따라 보류했다.
### plan·goal 전환 및 종료 판정
- `REV-P4-010``plan-task.md` 신규 `P4-R7`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **남은 항목:** 기존 실제 개발 API Audio 수동 QA.
## 13. 종합 재점검 — 2026-07-31
- **검토 범위:** Audio 목록·상세·multipart 생성/수정·UTC 예약·soft delete·player·Comments 연결을 OpenAPI와 PRD `AUDIO-*`에 재대조했다.
- **실행 증거:** Audio를 포함한 도메인 묶음 36 files / 206 tests passed. exact server E2E 18/18과 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다.
- **판정:** `REV-P4-010`/`P4-R7` 수정 완료 상태가 유지되며 Audio 도메인 소유의 확정 신규 finding은 없다.
- **교차 Phase:** Audio cover에 영향을 주는 공용 crop 결함은 `REV-P1-016~017`/`P1-R10~R11`로만 추적한다.
- **남은 위험:** 실제 개발 API upload/예약/soft delete 수동 QA와 Safari Gate 복원이 필요하다.
- **신규 Phase 4 Task:** 없음.
## 14. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** Audio list/detail/create/update/upload/player, UTC release, price·file policy, cover crop와 Comments 연결.
- **실행 증거:** 관련 unit, `typecheck`·`lint`·build와 4-project mock Audio journey가 0 failure였고 server allowlist 36 tests도 통과했다.
- **판정:** `P4-R7`까지의 수정 완료 상태가 유지되며 Phase 4 소유의 신규 기능 결함은 없다.
- **문서 교차 항목:** 상단의 “교차 회귀 대기”와 이미 완료된 공통 crop 수정 이력 불일치는 `REV-P10-008`/`P10-R7`에서 정리한다.
- **남은 위험:** 실제 개발 API upload/예약/soft delete와 서버 파일 거부 수동 QA.
## 15. 최종 재검증 및 신규 판정 — 2026-07-31
### 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P4-011` | Medium | 수정 완료 | 완료된 network 실패를 뒤늦게 취소하면 upload 정산이 이중 실행되어 다음 session의 401을 무시한다 | `P4-R8` | `P4-R8` |
### REV-P4-011 — 완료된 network 실패를 뒤늦게 취소하면 upload 정산이 이중 실행되어 다음 session의 401을 무시한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUDIO-017`, `AUTH-005`, PRD §13의 upload 취소·재시도와 session 401 복구
- **관련 계약:** Audio multipart upload와 HTTP 401 공통 처리. OpenAPI endpoint·DTO 변경 없음.
- **소유 Task:** 신규 `P4-R8`
**관찰 내용**
`uploadAudioContent`의 resolve/reject helper는 호출될 때마다 보호 upload count를 감소시킨다. `onload`만 AbortSignal listener를 제거하고 `onerror`는 제거하지 않으므로, network error로 Promise가 이미 reject된 뒤 같은 signal이 abort되면 reject/정산이 다시 실행된다. count가 음수가 되고 authentication-expiry latch가 다음 login session까지 true로 남을 수 있다.
**근거**
- 코드: `src/features/audio-contents/api/upload-audio-content.ts:68-80`은 settle guard 없이 모든 reject에서 count를 감소시키고, `:96-100`은 listener cleanup을 `onload`에만 둔다. listener는 `:132`에서 등록된다.
- 테스트: `src/features/audio-contents/tests/audio-upload.test.ts:123-166`의 cancel/retry와 `:191-228`의 401 burst는 각각 독립 경로만 검증한다.
- 문서: `AUDIO-017`, `AUTH-005`, 기존 `P4-R2`는 취소·재시도 이후에도 session expiry 흐름을 보존해야 한다.
**재현 또는 검증 절차**
1. signal-bearing protected upload를 network error로 먼저 종료한 뒤 같은 `AbortController`를 abort한다.
2. 첫 session의 upload를 401로 종료해 만료 callback 1회를 확인하고, 새 token을 설정한다.
3. 새 session의 upload도 401로 종료해 callback 총 2회를 기대하는 임시 진단 test를 focused 실행한다.
4. 실제 결과는 exit 1, 1 failed였고 `clearSession` 기대 2회 대비 실제 1회였다. 진단 코드는 판정 후 제거해 제품 test 변경을 남기지 않았다.
**영향**
network error와 cancel이 인접한 제한 조건 뒤에는 새로 로그인해도 다음 upload 401에서 session 제거·login 이동이 실행되지 않을 수 있다. 업로드 오류 자체는 표시되지만 인증 만료 복구가 누락된다.
**권장 조치**
요청별 settled guard와 공통 terminal cleanup으로 Promise settle, count 감소와 AbortSignal listener 해제를 정확히 한 번만 수행한다. network error→late abort→첫/새 session 401 순서를 회귀 test로 고정하고 새 upload abstraction은 만들지 않는다.
**판정 기록**
- 2026-07-31 — terminal handler data flow를 추적하고 임시 진단 test에서 새 session callback 누락을 재현해 Medium 확정.
- 2026-07-31 — `plan-task.md` 신규 `P4-R8`로 전환하고 `P1-R12` 완료 뒤 실행하도록 의존성을 기록. 애플리케이션 코드는 수정하지 않음.
- 2026-07-31 — `P4-R8`에서 요청별 settled guard와 공통 terminal cleanup을 추가해 수정 완료. reviewer blocker로 fetch-first 순서와 주입 auth authoritative token 처리를 보강했다. upload focused 2 files / 13 tests, Audio feature 9 files / 58 tests, auth/app/shared 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, 개발/운영 build, LSP diagnostics가 통과했다.
### 종료 판정
- **자동 검증:** `P4-R8` focused 2 files / 13 tests, Audio feature 9 files / 58 tests, 교차 auth focused 3 files / 22 tests, `src/app src/features/auth src/shared/api` 포함 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check`, 변경 파일 LSP diagnostics가 통과했다. E2E는 사용자 지시에 따라 반복 실행하지 않았다.
- **최종 결론:** `REV-P4-011` 수정 완료. Blocker/High 없음.
- **남은 항목:** 실제 개발 API upload/예약/soft delete·서버 파일 거부 수동 QA.
## 16. 2026-07-31 문서 기준 재리뷰
- **검토 범위:** Audio list/detail/player/create/update/deactivate, XHR upload·cancel·auth lifecycle, UTC reservation, cover/audio file policy를 `AUDIO-001~033`·OpenAPI·`P4`·`P10-T2`에 대조했다.
- **실행 증거:** Audio contract/upload/auth lifecycle test는 full output에서 통과했고, type·lint·개발/운영 build·server E2E 36 tests도 통과했다. full unit 5 failure는 `REV-P9-009`/`P9-R9`, cover crop frame은 공용 `REV-P1-019`/`P1-R13`으로 분리했다.
- **판정:** Audio endpoint·DTO·upload transport 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 4 Task 없음.
- **남은 위험:** 실제 upload·container/codec·예약·soft delete 수동 QA와 `P1-R13`, `P9-R9` 회귀가 남는다.