# Phase 4 코드 리뷰·QA — 2026-07-29 ## 1. 리뷰 정보 | 항목 | 내용 | |---|---| | 리뷰 대상 | Phase 4 / Audio vertical slice | | 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 | | 리뷰 일자 | 2026-07-29 | | 리뷰어 | Codex | | 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` | | 리뷰 상태 | 판정 완료 | ## 2. 리뷰 목적과 범위 ### 목적 - `AUDIO-001~033`, upload·price·media/file 기준과 Phase 4/P10 후속 결과를 대조한다. ### 포함 범위 - 코드: `src/features/audio-contents` - 테스트: Audio contract/form/upload/player - 문서: Audio 요구사항·OpenAPI request·Phase 4 Task/Gate - 수동 검증: form control과 upload lifecycle 정적 대조 ### 제외 범위 - 공통 crop 계산은 Phase 1, inactive workspace와 공통 field 접근성은 Phase 9, 공통 오류 문구 최종 회귀는 Phase 10 ## 3. 판정 기준 심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다. ## 4. 검토한 근거 ### 문서와 코드 - 요구사항: `AUDIO-014`, `AUDIO-017~019`, `AUDIO-024`, `AUDIO-033`, `AUTH-005`, `FILE-002` - 코드: `AudioContentForm.tsx:60`, `AudioContentForm.tsx:103`, `AudioContentForm.tsx:171`, `AudioContentForm.tsx:179`, `audio-content-form-helpers.ts:11`, `audio-content-form-helpers.ts:12`, `audio-content-form-helpers.ts:87`, `upload-audio-content.ts:51`, `upload-audio-content.ts:61`, `upload-audio-content.ts:85` - 테스트: `audio-form.test.tsx`, `audio-form-upload.test.tsx`, `audio-upload.test.ts`, `audio-contract.test.ts` ### 실행 환경 ```text OS: macOS 26.0 Node: v24.12.0 npm: 11.7.0 API mode: injected upload client/XHR fake/MSW ``` ### 실행한 검증 | 명령 또는 검증 | 결과 | 핵심 증거 | |---|---|---| | `npm run test:run -- src/features/audio-contents src/features/series` | 성공 | 합계 11 files / 46 tests passed | | 가격 parser `node` 계산 | 실패 재현 | `-1 → 1`, `1.5 → 15` | | form/upload code 대조 | 실패 재현 | optional control 누락, upload 중 submit 활성, XHR 401 session 처리 없음 | ## 5. 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-P4-001` | High | 수정 완료 | Audio create form이 OpenAPI optional 설정을 고정값으로만 보낸다 | `P4-T3` | `P4-R1` | | `REV-P4-002` | High | 수정 완료 | 음수·소수 가격을 거부하지 않고 다른 유효값으로 바꾼다 | `P10-T7` | `P4-R1` | | `REV-P4-003` | High | 수정 완료 | upload 중 중복 submit이 가능하다 | `P4-T3` | `P4-R2` | | `REV-P4-004` | High | 수정 완료 | XHR upload 401이 session clear/login 이동을 실행하지 않는다 | `P4-T3` | `P4-R2` | | `REV-P4-005` | Medium | 수정 완료 | `.m4a + audio/mp4`가 file picker accept에서 누락됐다 | `P4-T3` | `P4-R3` | | `REV-P4-006` | Medium | 수정 완료 | cover image의 확장자와 MIME 불일치가 통과한다 | `P4-T3` | `P4-R3` | ## 6. 발견 사항 상세 ### REV-P4-001 — Audio create form이 확정 optional 설정을 제공하지 않는다 - **심각도:** High - **상태:** 수정 완료 - **관련 요구사항:** `AUDIO-033` - **관련 계약:** `AudioContentCreateRequest` - **소유 Task:** 신규 `P4-R1` **관찰 내용** `purchaseOption`, `limited`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 편집하는 control이 없다. serializer는 전부 고정 default로 보낸다. **영향** 운영자가 OpenAPI와 PRD에 확정된 발행·구매·댓글·미리보기 설정을 선택할 수 없다. **권장 조치** 계약 enum/type만 사용한 최소 control을 추가하고 create payload·dirty state·접근성 test를 보강한다. **판정 기록** - 2026-07-29 — visible control과 serializer 고정값을 대조해 확정. - 2026-07-30 — create 전용 optional 설정 control과 serializer를 추가하고 Audio unit/mock E2E/static/build 검증 통과. ### REV-P4-002 — Audio 가격의 금지 입력이 다른 값으로 변환된다 - **심각도:** High - **상태:** 수정 완료 - **관련 요구사항:** `AUDIO-019`, PRD `14.1` 가격 경계 - **관련 계약:** `price` integer `0..99999` - **소유 Task:** 신규 `P4-R1` **관찰 내용** parser가 숫자가 아닌 모든 문자를 제거한다. `-1`은 `1캔`, `1.5`는 `15캔`이 되어 validation을 통과한다. **영향** 사용자가 거부돼야 할 값을 입력했는데 의도와 다른 가격이 저장될 수 있다. **권장 조치** 허용 display suffix/group separator만 정규화하고 부호·소수점은 invalid raw state로 유지해 제출을 차단한다. `-1`, `1.5` form test를 추가한다. **판정 기록** - 2026-07-29 — parser 계산으로 확정. - 2026-07-30 — `-1`, `1.5`, `100000` raw 입력을 변형하지 않고 request 0건으로 차단하도록 수정하고 Audio unit/mock E2E/static/build 검증 통과. ### REV-P4-003 — Audio upload 중 중복 제출이 차단되지 않는다 - **심각도:** High - **상태:** 수정 완료 - **관련 요구사항:** Character/Audio form 저장 중 중복 제출, `AUDIO-017` - **관련 계약:** Audio create POST - **소유 Task:** 신규 `P4-R2` **관찰 내용** submit handler에 uploading guard가 없고 생성 버튼도 `uploadState.status`로 비활성화되지 않는다. 첫 요청 pending 중 다시 submit하면 새 AbortController와 POST를 시작한다. **영향** 중복 콘텐츠 생성·대용량 중복 전송이 발생할 수 있다. **권장 조치** synchronous ref guard와 disabled 상태를 함께 적용하고 pending upload double-submit test를 추가한다. **판정 기록** - 2026-07-29 — submit/button 경로에 guard 0건으로 확정. - 2026-07-30 — synchronous pending ref와 uploading disabled button을 추가해 pending 중 submit 2회를 request 1건으로 차단하고 cancel 후 재제출 가능함을 focused test로 확인. ### REV-P4-004 — XHR upload 401에서 인증 상태가 남는다 - **심각도:** High - **상태:** 수정 완료 - **관련 요구사항:** `AUTH-005` - **관련 계약:** upload 보호 요청 401 - **소유 Task:** 신규 `P4-R2` **관찰 내용** XHR adapter는 storage에서 token만 읽고 401을 일반 `ApiError`로 reject한다. 공통 client의 clearSession/onAuthExpired 경로를 사용하지 않는다. **영향** 대용량 upload 도중 session이 만료되면 login으로 복구되지 않는다. **권장 조치** 공통 인증 만료 callback을 upload adapter에 주입하거나 동일한 단일 session expiry controller를 사용하고 XHR 401 test를 추가한다. **판정 기록** - 2026-07-29 — XHR adapter의 session remove·redirect 호출 0건으로 확정. - 2026-07-30 — upload adapter에 token reader/session clear/login callback을 주입하고 protected 401 burst에서 clear/login 1회만 실행하도록 수정. malformed 401과 normal 401 회귀 test 및 공통 client-auth 회귀 통과. ### REV-P4-005 — canonical M4A MIME이 file picker에서 빠졌다 - **심각도:** Medium - **상태:** 수정 완료 - **관련 요구사항:** `AUDIO-014`, `AUDIO-024` - **관련 계약:** `.m4a` + `audio/mp4` - **소유 Task:** 신규 `P4-R3` **관찰 내용** validator는 `audio/mp4`를 허용하지만 input `accept`는 `audio/mpeg,audio/aac,audio/x-m4a`만 제공한다. **영향** 브라우저 file chooser가 정상 M4A 파일을 숨기거나 선택을 방해할 수 있다. **권장 조치** `AUDIO_FILE_POLICY.allowedMimeTypes`를 accept source로 재사용해 중복을 제거한다. **판정 기록** - 2026-07-29 — validator와 input accept 대조로 확정. - 2026-07-30 — Audio file picker accept를 `AUDIO_FILE_POLICY.allowedMimeTypes.join(",")`로 변경해 `audio/mp4` 포함을 focused test로 확인. ### REV-P4-006 — Audio cover format pair가 검증되지 않는다 - **심각도:** Medium - **상태:** 수정 완료 - **관련 요구사항:** `FILE-002` - **관련 계약:** JPEG/PNG cover - **소유 Task:** 신규 `P4-R3` **관찰 내용** `.png + image/jpeg`, `.jpg + image/png`가 extension allowlist와 MIME allowlist를 각각 통과한다. **영향** 잘못된 multipart가 backend까지 전송된다. **권장 조치** extension별 MIME map과 mismatch test를 추가한다. **판정 기록** - 2026-07-29 — `validateAudioCoverFile`이 공통 독립 allowlist만 호출함을 확인해 확정. - 2026-07-30 — cover extension별 MIME pair 검증을 추가해 `.jpg/.jpeg + image/png`, `.png + image/jpeg`를 `mime` 오류로 차단하고 contract 회귀 test로 확인. ## 7. 확정 항목의 plan·goal 전환 - `REV-P4-001~002` → `P4-R1` - `REV-P4-003~004` → `P4-R2` - `REV-P4-005~006` → `P4-R3` ## 8. 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | Audio contract/form/upload/file 대조 | | 후보 항목 판정 완료 | 충족 | 6건 확정 | | 확정 항목 plan 반영 | 충족 | `P4-R1~R3` | | 보류 항목 담당·재개 조건 | 해당 없음 | 보류 없음 | | 검증 명령과 결과 기록 | 충족 | §4 | **최종 결론:** 수정 goal 필요. **남은 항목:** `P4-R1~R3`, Phase 9 inactive/접근성, Phase 10 오류 회귀. ## 9. 수정 후 검증 기록 - 2026-07-30 — `P4-R1` 완료. 검증: `npm run test:run -- src/features/audio-contents` 8 files / 35 tests passed, `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 21 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과. - 2026-07-30 — `P4-R2` 완료. RED: `audio-form-upload.test.tsx`는 pending upload 중 2회 호출로 실패했고, `audio-upload.test.ts`는 protected 401에서 `clearSession` 0회로 실패했다. GREEN/검증: `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx` 1 file / 6 tests passed, `npm run test:run -- src/features/audio-contents/tests/audio-upload.test.ts src/shared/api/__tests__/client-auth.test.ts` 2 files / 19 tests passed, `npm run test:run -- src/features/audio-contents` 8 files / 38 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과. - 2026-07-30 — `P4-R3` 완료. RED: `audio-form-upload.test.tsx`는 accept `audio/mp4` 누락으로 실패했고, `audio-contract.test.ts`는 cover mismatch가 `{ ok: true }`로 실패했다. GREEN/검증: `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx` 1 file / 7 tests passed, `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts src/shared/validation/file-media-policy.test.ts` 2 files / 17 tests passed, `npm run test:run -- src/features/audio-contents src/features/community-posts` 12 files / 76 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.