Files

185 lines
7.9 KiB
Markdown

# Phase 5 코드 리뷰·QA — 2026-07-29
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 5 / Series vertical slice와 `P10-T3` |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`; 리뷰 시작 시 tracked diff 36개, 전체 변경 169개 |
| 리뷰 일자 | 2026-07-29 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json`, `plan-task.md` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- `SERIES-001~018`, Series image/file 계약과 Phase 5/P10 후속 구현을 대조한다.
### 포함 범위
- 코드: `src/features/series`
- 테스트: Series contract/form/route/contents/order
- 문서: Phase 5, `P10-T3`, OpenAPI Series request
- 수동 검증: update payload와 initial state 비교
### 제외 범위
- inactive workspace 교차 정책은 Phase 9, 공통 crop은 Phase 1
## 3. 판정 기준
심각도는 `Blocker/High/Medium/Low`, 상태는 `확정/오탐/보류/수정 완료`를 사용했다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `SERIES-004`, `SERIES-007`, `SERIES-010`, `FILE-002`
- 코드: `SeriesForm.tsx:77`, `SeriesForm.tsx:127`, `series-schema.ts:28`, `series-schema.ts:35`, `series-image-policy.ts:14`
- 테스트: `series-form.test.tsx`, `series-contract.test.ts`
### 실행 환경
```text
OS: macOS 26.0
Node: v24.12.0
npm: 11.7.0
API mode: injected client/MSW
```
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run test:run -- src/features/audio-contents src/features/series` | 성공 | 합계 11 files / 46 tests passed |
| edit payload 정적 대조 | 실패 재현 | 초기 state와 같아도 request에 `state` 포함 |
| image policy 대조 | 실패 재현 | extension/MIME 독립 allowlist |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P5-001` | High | 수정 완료 | 변경하지 않은 Series `state`를 항상 전송한다 | `P10-T3` | `P5-R1` |
| `REV-P5-002` | Medium | 수정 완료 | Series mutation schema가 `isActive=true/null`, `state=null`을 허용한다 | `P10-T3` | `P5-R1` |
| `REV-P5-003` | Medium | 수정 완료 | Series image 확장자와 MIME 불일치가 통과한다 | `P10-T3` | `P5-R2` |
## 6. 발견 사항 상세
### REV-P5-001 — 변경 없는 Series state가 update payload에 포함된다
- **심각도:** High
- **상태:** 확정
- **관련 요구사항:** `SERIES-004`, PRD `14.1`
- **관련 계약:** `SeriesUpdateRequest.state`
- **소유 Task:** 신규 `P5-R1`
**관찰 내용**
edit form은 초기 state를 local state에 넣고 모든 저장 request에 `state`를 포함한다. 현재 값과 원본을 비교해 생략하는 분기가 없다.
**영향**
요구된 partial update 의미를 위반하고 다른 동시 변경을 불필요하게 덮어쓸 수 있다.
**권장 조치**
원본과 달라진 경우에만 `state` key를 추가하고 unchanged/changed payload test를 분리한다.
**판정 기록**
- 2026-07-29 — `SeriesForm.tsx:127`과 기존 test의 항상 포함 assertion으로 확정.
- 2026-07-30 — `P5-R1`에서 unchanged state 생략 회귀 test와 원본 비교 serializer를 추가해 수정 완료.
### REV-P5-002 — Series schema가 제품 mutation 불변식을 강제하지 않는다
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** `SERIES-002`, `SERIES-010`
- **관련 계약:** `SeriesUpdateRequest`
- **소유 Task:** 신규 `P5-R1`
**관찰 내용**
export된 update schema가 nullable `state`와 boolean/nullable `isActive`를 허용한다. 따라서 API helper 호출자는 `state:null`, `isActive:true/null`을 전송할 수 있다.
**영향**
현재 UI 밖의 후속 호출자가 복원 금지·soft-delete-only 정책을 우회할 수 있다.
**권장 조치**
일반 update와 deactivate request schema를 분리해 `isActive=false`만 별도 adapter에서 허용하고 state는 enum만 허용한다.
**판정 기록**
- 2026-07-29 — schema safe-parse 가능 범위를 PRD 정책과 대조해 확정.
- 2026-07-30 — `P5-R1`에서 일반 update schema와 deactivate `{ isActive:false }` schema를 분리하고 `state:null`, `isActive:true/null` 거부 test로 수정 완료.
### REV-P5-003 — Series image format pair가 검증되지 않는다
- **심각도:** Medium
- **상태:** 확정
- **관련 요구사항:** `FILE-002`, `SERIES-014`
- **관련 계약:** JPEG/PNG image
- **소유 Task:** 신규 `P5-R2`
**관찰 내용**
`.png + image/jpeg` 또는 `.jpg + image/png`가 허용된다.
**영향**
Series 생성·이미지 교체가 잘못된 multipart를 전송할 수 있다.
**권장 조치**
extension별 MIME mapping과 양방향 mismatch test를 추가한다.
**판정 기록**
- 2026-07-29 — Series policy가 독립 allowlist validator만 호출함을 확인해 확정.
- 2026-07-30 — `P5-R2`에서 Series 전용 extension↔MIME pair test와 crop-before-validation test를 추가하고 policy mapping으로 수정 완료.
## 7. 확정 항목의 plan·goal 전환
- `REV-P5-001~002``P5-R1`
- `REV-P5-003``P5-R2`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | Series code/test/contract 대조 |
| 후보 항목 판정 완료 | 충족 | 3건 확정 |
| 확정 항목 plan 반영 | 충족 | `P5-R1`, `P5-R2` |
| 보류 항목 담당·재개 조건 | 해당 없음 | 기존 server fixture 차단은 계획에 이미 기록 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 수정 goal 필요.
**남은 항목:** 기존 P10 server integration, Phase 9 inactive 정책.
## 9. 수정 후 검증 기록
### P5-R1 Series partial update 불변식 복구, 2026-07-30
- 무엇을: Series edit payload에서 원본과 같은 `state`를 생략하고, 일반 update schema와 deactivate `{ isActive:false }` schema를 분리했다.
- 왜: partial update가 변경 없는 상태값을 덮어쓰지 않고, soft delete 이외의 `isActive` mutation과 `state:null`을 boundary에서 막기 위해서다.
- 어떻게:
- RED: `npm run test:run -- src/features/series/tests/series-form.test.tsx src/features/series/tests/series-update-invariants.test.ts`는 2 failed로, unchanged edit payload에 `state: "PROCEEDING"`이 포함되고 `state:null` parse가 성공함을 확인했다.
- GREEN: `SeriesForm`은 원본과 달라진 경우에만 `state`를 request에 넣고, `seriesUpdateRequestSchema`는 enum `state`만 허용한다. `deactivateSeries`와 mock store는 별도 `seriesDeactivateRequestSchema``{ isActive:false }`만 처리한다.
- 검증: focused 2 files / 5 tests 통과, `npm run test:run -- src/features/series`는 6 files / 20 tests 통과했다. `npm run typecheck`, `npm run lint`, `npm run build`도 모두 exit 0이었다.
### P5-R2 Series image format pair 검증, 2026-07-30
- 무엇을: Series image validation에서 `.jpg/.jpeg``image/jpeg`, `.png``image/png`만 허용하도록 extension↔MIME pair를 고정했다.
- 왜: 독립 allowlist만으로는 `.png + image/jpeg`, `.jpg + image/png`가 crop과 multipart upload까지 진행될 수 있기 때문이다.
- 어떻게:
- RED: `npm run test:run -- src/features/series/tests/series-image-policy.test.ts src/features/series/tests/series-form.test.tsx`는 2 failed로, mismatch file이 `{ ok: true }`를 반환하고 crop dialog가 열림을 확인했다.
- GREEN: `series-image-policy.ts`에 Series 전용 extension→MIME mapping을 추가해 mismatch를 `{ ok:false, reason:"mime" }`로 거부하고, 기존 size/extension/MIME allowlist 검사는 유지했다.
- 검증: focused 2 files / 5 tests 통과, `npm run test:run -- src/features/series src/shared/validation/file-media-policy.test.ts`는 8 files / 29 tests 통과했다. `npm run typecheck`, `npm run lint`, `npm run build`도 모두 exit 0이었다.
남은 항목: 기존 P10 server integration과 Phase 9 inactive 정책은 후속 범위로 유지한다.