# Phase 5 Series Management 코드 리뷰·QA ## 1. 리뷰 정보 | 항목 | 내용 | |---|---| | 리뷰 대상 | Phase 5 / Series CRUD, 장르, 콘텐츠 연결·해제·순서 | | 기준 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` | | 리뷰 상태 | 판정 완료, 신규 회귀 수정 완료 | ## 2. 리뷰 목적과 범위 ### 목적 - Series CRUD·lookup·연결·순서 payload와 responsive UI가 요구사항·계약에 맞는지 검증한다. - API 원본 enum/ID와 사용자 표시 label의 경계가 일관적인지 확인한다. ### 포함 범위 - 코드: `src/features/series` - 테스트: Series contract/form/route/order/E2E - 문서: `SERIES-001`~`SERIES-018`, 관련 OpenAPI operation, `P5-*` - 수동 검증: list/card/detail에서 같은 DTO의 표시 결과 정적 대조 ### 제외 범위 - 실제 서버의 전체 page 순서 저장 fixture - Phase 0 origin 문제로 실행되지 못한 server E2E의 중복 판정 ## 3. 판정 기준 심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 계약 payload 무결성과 운영자에게 노출되는 label 일관성을 중점 확인했다. ## 4. 검토한 근거 ### 문서와 코드 - 요구사항: `SERIES-002`, `SERIES-005`, `SERIES-007`, `SERIES-015`, PRD §13 - 계약: Series list/detail item, genre lookup, CRUD·link·unlink·order operations - 계획: `P5-T1`~`P5-GATE` - 코드: `SeriesList.tsx:8-38`, `SeriesListItem.tsx:5-28`, `SeriesSummary.tsx:3-35` - 테스트: `series-routes.test.tsx:147` 등 raw label 기대 ### 실행 환경 ```text macOS 26.0 / Node v24.12.0 / npm 11.7.0 Playwright 4 projects, 320/768/1280px ``` ### 실행한 검증 | 명령 또는 수동 검증 | 결과 | 핵심 증거 | |---|---|---| | `npm run test:run` | 성공 | 72 files, 354 tests passed | | `npm run e2e:mock` | 부분 실패 | Series 전체 흐름은 WebKit 768px 1회 timeout 외 통과 | | `npm run e2e:mock -- tests/e2e/series.spec.ts --project=webkit --grep "desktop and tablet Series management flow remains available at 768px"` | 성공 | 1 passed, 12.6s; 결함으로 확정하지 않음 | | `npm run typecheck` / `npm run lint` / `npm run build` | 성공 | 모두 exit 0 | | 표시 label 정적 대조 | 실패 | 목록·card는 raw day/state/genreId/boolean, 상세도 raw genreId/boolean | | 생성·수정 schema 2차 계약 대조 | 실패 | 누락 `genreId`와 중복 `publishedDaysOfWeek`가 parsing·mock create에서 허용됨 | ## 5. 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-P5-004` | Medium | 수정 완료 | Series 목록·card·상세가 enum·genreId·boolean 원본을 운영자에게 노출한다 | `P5-R3` | `P5-R3` | | `REV-P5-005` | Medium | 수정 완료 | Series schema와 mock이 필수 장르·요일 유일성 불변식을 강제하지 않는다 | `P5-R4` | `P5-R4` | ## 6. 발견 사항 상세 ### REV-P5-004 — Series 목록·card·상세가 enum·genreId·boolean 원본을 운영자에게 노출한다 - **심각도:** Medium - **상태:** 수정 완료 - **관련 요구사항:** `SERIES-002`, `SERIES-005`, `SERIES-007`, `SERIES-015`, PRD §13 - **관련 계약:** Series 원본 enum/ID는 수정 payload에 유지 - **소유 Task:** 신규 `P5-R3` **관찰 내용** desktop list와 mobile card는 요일·state enum을 그대로 표시하고 `genreId 77`, `isAdult false`, `isActive true` 같은 내부 필드명을 출력한다. 상세는 요일·state만 변환하며 `genreId`와 boolean은 그대로 표시한다. 같은 DTO가 surface마다 서로 다른 label 정책을 사용한다. **근거** - 코드: `src/features/series/components/SeriesList.tsx:8-38` - 코드: `src/features/series/components/SeriesListItem.tsx:5-28` - 코드: `src/features/series/components/SeriesSummary.tsx:3-35` - 테스트: `src/features/series/tests/series-routes.test.tsx:147`이 `genreId 77` 노출을 기대 - 문서: `prd.md:283` `SERIES-015`, `prd.md:806` **재현 또는 검증 절차** 1. Series 목록을 desktop과 mobile width에서 연다. 2. 요일 `SUN`/`RANDOM`, 상태 `PROCEEDING`, `genreId`, `isAdult false`가 표시되는 것을 확인한다. 3. 상세에서는 일부만 한글 label로 바뀌어 surface 간 표현이 다른 것을 확인한다. 4. 요구 결과는 API 원본을 payload에만 유지하고 표시에는 장르명·요일·상태·boolean label을 일관되게 사용하는 것이다. **영향** 운영자가 API 내부 표현을 해석해야 하고, desktop/mobile/detail 사이에서 같은 시리즈 상태가 다르게 보인다. **권장 조치** 장르 lookup map과 중앙 Series formatter를 사용해 모든 surface의 표시를 통일하되 form 초기화·update payload에는 기존 원본 enum과 ID를 보존한다. **판정 기록** - 2026-07-30 — PRD 표시 경계와 세 UI surface 및 고정 test를 대조해 확정. - 2026-07-30 — `P5-R3` 구현과 fallback/CJK/header 보완 후 focused unit, full Series mock E2E, static gate, 수동 확인, 독립 visual QA 최종 A/B PASS로 수정 완료 판정. ### REV-P5-005 — Series schema와 mock이 필수 장르·요일 유일성 불변식을 강제하지 않는다 - **심각도:** Medium - **상태:** 수정 완료 - **관련 요구사항:** `SERIES-006`, `SERIES-007` - **관련 계약:** `SeriesCreateRequest.publishedDaysOfWeek.uniqueItems=true`, `genreId=0`은 domain에서 무효 - **소유 Task:** 신규 `P5-R4` **관찰 내용** `seriesCreateRequestSchema`의 `genreId`가 optional이고 공용 `publishedDaysSchema`에는 중복 검사가 없다. 따라서 생성 schema는 장르가 없는 payload와 `["MON","MON"]`을 모두 허용한다. mock create는 장르가 없으면 첫 fixture 장르를 임의로 넣어 잘못된 request를 성공 처리한다. 화면 form은 장르 선택과 요일 toggle을 제한하지만 API adapter·mock 경계는 같은 불변식을 보장하지 않는다. **근거** - 코드: `src/features/series/schemas/series-schema.ts:15-26` - mock: `src/shared/mocks/series-mock-store.ts:48-55` - 실행 재현: `seriesCreateRequestSchema.safeParse()` 결과 누락 장르 `true`, 중복 요일 `true` - 문서: `prd.md:274-275` - 계약: OpenAPI `SeriesCreateRequest.publishedDaysOfWeek.uniqueItems=true`, `genreId` 설명의 domain validation **재현 또는 검증 절차** 1. `{title,introduction,publishedDaysOfWeek:["MON"],keyword}`를 create schema에 넣으면 성공한다. 2. `genreId:1`, `publishedDaysOfWeek:["MON","MON"]`을 넣어도 성공한다. 3. 같은 누락 장르 multipart를 mock POST에 보내면 mock store가 첫 장르로 보정해 생성한다. 4. 요구 결과는 누락/0 장르와 중복 요일을 request 경계에서 거부하는 것이다. **영향** 현재 form 경로 밖의 adapter 호출이나 회귀 test가 계약 밖 Series를 전송할 수 있고, mock preview가 이를 정상 생성으로 숨긴다. update schema도 같은 요일 schema를 재사용하므로 중복 요일을 허용한다. **권장 조치** create `genreId`를 positive integer 필수로 만들고 공용 요일 schema에 uniqueness를 추가한다. mock의 첫 장르 fallback을 제거하고 schema·contract test에서 invalid request를 고정한다. **판정 기록** - 2026-07-30 — PRD·OpenAPI와 schema 실행 결과·mock fallback을 대조해 확정. - 2026-07-30 — `P5-R4`에서 생성 `genreId` 필수와 생성·수정 요일 유일성을 schema에 적용하고 mock의 첫 장르 fallback을 제거했다. focused Series 단위 회귀와 정적 Gate로 수정 완료를 확인했다. ## 7. 확정 항목의 plan·goal 전환 - `REV-P5-004` → `plan-task.md` 신규 `P5-R3` - goal objective: `[P5-R3] Series 표시 label을 중앙화하고 API 원본값과 UI 표현의 경계를 복구한다.` - `REV-P5-005` → `plan-task.md` 신규 `P5-R4` - goal objective: `[P5-R4] Series 필수 장르와 요일 유일성을 schema·mock 경계에서 복구한다.` ## 8. 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | Series 코드·test·계약 대조 | | 후보 항목 판정 완료 | 충족 | 기존 1건 수정 완료, 신규 1건 수정 완료 | | 확정 항목 plan 반영 | 충족 | `P5-R3`, `P5-R4` 완료 | | 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 | | 검증 명령과 결과 기록 | 충족 | §4 | **최종 결론:** 확정 발견 사항 수정 완료. **남은 항목:** Series 일반 mutation pending 경계는 `REV-P9-004`/`P9-R4`, 실제 server integration은 기존 수동 QA 정책으로 분리한다. ## 9. 수정 후 검증 기록 - 2026-07-30 — RED: `src/features/series/tests/series-routes.test.tsx`와 `tests/e2e/series.spec.ts`에 desktop/mobile 목록·상세의 raw enum·boolean·field-name 노출 금지 assertion을 추가했다. 기존 raw 표시 기대를 운영자용 label 기대와 no-raw assertion으로 바꾸는 과정에서 `REV-P5-004` 재현 조건을 고정했다. - 2026-07-30 — GREEN/REFACTOR: `src/features/series/lib/series-display-labels.ts`를 추가해 요일, state, genre, 성인 여부, 노출 상태 label을 중앙화하고 `SeriesList`, `SeriesListItem`, `SeriesSummary`가 이를 사용하도록 정리했다. `SeriesListPage`와 `SeriesDetailPage`는 장르 lookup을 함께 불러와 표시 계층에 전달하며, form·mutation payload는 기존 원본 enum/ID를 유지한다. - 2026-07-30 — 독립 리뷰 1차: code review는 PASS, visual QA B는 PASS였다. visual QA A는 `formatSeriesGenre()`의 `장르 #id` fallback이 lookup 실패 시 내부 ID를 표시할 수 있다고 지적해 REVISE를 반환했다. - 2026-07-30 — fallback 보완: `formatSeriesGenre()` fallback을 `알 수 없는 장르`로 바꾸고, 장르 lookup miss에서 `장르 #`, `genreId`, `77`이 표시되지 않는 route 회귀 test를 추가했다. - 2026-07-30 — focused unit: `npm run test:run -- src/features/series/tests/series-routes.test.tsx` 결과 1 file / 5 tests passed. `npm run test:run -- src/features/series` 결과 7 files / 29 tests passed. - 2026-07-30 — E2E/static: `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium` 결과 7 passed. fallback 수정 후 `npm run e2e:mock -- tests/e2e/series.spec.ts` 결과 25 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. - 2026-07-30 — 수동 확인: mock mode에서 1280px desktop list/detail과 320px mobile list/detail fresh capture 및 DOM raw leak check를 수행했고 `/SUN|PROCEEDING|isAdult true|isActive true|genreId|장르 #/`는 네 화면 모두 false였다. 사용자가 이 문서의 재현 및 검증 절차를 보고 수동 확인했다. - 2026-07-30 — visual recheck B 보완: 모바일 안내문·카드 소개문의 CJK 중간 줄바꿈을 막기 위해 `break-keep`을 적용하고, 공통 workspace header의 visible `characterId:` 표시를 제거했다. `CharacterWorkspaceLayout.test.tsx`는 header의 `characterId:` 미노출을 확인한다. - 2026-07-30 — 최종 검증: `npm run test:run -- --fileParallelism=false src/layouts/CharacterWorkspaceLayout.test.tsx src/features/series/tests/series-routes.test.tsx` 결과 2 files / 10 tests passed. `npm run test:run -- src/features/series` 결과 7 files / 29 tests passed. `npm run e2e:mock -- tests/e2e/series.spec.ts` 결과 25 passed / 3 skipped. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 통과했다. - 2026-07-30 — `P5-R4` RED: `npm run test:run -- src/features/series/tests/series-contract.test.ts` 결과 1 failed / 6 passed로 `genreId` 누락 create payload 허용을 재현했다. - 2026-07-30 — `P5-R4` GREEN/회귀: `seriesCreateRequestSchema`의 `genreId`를 필수 positive integer로 바꾸고 공용 요일 schema에 uniqueness를 추가했으며, mock create의 첫 장르 fallback을 제거했다. `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/series/tests/series-form.test.tsx` 결과 2 files / 14 tests passed, `npm run test:run -- src/features/series src/shared/mocks` 결과 13 files / 51 tests passed. `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이었다. 개발 중 E2E는 사용자 지시에 따라 `P10-R5` 이후 최종 E2E로 미뤘다. - 2026-07-30 — 최종 visual QA: 1280px desktop list/detail, 320px mobile list/detail fresh capture에서 `/SUN|PROCEEDING|isAdult true|isActive true|genreId|장르 #|characterId:/` 누출 0건을 확인했다. 독립 visual QA 최종 A와 B는 모두 PASS, BLOCKING 없음이었다. - 2026-07-30 — 2차 계약 점검에서 누락 `genreId`와 중복 요일의 schema parse 성공 및 mock 첫 장르 fallback을 재현했다. 애플리케이션 코드는 수정하지 않고 `P5-R4`로 전환했다. ## 10. 2026-07-31 재점검 ### 실행·판정 요약 - **범위:** Series CRUD·장르·요일·연결·순서·image lifecycle을 PRD `SERIES-001~018`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`와 OpenAPI 2.3.0에 재대조했다. - **검증:** 전체 unit 72 files / 360 tests, server allowlist 36 tests, typecheck·lint·build가 통과했다. - **신규 발견:** Medium 1건. 기존 `REV-P5-004~005`는 수정 완료 상태를 유지한다. | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-P5-006` | Medium | 수정 완료 | Series image crop 준비·연속 선택 상태가 저장 경계에 없다 | `P5-R5` | `P5-R5` | ### REV-P5-006 — Series image crop 준비·연속 선택 상태가 저장 경계에 없다 - **심각도:** Medium - **상태:** 수정 완료 - **관련 요구사항:** `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD §10.5 - **소유 Task:** 신규 `P5-R5` **관찰 내용** Series form은 유효 파일을 committed `image`로 바로 넣지는 않지만 `createCropSource` 준비 상태와 마지막 선택 token, reject 오류를 관리하지 않는다. submit은 dialog가 열린 뒤에는 막지만 Promise가 pending인 동안에는 가능하다. edit는 새 image 선택 의도가 반영되지 않은 update를 먼저 보낼 수 있고 이전 느린 선택이 최신 dialog를 덮을 수 있다. **근거** - 코드: `src/features/series/components/SeriesForm.tsx:80-116,129-151,198-206` - 문서: `prd.md:354,356-361,524-538` - 테스트 공백: crop 적용·취소는 검증하지만 pending source, reject, 선택 순서 역전은 고정하지 않는다. **재현 또는 검증 절차** 1. edit form에 pending `createCropSource`를 주입하고 새 image를 선택한다. 2. Promise resolve 전에 저장해 새 image 없는 update가 진행되는지 확인한다. 3. 느린 첫 파일과 빠른 둘째 파일을 선택한 뒤 첫 Promise를 마지막에 resolve해 stale dialog가 열리는지 확인한다. **영향** 사용자가 교체를 선택했지만 기존 image가 유지되거나 잘못된 선택의 crop이 적용될 수 있다. 준비 실패도 화면에서 복구할 수 없다. **권장 수정 방향** Series form 내부에 selection token·준비 상태·inline 오류를 추가하고 준비/crop 중 submit을 비활성화·guard한다. crop 결과와 기존 서버 image 유지 계약은 그대로 둔다. **판정 기록:** - 2026-07-31 — 비동기 state와 파일 정책을 대조해 확정. - 2026-07-31 — `P5-R5`에서 Series image source 준비 상태와 selection token을 추가하고 준비/crop 중 submit을 차단했다. Promise reject는 inline 오류로 표시하고, crop 적용 결과만 `image`에 commit하며 수정 취소는 기존 서버 image 유지 계약을 보존한다. ### plan·goal 전환 및 종료 판정 - `REV-P5-006` → `plan-task.md` 신규 `P5-R5` - **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음. - **교차 Phase:** 2026-07-30 visual QA에서 제거한 workspace `characterId`는 Series 표시 문제가 아니라 PRD §7.2 위반이므로 `REV-P3-006`/`P3-R4`가 복구를 소유한다. ## 11. 최종 Phase별 점검 — 2026-07-31 - **검토 범위:** Series lookup·CRUD·image crop·연결/해제·전체 순서와 OpenAPI multipart/JSON payload를 `P5`에 재대조했다. - **실행 증거:** `npm run test:run -- src/features/series` 8 files / 37 tests, mock Chromium Series 시나리오 7 tests가 통과했다. - **판정:** Series 도메인 계약의 별도 신규 finding은 없다. 다만 create/update submit의 동기 재진입 guard와 저장 진행 표시 공백은 교차 품질 finding `REV-P9-006`/`P9-R6`에 귀속했다. - **남은 위험:** 실제 개발 API Series 수동 QA와 `P9-R6` 완료가 필요하다. - **신규 Phase 5 Task:** 없음. 중복 Task 대신 `P9-R6`에서 추적한다. ## 12. 종합 재점검 — 2026-07-31 - **검토 범위:** Series genre·CRUD·210:297 image·contents 연결/해제·전체 순서 저장과 일반 mutation pending 상태를 재대조했다. - **실행 증거:** Series를 포함한 도메인 묶음 36 files / 206 tests, exact server E2E 18/18, 현재 두 project mock E2E 109 passed / 5 skipped가 통과했다. - **판정:** `P9-R6`까지 포함한 Series 도메인 소유의 확정 신규 finding은 없다. - **교차 Phase:** Series image frame·좌표와 Blob URL 수명은 `REV-P1-016~017`/`P1-R10~R11`에서 수정한다. - **남은 위험:** 실제 개발 API Series mutation 수동 QA가 남아 있다. Safari/WebKit은 현재 지원 범위에서 제외한다. - **신규 Phase 5 Task:** 없음. ## 13. 요청 기준 재리뷰 — 2026-07-31 - **검토 범위:** genre lookup, CRUD, 210:297 crop, contents 연결·해제·전체 순서, 표시 label과 mutation pending. - **실행 증거:** Series unit과 mock 4-project CRUD·responsive·axe·order journey가 0 failure였고 OpenAPI `keyword`·`genreId`·`contentIdList`·`ids` 계약 대조도 통과했다. - **판정:** Phase 5 소유의 확정 신규 발견 사항 없음. - **남은 위험:** 실제 개발 API Series active-only·CRUD·연결·순서와 실기기 browser 수동 QA. - **신규 Phase 5 Task:** 없음. ## 14. 최종 재검증 — 2026-07-31 - **검토 범위:** Series genre·CRUD, crop, contents 연결/해제, 전체 순서와 mutation pending/error 상태. - **실행 증거:** 전체 unit 394 tests와 4-project mock Series·responsive·axe·order 시나리오가 0 failure였고 OpenAPI request/response schema 대조도 통과했다. - **판정:** Phase 5 소유의 확정 신규 발견 사항 없음. - **남은 위험:** 실제 개발 API Series active-only·CRUD·연결·순서와 실기기 browser 수동 QA. - **신규 Phase 5 Task:** 없음. ## 15. 2026-07-31 문서 기준 재리뷰 - **검토 범위:** Series genre lookup·CRUD·`210:297` crop·contents 연결/해제·전체 순서·pending/error를 `SERIES-001~018`·OpenAPI·`P5`·`P10-T3`에 대조했다. - **실행 증거:** Series route를 포함한 실패 후보 5-spec focused는 25 tests passed, Series contract·form·order·contents test는 full output에서 통과했다. type·lint·build·server E2E 36 tests도 통과했다. full unit 비결정성은 `REV-P9-009`/`P9-R9`, crop frame은 `REV-P1-019`/`P1-R13`으로 분리했다. - **판정:** Series 도메인 계약·UI 소유의 확정 신규 발견 사항 없음. 후보·오탐·보류 0건, 신규 Phase 5 Task 없음. - **남은 위험:** 실제 Series active-only·CRUD·연결·순서 수동 QA와 `P1-R13`, `P9-R9` 회귀가 남는다.