# Phase 4 시리즈 관리 리뷰 ## 1. 리뷰 정보 | 항목 | 내용 | |---|---| | 리뷰 대상 | Phase 4 / 시리즈 9개 operation | | 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 4~7 working tree | | 리뷰 일자 | 2026-07-28 | | 리뷰어 | Codex | | 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` | | 리뷰 상태 | 후속 수정 및 Gate 완료 | ## 2. 리뷰 목적과 범위 ### 목적 - PRD Feature D와 OpenAPI Series 9개 operation을 controller/facade/repository/test에 직접 대조한다. - 공개 route 수, soft delete 방식, owner 검증과 JSON request schema 경계를 점검한다. ### 포함 범위 - `src/main/kotlin/.../aicharacter/series/*` - `src/test/kotlin/.../aicharacter/series/*` - OpenAPI Series path/schema와 plan Phase 4 ### 제외 범위 - production 수정, legacy series API 변경, 테스트 실행 ## 3. 판정 기준 | 심각도 | 기준 | |---|---| | Blocker | 보안·소유권 우회 또는 데이터 손실 | | High | OpenAPI operation/path 위반 또는 주요 사용자 흐름 회귀 | | Medium | request schema·오류 계약의 제한된 위반 | | Low | 유지보수성 또는 문서 정합성 문제 | ## 4. 검토한 근거 | 근거 | 판정 | |---|---| | OpenAPI `:247`~`:389` | Series는 9개 operation이며 `/series/{seriesId}`에는 GET/PUT만 존재 | | `AiCharacterAdminSeriesController.kt:23`~`:112` | 10개 route를 노출하며 마지막 DELETE가 계약에 없음 | | `AiCharacterAdminSeriesFacade.kt:166`~`:187` | 별도 DELETE facade가 PUT과 같은 `isActive=false`를 구성 | | `AiCharacterAdminSeriesMutationTest.kt:201` 이후 | 계약 밖 DELETE 성공·거부 동작을 테스트가 고정 | | OpenAPI `:1099`~`:1109` | 콘텐츠 추가와 순서 변경 schema는 `additionalProperties: false` | | controller `:55`~`:82` | 두 request를 기본 `@RequestBody` DTO binding으로 수신 | | strict reader 검색 | multipart create/update에만 `FAIL_ON_UNKNOWN_PROPERTIES`가 있고 두 JSON body에는 없음 | ### 실행한 검증 | 명령 또는 수동 검증 | 결과 | 핵심 증거 | |---|---|---| | route/schema/호출 정적 대조 | 성공 | OpenAPI 9개 대비 controller 10개, strict JSON 경계 누락 확인 | | Gradle/컴파일/테스트 | 미실행 | 사용자 요청에 따라 실행하지 않음 | ## 5. 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-023` | High | 처리 완료 | OpenAPI에 없는 시리즈 DELETE route 노출 | `Task 4.7` | `P4-R1` | | `REV-024` | Medium | 처리 완료 | 콘텐츠 추가·순서 변경이 미지 JSON 필드를 허용 | `Task 4.7` | `P4-R1` | ## 6. 발견 사항 상세 ### REV-023 — OpenAPI에 없는 시리즈 DELETE route - **심각도:** High - **상태:** 처리 완료 - **관련 요구사항:** PRD Feature D, 공개 API schema 임의 변경 금지 - **관련 계약:** `/series/{seriesId}`는 GET과 PUT 수정/soft delete만 제공 - **소유 Task:** `Task 4.7`, `P4-R1` **관찰 내용** controller는 `DELETE /series/{seriesId}`를 추가로 노출한다. 같은 soft delete는 계약에 있는 PUT의 `isActive=false`로 이미 표현되므로 별도 route는 중복이자 공개 API 표면 확장이다. **영향** 서버와 OpenAPI 기반 client가 서로 다른 operation 집합을 사용한다. 현재 테스트도 계약 밖 route를 정상 동작으로 고정해 향후 차이를 지속시킨다. **권장 조치** DELETE controller/facade를 제거하고 기존 DELETE 성공 기대를 405 계약으로 교정하며 PUT `isActive=false` actual endpoint 테스트를 보강한다. **처리 결과** `DELETE /series/{seriesId}` controller/facade 경로를 제거했고, actual endpoint 테스트를 405와 `PUT isActive=false` soft delete 계약으로 교정했다. ### REV-024 — 두 JSON body의 `additionalProperties: false` 미적용 - **심각도:** Medium - **상태:** 처리 완료 - **관련 요구사항:** OpenAPI request schema 준수 - **관련 계약:** `SeriesContentAddRequest`, `SeriesOrderUpdateRequest` - **소유 Task:** `Task 4.7`, `P4-R1` **관찰 내용** multipart create/update는 facade strict reader를 사용하지만 콘텐츠 추가와 순서 변경은 기본 `@RequestBody` binding을 사용한다. repository 전역 설정에는 unknown property 실패 설정이 없어 두 schema의 계약 밖 필드를 무시하고 요청을 처리한다. 현재 malformed JSON 테스트는 문법 오류만 확인하고 미지 필드를 확인하지 않는다. **영향** 잘못된 client field가 성공으로 처리되어 계약 오류를 조기에 발견할 수 없고 mutation side effect까지 진행될 수 있다. **권장 조치** 두 body만 strict parse하고 미지 필드 요청의 400 `common.error.invalid_request`와 DB/event 0회를 actual endpoint로 고정한다. 전역 ObjectMapper 설정은 legacy/public 영향이 있으므로 변경하지 않는다. **처리 결과** 콘텐츠 추가와 순서 변경 body를 facade strict reader로 역직렬화하도록 변경했고, 미지 필드 요청의 400/no-side-effect 테스트를 추가했다. ## 7. plan·goal 전환 `plan-task.md` Phase 4의 `Task 4.7` / `P4-R1`과 `P4-R1-GATE`를 완료 처리했다. 기존 `P4-GATE` 완료 이력은 유지한다. ## 8. 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | OpenAPI operation 대조 | 충족 | Series controller mapping 9개, 계약 밖 `DELETE /series/{seriesId}` 제거 | | JSON schema 대조 | 충족 | content add/order body strict parse와 unknown-field 400 회귀 통과 | | ownership/soft delete 핵심 추적 | 충족 | facade owner 검증과 PUT soft delete 경로 존재 | | 실행 검증 | 충족 | focused 테스트, series/common 회귀, `ktlintCheck`, `git diff --check` 통과 | **최종 결론:** 후속 수정 및 Gate 완료 **남은 항목:** 없음. 다음 Goal은 `P5-R1`이다. ## 9. 2차 정적 리뷰 및 판정 — 2026-07-28 ### 리뷰 정보와 검증 범위 - 기준 commit/working tree: `2f93e2c9` + 현재 working tree - 기준 문서: PRD Feature D·API Expectations, plan Phase 4, OpenAPI Series 9개 operation - 검토 범위: 시리즈 생성 multipart binding, 콘텐츠 연결/해제 facade·repository·legacy service와 관련 테스트 - 검증 방식: controller → facade → repository/legacy service 상태 전이를 정적으로 추적했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. ### 추가 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-031` | Medium | 처리 완료 | soft-delete된 linked content를 시리즈에서 해제할 수 없음 | `Task 4.8` | `P4-R2` | | `REV-032` | Medium | 처리 완료 | 생성 필수 `image`가 nullable binding으로 공통 오류 계약 우회 | `Task 4.8` | `P4-R2` | ### REV-031 — soft-delete 콘텐츠의 기존 link 해제 차단 - **심각도:** Medium - **상태:** 처리 완료 - **관련 요구사항:** PRD Feature D의 콘텐츠 연결/해제와 동일 owner 검증 - **관련 계약:** 존재하는 owner link 해제 성공, 없는/cross-owner link만 400 - **소유 Task:** `Task 4.8`, `P4-R2` **관찰 내용** 해제 facade가 실제 link 존재 여부뿐 아니라 연결 추가/미연결 검색용 적격성 method를 호출한다. 이 method는 `duration != null && (isActive || releaseDate != null)`을 요구하므로, 정상 연결 후 콘텐츠가 soft delete되어 `isActive=false`, `releaseDate=null`이 되면 link가 존재해도 해제를 400으로 거부한다. **근거** - 코드: `AiCharacterAdminSeriesFacade.kt:109`~`:120`은 해제 전에 `findEligibleContentByIdAndCreatorMemberId`를 요구한다. - 코드: `AiCharacterAdminSeriesRepository.kt:28`~`:30`은 inactive/unreleased 콘텐츠를 제외한다. - 코드: legacy `CreatorAdminContentSeriesService.kt:297`~`:303`은 owner series의 link를 content 상태와 무관하게 제거한다. - 테스트: `AiCharacterAdminSeriesContentTest`는 정상 active link 해제와 inactive 콘텐츠 연결 거부를 분리 검증하지만, 이미 연결된 콘텐츠를 soft delete한 뒤 해제하는 상태 전이는 없다. **정적 재현 절차** 1. owner의 active content를 active series에 연결한다. 2. content를 soft delete해 `isActive=false`, `releaseDate=null`로 만든다. 3. 같은 owner/series/content로 DELETE unlink를 요청한다. 4. 실제 link는 남아 있지만 적격성 guard가 null을 반환해 400이 된다. **영향** 관리자는 삭제된 콘텐츠의 시리즈 연결을 정리할 수 없고 orphan link가 남는다. 콘텐츠 연결 추가 적격성과 기존 link 해제 조건이 불필요하게 결합된 문제다. **권장 조치** 해제는 series에 존재하는 link와 link 콘텐츠의 owner만 검증한다. `findEligibleContentByIdAndCreatorMemberId`는 연결 추가에만 유지하고 soft-delete link 해제 회귀를 추가한다. **처리 결과** 해제 경로에서 연결 추가·검색용 active/release/duration 적격성 guard를 제거하고, 실제 series link와 link 콘텐츠의 owner만 검증하도록 변경했다. soft-delete된 owner content의 기존 link 해제 성공 회귀를 추가했다. ### REV-032 — 시리즈 생성 필수 image의 nullable binding - **심각도:** Medium - **상태:** 처리 완료 - **관련 요구사항:** PRD API Expectations의 필수 multipart 누락 오류 - **관련 계약:** OpenAPI `SeriesCreateMultipart.required = ["image", "request"]` - **소유 Task:** `Task 4.8`, `P4-R2` **관찰 내용** OpenAPI는 생성 `image`를 필수로 선언하지만 controller와 facade는 nullable로 받아 누락 요청을 legacy service까지 전달한다. 결과는 400이지만 exact `MissingServletRequestPartException`과 `common.error.invalid_request` 대신 legacy `creator.admin.series.cover_image_required`가 된다. plan의 Phase 4 오류 표가 이 legacy 결과를 기록해 PRD 공통 계약과 충돌한다. **근거** - 계약: OpenAPI `SeriesCreateMultipart`의 required `image`. - 요구사항: PRD `:183`~`:184`는 필수 part 누락을 exact `MissingServletRequestPartException`, 400 `common.error.invalid_request`로 고정한다. - 코드: `AiCharacterAdminSeriesController.kt:83`~`:90`은 `required=false`, nullable image를 사용한다. - 코드: facade create와 legacy service는 null을 받아 domain key로 변환한다. - 테스트: `AiCharacterAdminSeriesMutationTest`는 missing image가 facade/legacy까지 도달하는 동작을 기대한다. **영향** 클라이언트는 같은 신규 prefix의 다른 필수 multipart 누락과 다른 message/exception 계약을 받는다. 상태는 400으로 같고 mutation은 시작되지 않으므로 영향은 오류 표면에 제한된다. **권장 조치** PRD/OpenAPI를 우선해 생성 image만 non-null binding으로 변경하고 세 locale의 exact exception/envelope과 facade/DB/S3/event 0회를 검증한다. update image의 optional 계약은 유지한다. **처리 결과** 시리즈 생성 `image` part를 non-null `MultipartFile` binding으로 변경해 누락 요청을 `MissingServletRequestPartException`, 400 `common.error.invalid_request`로 통일했다. KO/EN/JA envelope과 no-side-effect를 actual endpoint로 고정했고 update의 optional `image` 계약은 유지했다. **판정 기록** - 2026-07-28 — `REV-031`, `REV-032` 모두 코드·문서·테스트 정적 추적으로 확정. 테스트는 미실행. ### plan·goal 전환 `plan-task.md` Phase 4에 `Task 4.8` / `P4-R2`와 `P4-R2-GATE`를 추가했다. `DEC-P4-R2-001`로 생성 image 누락의 canonical 오류를 확정했으며 기존 완료 이력은 유지한다. `P4-R2`와 `P4-R2-GATE`를 완료 처리했다. ### 2차 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | operation/path 대조 | 충족 | Series 9개 mapping 유지 | | 상태 전이 대조 | 충족 | soft-delete linked content unlink 성공 회귀 통과 | | multipart binding | 충족 | required image 누락이 `MissingServletRequestPartException` 400으로 처리됨 | | plan 반영 | 충족 | `Task 4.8`, `P4-R2`, `P4-R2-GATE` | | 실행 검증 | 충족 | focused 테스트, series/common 회귀, `ktlintCheck`, `git diff --check` 통과 | **최종 결론:** 후속 수정 및 Gate 완료 **남은 항목:** 없음. 다음 Goal은 `P5-R2`다. ## 10. 3차 정적 리뷰 및 판정 — 2026-07-28 ### 리뷰 정보와 범위 - 기준 commit/working tree: `2f93e2c9` + 현재 working tree - 기준 문서: PRD Feature D, plan Phase 4, OpenAPI Series 9개 operation - 검토 범위: 생성·수정 multipart facade, legacy series service의 S3 upload, mutation 테스트 - 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다. ### 발견 사항 #### `REV-037` — Medium — 시리즈 빈 image의 0-byte upload - 생성 controller의 non-null binding은 part 누락만 차단하며 `MultipartFile.isEmpty`는 검증하지 않는다. - facade는 생성 image를 그대로 legacy service로 넘기고, legacy service는 size 0 metadata와 input stream을 S3에 업로드한 뒤 cover를 교체한다. - 수정의 optional 빈 image도 null이 아니므로 같은 0-byte upload와 기존 cover 교체가 발생한다. - 현재 테스트는 생성 image 누락과 정상 image 경로를 다루지만 생성·수정의 빈 part는 다루지 않는다. **권장 조치:** 생성 빈 image는 legacy 호출 전에 400으로 거부하고, 수정 빈 image는 optional part 생략으로 정규화한다. 정상 upload, 수정 image 생략, 빈 image no-S3/no-cover-change를 actual endpoint로 고정한다. ### plan·goal 전환 `plan-task.md` Phase 4에 `Task 4.9` / `P4-R3`과 `P4-R3-GATE`를 추가했다. | 판정 항목 | 결과 | 근거 | |---|---|---| | operation/path | 충족 | Series 9개 mapping 유지 | | 생성 empty file | 수정 필요 | 0-byte upload와 DB/event 부작용 발생 | | 수정 empty file | 수정 필요 | 기존 cover가 0-byte 객체로 교체됨 | | plan 반영 | 충족 | `Task 4.9`, `P4-R3`, `P4-R3-GATE` 추가 | | 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 | **최종 결론:** Phase 4 후속 수정 필요 **남은 항목:** `P3-R11-GATE` 후 `P4-R3` → `P4-R3-GATE`. ## 11. 3차 후속 수정 및 Gate — 2026-07-29 - 무엇을: `REV-037`을 처리했다. - 왜: 생성·수정의 빈 `image` part가 legacy service로 전달되어 0-byte S3 upload와 cover 교체를 유발했기 때문이다. - 어떻게: - RED: `AiCharacterAdminSeriesMutationTest`에 빈 생성 image 400/no-side-effect, 빈 수정 image cover 유지, 빈 image-only no_changes 유지 테스트를 추가했다. focused 실행에서 신규 3건이 실패했다. - GREEN: `AiCharacterAdminSeriesFacade.create`는 empty image를 400으로 거부하고, `update`는 empty image를 null로 정규화해 legacy service에 전달했다. - 검증: focused series mutation test, targeted aicharacter 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다. - 결과: `REV-037` 처리 완료. 생성 empty image는 부작용 전에 400이고, 수정 empty image는 생략으로 처리해 기존 cover를 유지한다. **최종 결론:** Phase 4 3차 리뷰 종결 **남은 항목:** 없음. ## 12. 4차 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 commit/working tree: `2f93e2c9` + 현재 working tree - 기준 문서: PRD Feature D, plan Phase 4, OpenAPI Series 9개 operation - 검토 범위: CRUD, 콘텐츠 조회·검색·연결·해제, owner-scoped 순서 변경, 최신 empty image 보완 - 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다. ### 발견 사항과 판정 Series runtime의 신규 확정 finding은 없다. 9개 controller mapping, owner-scoped lock·연결/해제와 생성·수정 empty image 정책은 현재 OpenAPI와 일치한다. 다만 plan Phase 4 endpoint 설명은 콘텐츠 해제를 `DELETE /series/{seriesId}/contents`와 request body로 적고 있어, OpenAPI와 실제 `DELETE /series/{seriesId}/contents/{contentId}` body 없음 route와 달랐다. 이를 `REV-039`로 확정했고, Phase 7 `Task 7.6` / `P7-R4`에서 설명을 정정했다. `Task 4.9` 헤더의 미완료 표기도 `REV-038`로 함께 동기화했다. | 판정 항목 | 결과 | 근거 | |---|---|---| | runtime operation | 충족 | Series controller mapping 9개와 OpenAPI 일치 | | ownership/순서 | 충족 | active owner ID 전체 검증과 ID 정렬 lock 유지 | | 연결/해제 | 충족 | owner link 검증 후 path `contentId`로 해제 | | 문서 계약 | 충족 | `REV-039`의 stale DELETE path/body 설명 정정 | | plan 전환 | 충족 | `Task 7.6`, `P7-R4` 완료 | **최종 결론:** Phase 4 기능 추가 수정 없음, Phase 7 문서 보완 완료 **남은 항목:** 없음. ## 13. 5차 정적 리뷰 및 판정 — 2026-07-29 ### 확인된 문제 #### `REV-042` — 시리즈 생성 primitive의 explicit null 허용 가능성 - **심각도:** Medium - **상태:** 처리 완료 - **계약:** OpenAPI `SeriesCreateRequest`의 `genreId`, `isAdult`은 nullable이 아니며 각각 생략 기본값 `0`, `false`를 가진다. - **구현:** `CreateSeriesRequest`의 두 값은 Kotlin primitive이고 facade strict reader는 미지 필드만 거부한다. - **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 explicit null은 primitive 기본값으로 보정될 수 있다. - **영향:** 특히 `isAdult: null`이 계약상 400 대신 `false`인 정상 생성으로 이어져 S3·DB·event mutation이 발생할 수 있다. ### 보완 결과 | 항목 | 판정 | |---|---| | 신규 Task | `Task 4.10` / `P4-R4` 처리 완료 | | 시작 조건 | `P3-R12-GATE` 완료 후 실행 | | Gate | `P4-R4-GATE` 완료 | | RED | `genreId: null`, `isAdult: null` actual POST가 보완 전 400 기대 실패 | | GREEN | v2 생성 strict reader에서 explicit null 거부, 필드 생략 기본값 유지 | | 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 | ### 실행 검증 | 명령 또는 검증 | 결과 | 핵심 증거 | |---|---|---| | `AiCharacterAdminSeriesMutationTest.shouldRejectNullPrimitiveCreateFieldsBeforeSideEffects` RED | 실패 확인 | 신규 2개 invocation이 400 기대 실패 | | 같은 focused RED/GREEN 명령 | 통과 | strict reader 보완 후 `BUILD SUCCESSFUL` | | `AiCharacterAdminSeriesMutationTest` | 통과 | 기존 생성 기본값·정상 mutation 회귀 유지 | | series/common 영향 범위 회귀 | 통과 | `BUILD SUCCESSFUL in 1m 12s` | | `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck`는 `BUILD SUCCESSFUL in 20s`, diff check 출력 없음 | **최종 결론:** Phase 4 `REV-042` 보완 완료 **다음 Goal:** `P5-R3`. ## 14. 장르 참조 API·상세 응답 후속 검토 — 2026-07-29 ### 확인 결과 - **`REV-046` / Medium / 구현 대기:** 시리즈 등록용 활성 장르 목록은 레거시 관리자 service/repository에 존재하지만 신규 v2 캐릭터 관리자 route에는 없다. 계약은 `orders` 오름차순의 `id`, `genre`, `isAdult` 직접 배열이다. - **`REV-047` / Medium / 정합화 대기:** 현재 시리즈 상세는 레거시 상세 DTO를 반환해 목록 item과 `publishedDaysOfWeek`, `genreId`, `state`, `isActive` 필드·타입이 다르다. - 승인된 상세 `data`는 목록 `items` 단일 객체와 동일한 11개 필드이며 기존 상세 전용 `genre`, `keywords`는 제거한다. ### plan 전환 - 장르 목록: `Task 4.11` / `P4-R5`, Gate `P4-R5-GATE` - 상세 정합화: `Task 4.12` / `P4-R6`, Gate `P4-R6-GATE` - 범위 밖: 장르 CRUD, 목록 wrapper 변경, legacy/public 상세 DTO 변경 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. **최종 결론:** Phase 4 장르 목록 구현과 시리즈 상세 정합화 필요 **다음 Goal:** `P4-R5`. ## 15. 장르 참조 API 구현 및 Gate — 2026-07-29 - 무엇을: `REV-046`을 처리했다. - 왜: 시리즈 등록 화면에서 사용할 활성 장르 목록이 레거시 관리자 API에는 있으나 신규 v2 캐릭터 관리자 route에는 없었기 때문이다. - 어떻게: - RED: `AiCharacterAdminSeriesGenreTest`에 활성/비활성 장르, `orders` 오름차순, 빈 목록, ADMIN 공통 경계 actual GET 테스트 2건을 추가했고 미구현 route로 실패했다. - GREEN: `AiCharacterAdminSeriesReferenceController`에 `GET /api/v2/admin/ai-characters/series-genres`를 추가하고 facade에서 기존 `AdminContentSeriesGenreService.getSeriesGenreList()`를 그대로 재사용했다. - Gate: focused 장르 테스트, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다. - 결과: `REV-046` 처리 완료. 별도 query, DTO, pagination, 장르 CRUD 변경은 추가하지 않았다. **최종 결론:** Phase 4 장르 목록 후속 기능 종결 **다음 Goal:** `P4-R6`. ## 16. 시리즈 상세 정합화 및 Gate — 2026-07-29 - 무엇을: `REV-047`을 처리했다. - 왜: 시리즈 상세가 레거시 상세 DTO를 반환해 목록 item과 field/type이 달랐기 때문이다. - 어떻게: - RED: `AiCharacterAdminSeriesQueryTest`에서 상세 `data`의 11개 목록 item field와 `genre`, `keywords` 부재를 고정했고 레거시 상세 응답 차이로 실패했다. - GREEN: v2 상세 response type을 `GetCreatorAdminContentSeriesListItem`으로 통일하고 facade에서 owner-scoped series를 동일 필드로 매핑했다. - Gate: focused query/contract, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행한다. - 결과: `REV-047` 처리 완료. legacy/public 상세 DTO와 mapper는 변경하지 않았다. **최종 결론:** Phase 4 시리즈 상세 정합화 종결 **다음 Goal:** `P5-R5`. ## 17. 6차 통합 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature D, OpenAPI Series 10개 operation - 검토 범위: Series/reference controller, facade/repository, JSON·multipart mapping과 목록·상세 DTO - 기준 상태: 현재 working tree - 검증 방식: 문서·코드·관련 테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. ### 확인 결과 | 항목 | 판정 | 근거 | |---|---|---| | route/operation | 충족 | 장르 참조를 포함한 Series 10개 OpenAPI operation과 실제 mapping 유지 | | request 전체 media type | 충족 | 생성·수정 multipart, 순서 변경·콘텐츠 추가 JSON `consumes` 선언 일치 | | query/default | 충족 | 목록·연결 콘텐츠 page/size 기본값과 미연결 검색 필수 query 일치 | | response/ownership | 충족 | 상세-목록 item schema 정합화와 target owner 경계 유지 | ### `REV-057` — High — 처리 완료 — Series multipart request part의 JSON media type 미강제 - OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다. - 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다. - 정상 테스트는 JSON media type만 사용하며 미지원/누락 part media type의 415 `Accept` header와 S3/DB/event no-side-effect를 고정하지 않는다. - 같은 shared converter와 signature에서 AudioContent의 `text/plain` 성공 테스트가 있어 permissive binding을 정적으로 확인할 수 있다. ### 처리 결과 - `AiCharacterAdminSeriesController`의 POST·PUT에 `MultipartHttpServletRequest`를 받고 Character·AudioContent와 같은 part header 검사와 `HttpMediaTypeNotSupportedException`을 적용했다. 기존 String strict reader와 facade/domain 로직은 변경하지 않았다. - `AiCharacterAdminSeriesMutationTest`는 POST·PUT 각각의 `text/plain`·Content-Type 누락 request part를 KO/EN/JA 415 `ApiResponse.error`, `Accept: application/json`, S3/DB/event no-side-effect로 고정했고, 필수 `request` part 누락의 기존 400 binding 오류도 확인했다. - RED는 신규 media type 12건이 415 기대와 달리 실패했고, GREEN focused 36건과 Series/common error 영향 범위 회귀, `ktlintCheck`, OpenAPI encoding 정적 대조, `git diff --check`를 통과했다. **최종 결론:** `REV-057`, `P4-R7`, `P4-R7-GATE` 처리 완료 **다음 Goal:** `P5-R7` (`P4-R7-GATE` 완료 후). ## 18. 7차 통합 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature D, OpenAPI `SeriesCreateMultipart`·`SeriesUpdateMultipart` - 검토 범위: Series POST·PUT controller의 multipart binding과 image/genre/owner mutation 테스트 - 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. ### `REV-062` — Medium — `image`, `request` 외 multipart part가 무시됨 - 두 Series multipart schema는 `additionalProperties: false`이며 허용 이름은 `image`, `request`다. - controller의 `requireJsonRequestPart()`는 `request` media type만 검사하고 전체 part 이름을 열거하지 않는다. - 정상 part와 미정의 part를 함께 보낸 요청이 facade로 전달될 수 있어 OpenAPI 입력 범위보다 runtime이 넓다. ### plan 전환 | 항목 | 내용 | |---|---| | 신규 Task | `Task 4.14` / `P4-R8` | | Gate | `P4-R8-GATE` | | RED | POST·PUT 미정의 part와 S3·DB·event 결과 | | GREEN | 실제 part 이름을 `{image, request}`와 비교해 초과 이름 400 | | 회귀 | 필수/빈 image, request part 415, genre/owner 경계 | **처리 결과 (2026-07-29 / P4-R8):** - Series POST·PUT controller 경계에 multipart part allow-list를 추가해 `image`, `request` 외 file part를 400 `common.error.invalid_request`로 거부했다. - RED에서 POST·PUT `unexpected` part KO/EN/JA 테스트 6개가 기존 mutation 경로로 실패함을 확인했고, GREEN 후 focused/영향 범위 회귀, `ktlintCheck`, `git diff --check`를 통과했다. **Gate 결과 (2026-07-29 / P4-R8-GATE):** - Focused multipart 회귀, Phase 4 Series 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다. - POST·PUT 미정의 part 400/no-side-effect, 정상·필수/빈 image, request part 415, genre/owner 경계가 유지됨을 확인했다. **최종 결론:** `REV-062` resolved. Phase 4 완료. **다음 Goal:** `P5-R9`. ## 19. 8차 통합 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature D, OpenAPI Series create/update multipart 및 장르 계약 - 검토 범위: Series controller/facade, active genre 조회 경계, legacy genre repository와 mutation 테스트 - 기준 상태: 현재 working tree - 리뷰어/상태: Codex / 판정 완료 - 검증 방식: 문서·코드·테스트 소스와 기존 compile output을 정적으로 대조했다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. ### `REV-067` — Medium — 일반 form-field multipart part가 allow-list 우회 - `AiCharacterAdminSeriesController.kt:114-117`은 `fileMap.keys`만 `{image, request}`와 비교한다. - 기존 `AiCharacterAdminSeriesMutationTest.kt:664-710`은 filename이 있는 `MockMultipartFile("unexpected", ...)`만 검증하므로 filename 없는 일반 form-field part 경계는 빠져 있다. - OpenAPI의 create/update multipart `additionalProperties: false`를 우회할 수 있어 Medium으로 확정한다. ### `REV-068` — Medium — `genreId <= 0`이 active genre 검사를 우회 - `AiCharacterAdminSeriesFacade.kt:155-168`은 create와 non-null update `genreId`에 공통 검사를 호출하지만, `:213-215`의 구현은 양수인 경우에만 active genre 존재를 확인한다. - OpenAPI create schema는 0이 domain validation에서 유효하지 않다고 명시한다. 그러나 0 또는 음수는 legacy service로 전달되고, `CreatorAdminContentSeriesGenreRepository.kt:19-26`의 QueryDSL `fetchFirst()` 결과를 Kotlin non-null 반환으로 취급하는 repository 경계에서 NPE가 발생할 수 있다. - 기존 compile output의 해당 repository bytecode를 `javap`로 확인한 결과 `fetchFirst()` 뒤 Kotlin `checkNotNullExpressionValue`가 존재했다. 공통 예상 밖 예외는 500 경로이므로 요청 오류 400 계약과 다르다. ### plan 전환 | finding | 신규 Task / Goal | Gate | 최소 보완 | |---|---|---|---| | `REV-067` | `Task 4.15` / `P4-R9` | `P4-R9-GATE` | servlet 전체 part 이름을 `{image, request}`와 대조 | | `REV-068` | `Task 4.16` / `P4-R10` | `P4-R10-GATE` | 0 이하 또는 비활성·미존재 장르를 legacy 호출 전에 400으로 거부 | **최종 결론:** Phase 4 보완 필요 — Medium 2건 확정 **다음 Goal:** `P4-R9` (`P3-R18-GATE` 완료 후), 이어서 `P4-R10`. ## 20. 8차 후속 수정 및 Gate — 2026-07-29 - 무엇을: `REV-067`의 Series POST·PUT filename 없는 multipart part 우회와 `REV-068`의 `genreId <= 0` 사전 거부 누락을 보완했다. - 왜: `{image, request}` 외 일반 form-field part와 0 이하 장르가 legacy service 호출 전 400으로 고정되어야 하기 때문이다. - 어떻게: `AiCharacterAdminSeriesMutationTest`에 filename 없는 part와 `genreId=0/-1` create·update KO/EN/JA actual endpoint 회귀를 추가하고, controller part 검사와 facade active genre guard를 최소 수정했다. - 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다. **최종 결론:** `REV-067`, `REV-068` 처리 완료. Phase 4 후속 Gate 완료. **남은 항목:** 없음. ## 21. 9차 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature E, OpenAPI Series 10개 operation - 검토 범위: 목록·상세·생성·수정·연결 콘텐츠·순서·장르, owner/active genre/multipart 경계 - 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. ### 판정 및 plan 전환 - Series 10개 operation과 controller/facade의 owner·active genre·exact multipart 경계를 대조했다. - 기존 완료 finding 이후 신규 확정 finding은 없다. - Phase 4 신규 Task/Gate 없음. **최종 결론:** Phase 4 추가 수정 없음. **남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정. ## 22. 10차 정적 리뷰 및 판정 — 2026-07-30 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature D, OpenAPI Series 10개 operation - 검토 범위: 장르·목록·상세·생성·수정, 연결 콘텐츠 조회·검색·추가·해제와 순서 변경 - 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. ### 판정 - Series 10개 operation과 controller mapping, 목록 item과 동일한 상세 schema가 일치한다. - active target/series·genre와 owner-scoped 콘텐츠 연결/해제, 순서 변경 lock·전체 ID 선검증이 유지된다. - 생성·수정의 필수/빈 image, strict JSON과 exact multipart part 경계가 OpenAPI와 일치한다. - 신규 확정 finding이 없어 Phase 4 회귀 수정 Task/Gate를 추가하지 않는다. **최종 결론:** Phase 4 요구사항 충족, 추가 수정 없음. **남은 항목:** 없음.