Files

248 lines
11 KiB
Markdown

# 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` 통과.