606 lines
31 KiB
Markdown
606 lines
31 KiB
Markdown
# 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 요구사항 충족, 추가 수정 없음.
|
|
|
|
**남은 항목:** 없음.
|