docs(ai-character): 관리자 API 계약과 검증 기록을 갱신한다

This commit is contained in:
2026-07-30 00:35:34 +09:00
parent d86a9fb059
commit 5a90fbf98e
11 changed files with 9179 additions and 287 deletions

View File

@@ -0,0 +1,310 @@
# Phase 1 공통 경계·보안 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / 공통 target resolver, 보안, 오류 경계 |
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 2~7 working tree |
| 리뷰 일자 | 2026-07-28 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
| 리뷰 상태 | 판정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- PRD Feature A와 공통 API Expectations가 현재 resolver/security/error 구현에 유지되는지 확인한다.
- Phase 2~6의 모든 신규 controller가 같은 prefix 경계와 target 불변식을 공유하는지 정적으로 추적한다.
### 포함 범위
- `AiCharacterAdminTargetResolver`, `SecurityConfig`, 신규 prefix 오류 handler/writer
- `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, resolver 관련 테스트
- OpenAPI 공통 오류·security 정의와 plan Phase 1 완료 기록
### 제외 범위
- 테스트 재실행, Phase 2~6 domain 세부 동작, legacy/public API 변경
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 인증·인가 우회, cross-owner write, 데이터 손실 위험 |
| High | PRD/OpenAPI 공통 보안·오류 계약 위반 |
| Medium | 제한된 경로의 오류·현지화·부작용 계약 누락 |
| Low | 유지보수성 또는 문서 정합성 문제 |
## 4. 검토한 근거
### 문서와 코드
- 요구사항: PRD Feature A, API Expectations, Acceptance Criteria
- 계획: Phase 1 `Task 1.1`~`Task 1.7`과 완료 증거
- 코드: `AiCharacterAdminTargetResolver.kt:17`~`35`
- 코드: `SecurityConfig.kt:118`~`130`, `:202`~`:207`
- 코드: `AiCharacterAdminExceptionHandler.kt:28`~`97`
- 테스트: resolver unit/integration, authorization, error contract 테스트
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| 코드·문서 정적 추적 | 성공 | creator role/kind 불변식, JWT authority + 현재 DB ADMIN 이중 인가, prefix 전용 오류 경계 확인 |
| Gradle/컴파일/테스트 | 미실행 | 사용자가 기존 통과 사실을 제공하고 직접 실행하지 말 것을 요청함 |
## 5. 발견 사항 요약
확정 발견 사항 없음.
## 6. 주요 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| target 해석 | 충족 | character와 creatorMember를 함께 조회하고 `CREATOR + AI_CHARACTER`를 검증 |
| ADMIN 이중 인가 | 충족 | `ROLE_ADMIN`, `MemberAdapter`, 현재 DB `Member.role == ADMIN`을 모두 요구 |
| prefix 오류 경계 | 충족 | security/MVC/fallback 오류가 신규 prefix 전용 handler로 연결됨 |
| Phase별 재사용 | 충족 | Character/Content/Series/Community/FanTalk facade가 공통 resolver를 사용 |
| 후속 Task 필요성 | 없음 | 정적 근거에서 신규 확정 finding이 발견되지 않음 |
## 7. plan·goal 전환
확정 finding이 없어 Phase 1 신규 Task나 Goal을 추가하지 않았다. 기존 Phase 1 완료 이력은 유지한다.
## 8. 리뷰 종료 판정
**최종 결론:** Phase 1 추가 수정 없음
**검증 제한:** 이번 판정은 정적 리뷰 결과다. 사용자 요청에 따라 테스트·컴파일을 재실행하지 않았으며 기존 plan의 통과
기록을 실행 증거로 재사용하지 않고 참고만 했다.
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature A·API Expectations, plan Phase 1, OpenAPI 공통 security/error
- 검토 범위: target resolver, SecurityConfig/WebConfig, JWT와 prefix 전용 security/MVC 오류 handler,
Phase 2~6 facade의 resolver 사용
- 검증 방식: 코드·문서·테스트 정적 추적. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER`, creatorMember 연결을 공통 resolver에서 검증 |
| ADMIN 이중 인가 | 충족 | JWT authority와 현재 DB role을 모두 확인 |
| 오류/CORS 경계 | 충족 | 신규 prefix 전용 handler와 허용 Origin 분리 유지 |
| Phase별 적용 | 충족 | Character/AudioContent/Series/Community/FanTalk facade가 resolver 사용 |
| plan 전환 | 해당 없음 | Phase 1 신규 확정 finding 없음 |
**최종 결론:** Phase 1 추가 수정 없음
**남은 항목:** 없음. `REV-030`~`REV-033`의 소유 Phase 보완 뒤 `P7-R2` 통합 재판정에 참여한다.
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature A·공통 오류, plan Phase 1, OpenAPI security/error
- 검토 범위: target resolver, ADMIN 이중 인가, 신규 prefix 오류·CORS, Phase 2~6 resolver 적용
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| target 불변식 | 충족 | creatorMember fetch와 `CREATOR + AI_CHARACTER` 검증 유지 |
| ADMIN 인가 | 충족 | JWT authority와 현재 DB role 이중 확인 유지 |
| 오류/CORS | 충족 | 신규 prefix 전용 handler와 전용 Origin 정책 유지 |
| Phase 적용 | 충족 | 각 domain facade가 공통 resolver를 통해 target을 해석 |
| plan 전환 | 해당 없음 | Phase 1 신규 Task 불필요 |
**최종 결론:** Phase 1 추가 수정 없음
**남은 항목:** `P7-R3`에서 공통 인가·오류 회귀를 통합 재검증한다.
## 11. 4차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature A·공통 오류, plan Phase 1, OpenAPI 공통 security/error
- 검토 범위: target resolver, ADMIN 이중 인가, 오류·CORS·firewall, 각 domain의 resolver 적용
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| target 불변식 | 충족 | fetch join과 `CREATOR + AI_CHARACTER` 검증 유지 |
| ADMIN 인가 | 충족 | JWT authority와 현재 DB role을 독립 확인 |
| 오류/CORS/firewall | 충족 | prefix 전용 handler와 legacy fallback 유지 |
| Phase 적용 | 충족 | Character~FanTalk facade가 공통 resolver 사용 |
| plan 전환 | 해당 없음 | Phase 1 신규 Task 불필요 |
**최종 결론:** Phase 1 추가 수정 없음
**남은 항목:** 없음.
## 12. 5차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위
- PRD·OpenAPI 공통 ADMIN 인가, target resolver, 오류·CORS·firewall 계약
- Phase 2~6 facade의 공통 resolver 사용과 JSON mapping 오류 변환 경계
- primitive nullability 보완을 전역 설정이 아닌 각 v2 request 경계에 둘 수 있는지
### 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER` 검증과 owner 전달 경로 유지 |
| ADMIN 인가 | 충족 | JWT authority와 현재 DB role의 이중 확인 유지 |
| 오류/CORS/firewall | 충족 | prefix 전용 error writer/handler와 허용 origin 정책 유지 |
| primitive finding 소유 | Phase 2~5 | 공통 mapper가 아니라 domain별 수동 `ObjectMapper` reader와 DTO에서 발생 |
| plan 전환 | 해당 없음 | 전역 Jackson·공통 계층 변경 없이 각 Phase Task로 분리 |
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않고 코드·계약만 정적으로 대조했다.
**최종 결론:** Phase 1 신규 수정 없음
**남은 항목:** 없음.
## 13. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD 공통 보안·오류·CORS 요구사항, OpenAPI 공통 response/security 계약
- 검토 범위: 신규 prefix의 security matcher, JWT authority와 DB role 이중 인가, target resolver,
공통 exception handler와 CORS 설정
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·관련 테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### 확인 결과
| 항목 | 판정 | 근거 |
|---|---|---|
| ADMIN 인가 | 충족 | 신규 prefix는 JWT `ROLE_ADMIN`과 현재 principal Member의 DB `ADMIN` role을 모두 확인 |
| target 불변식 | 충족 | `characterId`가 가리키는 creatorMember의 `CREATOR + AI_CHARACTER`를 공통 resolver에서 검증 |
| 오류/CORS | 충족 | prefix 전용 handler/writer와 승인된 Origin 범위 유지 |
| 6차 finding 영향 | 없음 | `REV-052`~`REV-058`은 domain controller의 query/media type 경계에 한정 |
### finding 및 plan 전환
- 신규 Phase 1 finding 없음.
- Phase 1 신규 Task/Gate 없음.
**최종 결론:** Phase 1 공통 보안·resolver·오류 경계 유지
**남은 항목:** 없음.
## 14. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD 공통 보안·오류·CORS 요구사항, OpenAPI 공통 security/error 계약
- 검토 범위: security matcher, JWT authority/현재 DB role 이중 인가, target resolver, 공통 exception/CORS 경계
- 검증 방식: 현재 working tree의 문서·코드·관련 테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| ADMIN 이중 인가 | 충족 | JWT `ROLE_ADMIN`과 현재 principal Member의 DB `ADMIN` role을 독립 확인 |
| target 불변식 | 충족 | `characterId` 대상의 `CREATOR + AI_CHARACTER` 검증과 owner 전달 경로 유지 |
| 공통 오류/CORS | 충족 | 신규 prefix 전용 handler와 승인 Origin 정책 유지 |
| 7차 finding 소유 | Phase 2~5·7 | multipart 이름 검증은 domain controller, 구현 상태 문서는 통합 Phase 소유 |
### finding 및 plan 전환
- 신규 Phase 1 finding 없음.
- Phase 1 신규 Task/Gate 없음.
**최종 결론:** Phase 1 추가 수정 없음
**남은 항목:** `P7-R9` 통합 재판정에 공통 오류·인가 회귀 근거로 참여한다.
## 15. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD 공통 보안·오류·CORS 요구사항, OpenAPI 공통 security/error 계약
- 검토 범위: security matcher, JWT authority/현재 DB role 이중 인가, target resolver, 공통 exception/CORS 경계
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·관련 테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| ADMIN 이중 인가 | 충족 | JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN`을 독립 확인하는 경계 유지 |
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER` 검증과 owner 전달 경로 유지 |
| 공통 오류/CORS | 충족 | 신규 prefix 전용 오류 envelope/i18n과 승인 Origin 정책 유지 |
| 8차 finding 소유 | Phase 2~7 | multipart 전체 part, Series 장르 ID, FanTalk 설명, 문서 상태 문제로 공통 경계 변경 불필요 |
### finding 및 plan 전환
- 신규 Phase 1 finding 없음.
- Phase 1 신규 Task/Gate 없음.
**최종 결론:** Phase 1은 요구사항과 일치하며 추가 수정이 없다.
**남은 항목:** Phase 2~7 후속 Goal 완료 뒤 `P7-R10` 통합 재판정에 공통 오류·인가 근거로 참여한다.
## 16. 9차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD 공통 인가·target resolver·오류·CORS 요구사항, OpenAPI 공통 security
- 검토 범위: security matcher, JWT/현재 DB role 이중 인가, target resolver, 신규 prefix 오류·CORS 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| ADMIN 이중 인가 | 충족 | JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN` 검증 경계 유지 |
| target 불변식 | 충족 | `CREATOR + AI_CHARACTER` 검증과 creator member 해석 경로 유지 |
| 오류·CORS | 충족 | 신규 prefix 전용 오류 envelope/i18n과 승인 Origin 경계 유지 |
| 신규 finding | 없음 | Phase 3 preview 검증 회귀는 공통 security/target 경계 변경 없이 소유 Phase에서 수정 가능 |
### plan 전환
- 신규 Phase 1 Task/Gate 없음.
**최종 결론:** Phase 1 추가 수정 없음.
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
## 17. 10차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature A, OpenAPI 공통 security/error 계약
- 검토 범위: 신규 prefix security matcher, JWT authority/현재 DB role 이중 인가, target resolver, 오류·CORS 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN`의 이중 인가가 신규 prefix보다 먼저 적용된다.
- target resolver는 `ChatCharacter.creatorMember``CREATOR + AI_CHARACTER` 불변식을 유지한다.
- prefix 전용 오류 envelope/i18n, 405 `Allow`, 415 `Accept`, 승인 Origin과 공유 로그인·로그아웃 CORS 경계가 유지된다.
- 신규 확정 finding이 없어 Phase 1 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 1 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.

View File

@@ -9,7 +9,7 @@
| 리뷰 일자 | 2026-07-27 |
| 리뷰어 | Sisyphus |
| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` |
| 리뷰 상태 | 판정 완료 |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
@@ -581,3 +581,433 @@ Endpoint Contract Summary를 source of truth로 사용하는 client는 제공
**최종 결론:** Phase 2 6차 리뷰 종결
**남은 항목:** `P3-R7``P3-R8``P3-R5-GATE`. Phase 4는 진행하지 않는다.
## 15. 7차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 Phase 2~7 working tree
- 기준 문서: PRD Feature B, `plan-task.md`, `api-contract.openapi.json`
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
- 검증 방식: controller/facade/mapper/test 호출을 정적으로 추적했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는
실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-021` | Low | 처리 완료 | mutation에서 사용하지 않는 전체 response mapping 수행 | `Task 2.12` | `P2-R6` |
### REV-021 — mutation의 미사용 response mapping
- **심각도:** Low
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature B, 레거시 mutation 성공 응답 유지
- **관련 계약:** Character POST/PUT의 성공 `data``null`
- **소유 Task:** `Task 2.12`, `P2-R6`
**관찰 내용**
controller는 create/update facade 반환값을 사용하지 않고 항상 `ApiResponse.ok(null)`을 반환한다. 그러나 facade는 두
mutation 마지막에 전체 `AiCharacterAdminCharacterResponse`를 생성한다. 외부 계약에 필요 없는 객체 그래프 mapping이
write 성공 뒤 추가 실패 지점과 유지보수 비용을 만든다.
**근거**
- 코드: `AiCharacterAdminCharacterController.kt:34`~`50`은 facade 호출 뒤 exact `data: null`을 반환한다.
- 코드: `AiCharacterAdminCharacterFacade.kt:62`, `:116`은 response DTO 반환형을 선언한다.
- 코드: 같은 파일 `:112`, `:163`은 controller가 버리는 `characterMapper.toResponse(...)`를 수행한다.
- 계약: OpenAPI Character mutation은 `NullSuccess`를 사용한다.
**권장 조치**
`P2-R6`에서 create/update facade 반환형을 `Unit`으로 축소하고 두 mapper 호출만 제거한다. controller와 공개 schema,
business pipeline은 변경하지 않고 기존 mutation exact `data: null` 회귀로 동작 불변을 확인한다.
### plan·goal 전환
`plan-task.md` Phase 2에 `Task 2.12` / `P2-R6``P2-R6-GATE`를 추가했다. 이전 완료 Task/Gate는 다시 열지 않는다.
### 7차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 문서·구현 대조 | 충족 | Character 4개 operation과 controller/facade 정적 추적 |
| 후보 판정 | 충족 | `REV-021` 확정 |
| plan 반영 | 충족 | `Task 2.12`, `P2-R6`, `P2-R6-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 compile/test 미실행 |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P2-R6` 실행 후 `P2-R6-GATE`에서 Phase 2를 재판정한다.
### P2-R6-GATE 종료 판정 — 2026-07-28
- 무엇을: `REV-021`의 캐릭터 POST/PUT 미사용 response mapping 제거를 최종 판정했다.
- 왜: mutation 성공 응답은 `data: null`인데 facade가 controller가 사용하지 않는 상세 DTO를 만들고 있었기 때문이다.
- 어떻게: `AiCharacterAdminCharacterFacade.create/update` 반환형을 `Unit`으로 축소하고 마지막 `characterMapper.toResponse(...)` 호출만 제거했다. 이후 mutation focused, character/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
- 결과: mutation focused는 `BUILD SUCCESSFUL in 1m 54s`, character/common 회귀는 `BUILD SUCCESSFUL in 1m 57s`, `ktlintCheck``BUILD SUCCESSFUL in 33s`, `git diff --check`는 출력 없음이었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-021` 처리 | 충족 | facade 반환형 축소와 미사용 mapper 호출 제거 |
| actual endpoint 회귀 | 충족 | POST/PUT mutation `data: null` focused test 성공 |
| 영향 범위 회귀 | 충족 | character/common 회귀, lint, diff check 성공 |
| 범위 준수 | 충족 | business pipeline, controller response, schema 변경 없음 |
**최종 결론:** Phase 2 7차 리뷰 종결
**남은 항목:** `P3-R9` 실행 후 `P3-R9-GATE`에서 Phase 3을 재판정한다.
## 16. 8차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature B, plan Phase 2, OpenAPI Character 4개 operation
- 검토 범위: controller/facade/DTO/mapper, 외부 API·S3·DB/event 순서와 관련 actual endpoint 테스트
- 검증 방식: 정적 호출·schema 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | Character 4개 mapping과 OpenAPI request/response 형태 일치 |
| target/ownership | 충족 | detail/update가 공통 resolver와 character target을 사용 |
| mutation 응답 | 충족 | POST/PUT `data: null`, 미사용 상세 mapping 제거 상태 유지 |
| 외부 부작용 경계 | 신규 finding 없음 | 기존 선검증·rollback/비보상 결정과 테스트 존재 |
| plan 전환 | 해당 없음 | Phase 2 신규 Task 불필요 |
**최종 결론:** Phase 2 추가 수정 없음
**남은 항목:** 없음. `P7-R2` 통합 재판정에서 기존 Character/common 회귀만 확인한다.
## 17. 9차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature B, plan Phase 2, OpenAPI Character 4개 operation
- 검토 범위: controller/facade/mapper, multipart 생성, optional update field와 레거시 controller/service parity
- 검증 방식: 정적 호출·schema·테스트 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항
#### `REV-034` — Medium — 캐릭터 생성의 빈 필수 image 허용
- OpenAPI `CharacterCreateMultipart``image`를 required로 선언한다.
- controller는 non-null part 존재까지만 강제하고, facade는 `image?.isEmpty == false`일 때만 upload한다.
- 빈 part는 외부 API 생성과 DB 저장을 먼저 수행한 뒤 image upload를 건너뛰므로 이미지 없는 캐릭터와 관련 부작용을 남긴다.
- 기존 mutation 테스트는 image part 누락은 다루지만 빈 part를 다루지 않는다.
**권장 조치:** facade 진입 직후 빈 image를 400 `common.error.invalid_request`로 거부하고 외부 API, DB, S3,
event 0회를 actual endpoint로 고정한다.
#### `REV-035` — Medium — `isActive=true` 단독 수정의 레거시 parity 위반
- OpenAPI `CharacterUpdateRequest.isActive`는 nullable boolean이며 `true`도 유효하다.
- 레거시 `AdminChatCharacterController.hasChanges``isActive != null`을 변경 요청으로 인정해 200
`data: null` pipeline을 수행한다.
- v2 mapper는 `request.isActive == false`만 external change로 인정하므로 `{"isActive":true}` 단독 요청이
facade no-change guard에서 400이 된다.
**권장 조치:** non-null `isActive`를 변경 요청으로 인정하되 기존 `false` 비활성화 의미와 일반 update pipeline은
변경하지 않는다.
### plan·goal 전환
`plan-task.md` Phase 2에 `Task 2.13` / `P2-R7``P2-R7-GATE`를 추가했다. 두 finding은 같은 facade/mapper와
mutation test 범위이므로 하나의 최소 보완 Task로 묶었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | route 유지 | Character 4개 mapping은 유지되나 두 runtime 의미 불일치 존재 |
| multipart create | 수정 필요 | 빈 required image가 외부·DB 부작용 뒤 무시됨 |
| update parity | 수정 필요 | `isActive=true` 단독 요청이 legacy와 다른 400 |
| plan 반영 | 충족 | `Task 2.13`, `P2-R7`, `P2-R7-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** Phase 2 후속 수정 필요
**남은 항목:** `P2-R7``P2-R7-GATE`.
## 18. 9차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-034`, `REV-035`를 처리했다.
- 왜: 빈 필수 `image`가 외부 생성·DB 저장 뒤 무시되고, `isActive=true` 단독 수정이 레거시와 달리 400으로 거부됐기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminCharacterControllerMutationTest`에 빈 image POST 400/no-side-effect와 `isActive=true` PUT 200 `data:null` actual endpoint 테스트를 추가했다. focused 실행에서 신규 2건이 실패했다.
- GREEN: create facade가 empty image를 400으로 거부하고, mapper가 non-null `isActive`를 변경 요청으로 인정하도록 최소 수정했다.
- 검증: focused mutation test, `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, 전체 `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
- 결과: `REV-034`, `REV-035` 처리 완료. 빈 image는 외부 API·DB·S3·event 전에 400으로 종료되고, `isActive=true` 단독 PUT은 200 `data:null`로 통과한다.
**최종 결론:** Phase 2 9차 리뷰 종결
**남은 항목:** 없음.
## 19. 10차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature B, plan Phase 2, OpenAPI Character 4개 operation
- 검토 범위: 목록·상세·생성·수정 controller/facade/mapper, 레거시 parity와 최신 `REV-034`~`REV-035` 보완
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
Character runtime의 신규 확정 finding은 없다. 빈 필수 image는 외부·DB·S3·event 전에 거부되고,
`isActive=true` 단독 수정은 레거시 200 `data: null` 의미를 유지한다.
`Task 2.13` 헤더가 `[ ]`로 남은 문제는 기능 문제가 아닌 전체 완료 상태 기록 불일치였고,
Phase 7 `REV-038` / `Task 7.6`에서 완료 상태로 동기화했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | Character 4개 route와 OpenAPI field·multipart 경계 일치 |
| ownership/인가 | 충족 | 목록 외 target route와 공통 ADMIN 이중 인가 유지 |
| mutation parity | 충족 | create/update/soft delete와 최신 empty/no-op 보완 유지 |
| Phase 2 기능 Task | 해당 없음 | 신규 production 수정 불필요 |
| 문서 상태 | 충족 | `REV-038`, `P7-R4`에서 완료 헤더 동기화 |
**최종 결론:** Phase 2 기능 추가 수정 없음
**남은 항목:** 없음.
## 20. 11차 정적 리뷰 및 판정 — 2026-07-29
### 확인된 문제
#### `REV-040` — 필수 관계 정수의 누락·null이 기본값으로 보정될 수 있음
- **심각도:** Medium
- **상태:** 처리 완료
- **계약:** OpenAPI `CharacterRelationship``importance`를 required non-null integer로 정의한다.
- **구현:** `ChatCharacterRelationshipRequest.importance`는 Kotlin `Int`이고, v2 character facade의 strict reader는
`FAIL_ON_UNKNOWN_PROPERTIES``FAIL_ON_NULL_FOR_PRIMITIVES`를 활성화한다.
- **근거:** 사용 중인 Jackson Kotlin/databind 2.13.5에서 `FAIL_ON_NULL_FOR_PRIMITIVES` 기본값은 비활성화되어
Kotlin/JVM primitive의 누락·null이 `0`으로 역직렬화될 수 있다.
- **영향:** 잘못된 관계 입력이 400으로 거부되지 않고 외부 캐릭터 생성·DB mutation으로 이어질 수 있다.
### 보완 계획
| 항목 | 판정 |
|---|---|
| 신규 Task | `Task 2.14` / `P2-R8` |
| Gate | `P2-R8-GATE` |
| RED | `importance` 누락·null actual POST와 외부 API·DB·S3·event 결과 |
| GREEN | v2 생성 경계의 primitive null/누락 400 변환 완료 |
| 범위 제한 | 전역 mapper·레거시 DTO·OpenAPI 변경 없음 |
### 처리 결과
- `AiCharacterAdminCharacterControllerMutationTest`에 관계 `importance` 누락·null actual POST와 외부 API·DB·S3·event no-side-effect 회귀를 추가했다.
- `AiCharacterAdminCharacterFacade.readRequest()``FAIL_ON_NULL_FOR_PRIMITIVES`를 추가해 전역 mapper·레거시 DTO·OpenAPI 변경 없이 v2 경계에서 400으로 변환했다.
- RED: 신규 2건은 보완 전 `status().isBadRequest` 기대에서 실패했다.
- GREEN/GATE: 보완 후 focused, character/common 영향 범위, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
**최종 결론:** Phase 2는 `REV-040` 처리 완료
**다음 Goal:** `P3-R12`.
## 21. 등록 참조 API 후속 검토 — 2026-07-29
### 확인 결과
- **`REV-044` / High / 구현 대기:** 캐릭터 등록용 원작 검색은 레거시
`AdminOriginalWorkController.search``AdminOriginalWorkService.searchOriginalWorksAll`에 존재하지만 신규 v2
캐릭터 관리자 route에는 없다.
- 검색 계약은 필수 `searchTerm`, 제목·콘텐츠 타입·카테고리 부분 검색, soft delete 제외, 무페이징
`OriginalWorkResponse` 직접 배열로 확정됐다.
- target 없는 reference endpoint지만 신규 prefix의 ADMIN 이중 인가·오류·CORS 경계는 동일하게 적용한다.
### plan 전환
- 신규 Task: `Task 2.15` / `P2-R9`
- Gate: `P2-R9-GATE`
- 범위 밖: 원작 CRUD, pagination·정렬 추가, 레거시 endpoint 변경
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 2 원작 검색 구현 필요
**다음 Goal:** `P2-R9`.
## 22. 12차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature B, OpenAPI Character 5개 operation
- 검토 범위: Character controller/facade/strict request reader, 원작 검색, 외부 API·S3·DB 선검증 경계
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·관련 테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### 확인 결과
| 항목 | 판정 | 근거 |
|---|---|---|
| route/operation | 충족 | Character 5개 OpenAPI operation과 실제 mapping 유지 |
| request 전체 media type | 충족 | 생성·수정 mapping은 multipart/form-data로 제한 |
| pagination/reference query | 충족 | 목록 기본값과 원작 검색 필수 `searchTerm` 계약 일치 |
| mutation 경계 | 충족 | strict JSON, 필수 image, target/원작 검증과 기존 외부/S3/DB 순서 유지 |
### `REV-055` — High — 처리 완료
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
- controller는 기존 `@RequestPart("request") request: String` strict reader 전달을 유지하면서 multipart part header에서
`application/json` 호환 여부를 확인한다.
- `text/plain`과 content type 누락은 `HttpMediaTypeNotSupportedException`으로 공통 415 오류 계약에 연결해
localized `ApiResponse.error``Accept: application/json`을 반환한다.
- POST·PUT actual endpoint의 KO/EN/JA 12개 case는 external API·S3·DB·event 부작용 없이 415를 반환하고,
request part 누락은 기존 400으로 유지한다.
### plan 전환
- 신규 Task: `Task 2.16` / `P2-R10`
- Gate: `P2-R10-GATE`
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
external/S3/DB/event no-side-effect
### `P2-R10` / `P2-R10-GATE` 완료 판정 — 2026-07-29
- RED: focused mutation test에서 새 `text/plain`·content type 누락 POST·PUT 12개 415 기대 case가 수정 전 실패했다.
- GREEN: controller의 part-level JSON compatibility 확인 뒤 기존 String payload를 facade strict reader에 그대로 전달했다.
- GATE: focused mutation test는 `BUILD SUCCESSFUL in 31s`, Character/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 2s`, `ktlintCheck``BUILD SUCCESSFUL in 18s`, `git diff --check`는 출력 없이 성공했다.
- 전체 `./gradlew test`는 controller part 경계와 직접 영향 Character/common 회귀를 실행했으므로 생략했다.
**최종 결론:** `REV-055` 처리 완료, Phase 2 완료.
**다음 Goal:** `P3-R15`.
## 23. 13차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature B, OpenAPI `CharacterCreateMultipart`·`CharacterUpdateMultipart`
- 검토 범위: Character POST·PUT controller의 multipart binding·part media type 검사와 mutation 테스트
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### `REV-060` — Medium — 미정의 multipart part를 무시하고 mutation 진행
- OpenAPI의 두 Character multipart schema는 `additionalProperties: false`이고 허용 이름을 `image`, `request`
한정한다.
- controller는 선언된 `@RequestPart`를 binding하고 `request``application/json` 여부만 확인한다.
`MultipartHttpServletRequest`의 전체 part 이름 집합은 검사하지 않는다.
- 따라서 정상 `image`·`request``unexpected` part를 함께 보내도 추가 part는 무시되고 create/update facade가
실행될 수 있다.
- 잘못된 입력이 성공 mutation으로 이어지므로 계약 정합성 문제로 확정하되, 추가 part 자체를 사용하거나 저장하지는
않으므로 심각도는 Medium으로 판정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 2.17` / `P2-R11` |
| Gate | `P2-R11-GATE` |
| RED | POST·PUT의 미정의 file/text part와 외부 API·S3·DB·event 결과 |
| GREEN | 실제 part 이름을 `{image, request}`와 비교해 초과 이름을 공통 400으로 거부 |
| 범위 제한 | OpenAPI·legacy/public·전역 multipart resolver 변경 없음 |
### `P2-R11` / `P2-R11-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminCharacterControllerMutationTest`에 Character POST·PUT의 `unexpected` multipart part KO/EN/JA 400/no-side-effect actual endpoint test를 추가했고, 기존 구현은 6개 invocation 모두 400 기대 대비 200으로 실패했다.
- GREEN: `AiCharacterAdminCharacterController`에서 `MultipartHttpServletRequest.fileMap.keys``{image, request}`의 부분집합인지 확인하고 초과 part를 400 `common.error.invalid_request`로 거부했다.
- Gate: undefined part, 기존 request part 415, request part 누락 400 focused 회귀는 `BUILD SUCCESSFUL in 1m 41s`, Character/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 36s`, `ktlintCheck``BUILD SUCCESSFUL in 30s`, `git diff --check`는 출력 없이 성공했다.
**최종 결론:** `REV-060` 처리 완료, Phase 2 13차 리뷰 종결.
**다음 Goal:** `P3-R17`.
## 24. 14차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: OpenAPI `CharacterCreateMultipart`·`CharacterUpdateMultipart`
`additionalProperties: false`, 허용 part `{image, request}`
- 검토 범위: Character POST·PUT controller의 part allow-list와 미정의 part 회귀 테스트
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-065` — Medium — 일반 form-field multipart part가 allow-list 우회
- `AiCharacterAdminCharacterController.kt:71-74``MultipartHttpServletRequest.fileMap.keys`만 검사한다.
filename 없는 일반 form-field part는 file map 대상이 아니므로 `{image, request}` 외 이름을 검출하지 못한다.
- 로컬 의존성 Spring Web 5.3.29의 `StandardMultipartHttpServletRequest.parseRequest` bytecode도 filename이 있는
part만 multipart file map에 넣고, filename이 없는 part 이름은 별도 parameter 집합에 넣음을 확인했다.
- 기존 `AiCharacterAdminCharacterControllerMutationTest.kt:424-469`는 filename이 있는
`MockMultipartFile("unexpected", ...)`만 사용해 이 경계를 고정하지 않는다.
- 따라서 `additionalProperties: false` 계약을 일반 form-field part가 우회해 facade mutation으로 진행할 수 있다.
입력 자체를 저장하지는 않지만 계약 위반과 부작용 가능성이 있어 Medium으로 판정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 2.18` / `P2-R12` |
| Gate | `P2-R12-GATE` |
| RED | filename 없는 `unexpected` part의 POST·PUT 400/no-side-effect |
| GREEN | servlet 전체 part 이름을 `{image, request}`와 비교 |
| 범위 제한 | Character controller/test만 최소 변경, OpenAPI·전역 resolver·legacy/public 변경 없음 |
**최종 결론:** Phase 2 보완 필요 — `REV-065` 확정
**다음 Goal:** `P2-R12`.
## 24. 8차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-065`의 Character POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
- 왜: 기존 `fileMap.keys` 검사만으로는 `{image, request}` 외 일반 form-field part를 mutation 전 거부하지 못했기 때문이다.
- 어떻게: `AiCharacterAdminCharacterControllerMutationTest``shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects` KO/EN/JA POST·PUT actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하게 했다.
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
**최종 결론:** `REV-065` 처리 완료. Phase 2 후속 Gate 완료.
**남은 항목:** 없음.
## 25. 15차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI Character 5개 operation
- 검토 범위: 목록·원작 검색·상세·생성·수정, target/owner, multipart·strict JSON, external/S3/event 순서
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- Character 5개 OpenAPI operation과 controller mapping, target/owner 선검증, strict multipart/JSON 경계를 대조했다.
- 기존 완료 finding 이후 새 계약 불일치나 확정 가능한 production 결함은 확인되지 않았다.
- Phase 3의 `REV-072`는 Character 경로에 영향을 주지 않는다.
### plan 전환
- 신규 Phase 2 finding 및 Task/Gate 없음.
**최종 결론:** Phase 2 추가 수정 없음.
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
## 26. 16차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature B, OpenAPI Character 5개 operation
- 검토 범위: 목록·원작 검색·상세·생성·수정, AI target, strict multipart/JSON, external API·S3·DB·event 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- Character 5개 operation과 controller mapping, active AI 목록 필터와 target 없는 원작 검색 계약이 일치한다.
- 생성·수정은 exact multipart part와 JSON media type, 필수/nullable·미지 필드, 원작·타입·중복 선검증을 유지한다.
- external API, S3, DB와 event의 기존 호출·rollback/비보상 경계가 계획의 특성화 결과와 일치한다.
- 신규 확정 finding이 없어 Phase 2 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 2 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.

View File

@@ -9,7 +9,7 @@
| 리뷰 일자 | 2026-07-27 |
| 리뷰어 | Sisyphus |
| 기준 문서 | `docs/20260724_AI캐릭터_관리자_API/prd.md`, `docs/20260724_AI캐릭터_관리자_API/plan-task.md` |
| 리뷰 상태 | 판정 완료 |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
@@ -726,3 +726,622 @@ production code는 변경하지 않는다.
**최종 결론:** Phase 3 6차 리뷰 종결
**남은 항목:** 없음. 다음은 사용자 진행 지시 후 `P4-T1`이다.
## 15. 7차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 Phase 2~7 working tree
- 기준 문서: PRD Feature C, `plan-task.md`, `api-contract.openapi.json`
- 리뷰 상태: 판정 완료, 후속 수정 goal 필요
- 검증 방식: production/test 전체 호출 검색과 repository/DTO 정적 추적을 수행했다. 사용자 요청에 따라 Gradle,
컴파일, 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-022` | Low | 처리 완료 | 관리자 content repository에 호출되지 않는 확장 코드 잔존 | `Task 3.19` | `P3-R9` |
### REV-022 — 호출되지 않는 repository 확장과 전용 enum
- **심각도:** Low
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C
- **관련 계약:** 현재 5개 AudioContent operation의 실제 조회/생성/수정 경계
- **소유 Task:** `Task 3.19`, `P3-R9`
**관찰 내용**
현재 facade가 사용하는 관리자 전용 repository method는 owner-scoped 상세 조회
`findByIdAndCreatorMemberId` 하나다. 동일 repository의 page 조회, series ID 조회/교체, 활성 series 검사와 private helper는
production/test 호출자가 없고, `AiCharacterAdminAudioContentStatus`도 이 미사용 코드에서만 참조된다.
**근거**
- 코드: `AiCharacterAdminAudioContentRepository.kt:50`~`56`의 owner-scoped 상세 조회는 실제 facade 호출 대상이다.
- 코드: 같은 파일 `:25`~`:48`, `:58`~`:142`의 나머지 public/private method는 전체 호출 검색 결과 외부 참조가 없다.
- 코드: `AiCharacterAdminAudioContentDto.kt:28`~`31`의 status enum은 위 미사용 page query에서만 참조된다.
**권장 조치**
`P3-R9`에서 실제 사용 중인 상세 조회만 보존하고 호출 0건 method, helper, enum 및 그로 인해 unused가 된 import만 제거한다.
legacy repository나 콘텐츠/series 동작은 변경하지 않고 상세·ownership 회귀로 동작 불변을 확인한다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.19` / `P3-R9``P3-R9-GATE`를 추가했다. 이전 완료 Task/Gate는 다시 열지 않는다.
### 7차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 문서·구현 대조 | 충족 | AudioContent 5개 operation과 facade/repository 호출 정적 추적 |
| 후보 판정 | 충족 | `REV-022` 확정 |
| plan 반영 | 충족 | `Task 3.19`, `P3-R9`, `P3-R9-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 compile/test 미실행 |
**최종 결론:** 수정 goal 필요
**남은 항목:** `P2-R6-GATE``P3-R9`를 실행하고 `P3-R9-GATE`에서 Phase 3을 재판정한다.
### P3-R9-GATE 종료 판정 — 2026-07-28
- 무엇을: `REV-022`의 관리자 오디오 repository 미사용 확장 제거를 최종 판정했다.
- 왜: 실제 facade 호출 대상은 owner-scoped 상세 조회 하나뿐이고, 나머지 page/series helper와 전용 status enum은 현재 5개 AudioContent operation에 쓰이지 않기 때문이다.
- 어떻게: `AiCharacterAdminAudioContentRepository`에서 상세 조회 외 method와 helper를 제거하고 `AiCharacterAdminAudioContentStatus`를 삭제했다. package-scoped 호출 검색, 상세·ownership focused, content/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다.
- 결과: 대상 package 호출 검색은 출력이 없었다. 상세·ownership focused는 `BUILD SUCCESSFUL in 3m 39s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck``BUILD SUCCESSFUL in 31s`, `git diff --check`는 출력 없음이었다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-022` 처리 | 충족 | 미사용 repository method/helper/status enum 제거 |
| static 호출 검색 | 충족 | 대상 admin content package에서 제거 대상 호출 0건 |
| owner-scoped 상세 회귀 | 충족 | detail/ownership focused test 성공 |
| 영향 범위 회귀 | 충족 | content/common 회귀, lint, diff check 성공 |
| 범위 준수 | 충족 | legacy repository/service, 콘텐츠·시리즈 동작 변경 없음 |
**최종 결론:** Phase 3 7차 리뷰 종결
**남은 항목:** `P4-R1` 실행 후 `P4-R1-GATE`에서 Phase 4를 재판정한다.
## 16. 8차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C·API Expectations, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 생성 request strict parse와 legacy `AudioContentService` 호출, Java time 변환, prefix 예외 handler,
생성 actual endpoint 테스트
- 검증 방식: 요청값 → facade → legacy service → 예외 handler와 side-effect 순서를 정적으로 역추적했다.
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-030` | Medium | 처리 완료 | 잘못된 생성 날짜·시간대가 client 오류가 아닌 500으로 반환됨 | `Task 3.20` | `P3-R10` |
### REV-030 — 생성 날짜·시간대 의미 오류가 500으로 분류됨
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature C, API Expectations의 request/domain 오류 400
- **관련 계약:** `AudioContentCreateRequest.releaseDate``yyyy-MM-dd HH:mm`, `timezone`은 시간대 ID
- **소유 Task:** `Task 3.20`, `P3-R10`
**관찰 내용**
facade의 strict reader는 JSON 문법·타입·미지 필드만 검증하고 파싱한 request를 버린다. 이후 legacy service가
`releaseDate``LocalDateTime`으로 변환하고 `ZoneId.of(timezone)`을 호출한다. 잘못된 값은
`DateTimeParseException`/`ZoneRulesException``DateTimeException`으로 빠지며, prefix handler는 이를 client 오류로
분류하지 않아 500 `common.error.unknown`을 반환한다.
**근거**
- 코드: `AiCharacterAdminAudioContentFacade.kt:65`~`:80`은 strict parse 뒤 같은 raw JSON을 legacy service에 전달한다.
- 코드: `AudioContentService.kt:226`~`:230`에서 날짜 형식과 `ZoneId`를 변환한다.
- 코드: `AiCharacterAdminExceptionHandler.kt:55`~`:71``DateTimeException`을 400 분기에 포함하지 않는다.
- 계약: OpenAPI `AudioContentCreateRequest``releaseDate` 설명과 기본 `timezone`.
- 계획: 기존 `Task 3.13`은 invalid date 400 증거를 완료 조건으로 적었지만 실제 테스트는 현재 계약 밖
`releaseDateUtc` 미지 필드만 검증한다.
- 테스트: 현재 생성 테스트에는 유효한 날짜·시간대와 제거된 `releaseDateUtc`만 있고 실제 `releaseDate` 형식·`timezone`
의미 오류가 없다.
**정적 재현 절차**
1. 유효한 `coverImage`, `contentFile`, theme과 필수 JSON field를 준비한다.
2. `releaseDate="not-a-date"` 또는 `timezone="Invalid/Zone"`으로 생성 요청을 보낸다.
3. strict JSON parse는 통과하고 legacy Java time 변환이 예외를 던진다.
4. 현재 handler 분류는 500이며 요구 결과는 side effect 없는 400 `common.error.invalid_request`다.
**영향**
형식상 JSON은 맞지만 의미가 잘못된 client 입력이 서버 장애로 기록·응답된다. 실제 업로드 이전에 실패하므로 현재 경로의
DB/S3/event 변경 가능성은 낮지만, 오류 계약과 운영 장애 지표가 왜곡된다.
**권장 조치**
공통 handler를 넓히지 말고 facade가 strict parse한 생성 DTO를 재사용해 날짜 형식과 `ZoneId`만 legacy 호출 전에 검증한다.
두 입력의 KO/EN/JA 400과 DB/S3/event 0건을 actual endpoint로 고정한다.
**판정 기록**
- 2026-07-28 — 코드·OpenAPI·handler·테스트 정적 추적으로 확정. 테스트는 사용자 요청에 따라 미실행.
- 2026-07-28 — `P3-R10`에서 `AiCharacterAdminAudioContentCreateTest``releaseDate="not-a-date"`
`timezone="Invalid/Zone"`의 KO/EN/JA actual endpoint matrix를 추가했다. RED는 신규 6건이 400 기대 assertion에서 실패했고,
facade가 strict parse 결과의 `releaseDate`/`timezone`을 legacy service 호출 전에 Java time API로 검증하도록 수정한 뒤
create focused, create+controller focused, content/common 영향 범위 회귀와 `ktlintCheck`가 모두 성공했다. production 변경은
v2 audio content facade 경계에 한정했고 OpenAPI schema와 legacy `AudioContentService`는 변경하지 않았다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.20` / `P3-R10``P3-R10-GATE`를 추가했다. 기존 완료 Task/Gate는 유지한다.
### 8차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema 대조 | 충족 | AudioContent 5개 mapping과 OpenAPI schema 유지 |
| 후보 판정 | 충족 | `REV-030` 원인·오류 분류·테스트 공백 확인 및 처리 완료 |
| plan 반영 | 충족 | `Task 3.20`, `P3-R10`, `P3-R10-GATE` |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** `REV-030` 처리 완료, Phase 3 8차 리뷰 종결
**남은 항목:** 없음. 다음은 `P4-R2` 실행 후 Phase 4를 재판정한다.
### P3-R10-GATE 종료 판정 — 2026-07-28
- 무엇을: 오디오 생성의 잘못된 `releaseDate` 형식과 `timezone` 의미 오류를 400 `common.error.invalid_request`로 복구했다.
- 왜: 형식상 JSON은 유효하지만 의미가 잘못된 client 입력이 legacy Java time 변환까지 내려가 500으로 반환되는 계약 위반을 막기 위해서다.
- 어떻게: v2 facade에서 strict parse 결과를 재사용해 `yyyy-MM-dd HH:mm``ZoneId`를 legacy service 호출 전에 검증했다.
- 결과: RED는 신규 6건 실패로 재현됐고, GREEN 후 create focused는 `BUILD SUCCESSFUL in 1m 4s`, create+controller focused는
`BUILD SUCCESSFUL in 1m 16s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`
`BUILD SUCCESSFUL in 38s`였다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-030` 처리 | 충족 | invalid `releaseDate`/`timezone` actual endpoint KO/EN/JA 400 추가 |
| side-effect 차단 | 충족 | DB count, S3 putObject 0회, event no-interaction 단언 |
| 영향 범위 회귀 | 충족 | content/common 회귀와 lint 성공 |
| 범위 준수 | 충족 | OpenAPI schema, legacy service, upload/processing pipeline 변경 없음 |
**최종 결론:** Phase 3 8차 리뷰 종결
**남은 항목:** 없음. 다음은 `P4-R2`다.
## 17. 9차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 상세 facade/mapper, 레거시 상세 response 파생 규칙, 예약일·locale·signed URL 테스트
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항
#### `REV-036` — High — 미래 예약 콘텐츠 상세의 releaseDate 소실
- OpenAPI 상세 response는 nullable `releaseDate`를 레거시 `GetAudioContentDetailResponse` 필드로 유지한다.
- 레거시 `AudioContentService`는 미래 예약일을 UTC에서 Asia/Seoul로 변환하고
`content.release_date.format`의 KO/EN/JA 형식 문자열을 반환하며, 공개 시각이 지나면 null을 반환한다.
- v2 `AiCharacterAdminAudioContentMapper.toResponse`는 콘텐츠 상태와 locale에 관계없이 `releaseDate = null`
고정한다.
- 현재 상세 테스트의 예약일은 점검일보다 과거라 null 분기만 검증해 미래 분기 누락을 발견하지 못한다.
**영향:** 예약 공개 전 관리자 상세에서 공개 예정 시각이 숨겨지고 레거시 response 의미와 OpenAPI 이관 원칙을 위반한다.
**권장 조치:** 기존 `SodaMessageSource``LangContext`를 사용해 미래 여부, UTC→Asia/Seoul 변환, locale별 포맷을
mapper에 최소 이관하고 미래·과거 KO/EN/JA actual endpoint를 고정한다.
### plan·goal 전환
`plan-task.md` Phase 3에 `Task 3.21` / `P3-R11``P3-R11-GATE`를 추가했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | route 유지 | AudioContent 5개 mapping과 response 필드는 존재 |
| 미래 예약일 | 수정 필요 | mapper가 `releaseDate`를 무조건 null로 설정 |
| 과거 예약일 | 충족 | null 반환은 레거시 의미와 일치 |
| plan 반영 | 충족 | `Task 3.21`, `P3-R11`, `P3-R11-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** Phase 3 후속 수정 필요
**남은 항목:** `P2-R7-GATE``P3-R11``P3-R11-GATE`.
## 18. 9차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-036`을 처리했다.
- 왜: 미래 예약 콘텐츠 상세의 `releaseDate`가 항상 null이라 레거시 locale별 공개 예정 시각을 숨겼기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentQueryTest`에 미래 예약일 KO/EN/JA와 과거 null actual endpoint 테스트를 추가했다. focused 실행에서 미래 3개 locale이 null 반환으로 실패했다.
- GREEN: `AiCharacterAdminAudioContentMapper``SodaMessageSource`, `LangContext`를 사용해 레거시 `content.release_date.format`과 UTC→Asia/Seoul 변환을 적용하도록 최소 수정했다.
- 검증: focused query test, targeted aicharacter 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/mapping/diff 점검을 실행했다.
- 결과: `REV-036` 처리 완료. 미래 예약일은 KO/EN/JA 형식 문자열로 반환하고 과거 예약일은 null을 유지한다.
**최종 결론:** Phase 3 9차 리뷰 종결
**남은 항목:** 없음.
## 19. 10차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature C, plan Phase 3, OpenAPI AudioContent 5개 operation
- 검토 범위: 테마·목록·상세·생성·수정 facade/mapper/repository, signed URL과 최신 예약일 보완
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
AudioContent runtime의 신규 확정 finding은 없다. owner-scoped 상세, 레거시 목록/생성/수정 DTO,
빈 파일 경계, signed URL과 미래·과거 KO/EN/JA 예약일 의미가 유지된다.
`Task 3.21` 헤더가 `[ ]`로 남은 문제는 Phase 7 `REV-038` / `Task 7.6`에서 완료 상태로 동기화했다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | AudioContent 5개 route와 OpenAPI field·multipart 경계 일치 |
| ownership | 충족 | target owner content만 상세·수정 가능 |
| signed URL/private path | 충족 | 공통 CloudFront 정책 재사용과 private path 비노출 |
| 예약 공개일 | 충족 | 미래 locale별 표시, 과거 null 유지 |
| 문서 상태 | 충족 | `REV-038`, `P7-R4`에서 완료 헤더 동기화 |
**최종 결론:** Phase 3 기능 추가 수정 없음
**남은 항목:** 없음.
## 20. 11차 정적 리뷰 및 판정 — 2026-07-29
### 확인된 문제
#### `REV-041` — 오디오 생성 primitive의 required·null 계약 미강제
- **심각도:** High
- **상태:** 처리 완료
- **계약:** OpenAPI `AudioContentCreateRequest``price`를 required non-null integer로 정의하고,
`themeId`, 각 boolean primitive도 nullable로 선언하지 않는다.
- **구현:** `CreateAudioContentRequest`의 해당 값은 Kotlin primitive이며, v2 content facade의 strict reader는
`FAIL_ON_UNKNOWN_PROPERTIES`, `FAIL_ON_NULL_FOR_PRIMITIVES`, `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 활성화한 뒤 원본 JSON을 legacy service로 전달한다.
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 primitive 누락·null은 `0`/`false`로 보정될 수 있다.
- **영향:** 필수 `price` 누락·null이 400 없이 생성으로 이어질 수 있고, `isFullDetailVisible` 같은 필드의 explicit null은
문서·DTO의 생략 기본값과 다른 값으로 처리될 수 있다.
### 보완 계획
| 항목 | 판정 |
|---|---|
| 신규 Task | `Task 3.22` / `P3-R12` |
| 시작 조건 | `P2-R8-GATE` |
| Gate | `P3-R12-GATE` |
| RED | required `price` 누락·null, non-null primitive null과 S3·DB·event 무변경 |
| GREEN | v2 생성 경계의 primitive null/누락 400 변환과 optional 생략 기본값 유지 완료 |
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
### 처리 결과
- `AiCharacterAdminAudioContentCreateTest``price` 누락·null, primitive field explicit null actual POST와 파일 업로드·DB·event no-side-effect 회귀를 추가했다.
- `AiCharacterAdminAudioContentFacade.readRequest()``FAIL_ON_NULL_FOR_PRIMITIVES``FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가해 전역 mapper·레거시 service·OpenAPI 변경 없이 v2 경계에서 400으로 변환했다.
- RED: 신규 8개 invocation은 보완 전 `status().isBadRequest` 기대에서 실패했다. `themeId:null`은 기존 missing-theme guard로 이미 400이었다.
- GREEN/GATE: 보완 후 focused, content/common 영향 범위, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
**최종 결론:** Phase 3는 `REV-041` 처리 완료
**다음 Goal:** `P4-R4`.
## 21. 오디오 콘텐츠 댓글 후속 검토 — 2026-07-29
### 확인 결과
- **`REV-045` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글
조회/작성/수정/삭제 5개 operation이 없다.
- 조회는 필수 `timezone``page`, `size`, 레거시 `totalCount/items`를 유지한다.
- 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 콘텐츠의 row를
작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다.
- 답글 `parentId`는 같은 콘텐츠의 활성 원댓글이어야 한다.
### plan 전환
- 신규 Task: `Task 3.23` / `P3-R13`
- Gate: `P3-R13-GATE`
- 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 CRUD 구현 필요
**다음 Goal:** `P3-R13`.
## 22. 오디오 콘텐츠 댓글 구현 및 Gate — 2026-07-29
- 무엇을: `REV-045`를 처리했다.
- 왜: 신규 v2 관리자 경계에 target 소유 오디오 콘텐츠의 댓글·답글 조회/작성/수정/삭제 5개 operation이 없었기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminAudioContentCommentTest`에 root/reply 조회, target AI 작성, target 작성 row 수정, owner 범위 row soft delete, 잘못된 parent/timezone/page/size/unknown field/cross-resource 계약 7건을 추가했고 미구현 route의 404/405로 실패했다.
- GREEN: `AiCharacterAdminAudioContentController`에 5개 route를 추가하고, facade에서 target active owner content, 같은 콘텐츠의 활성 root parent, target 작성 수정 권한을 선검증한 뒤 기존 `AudioContentCommentService`를 재사용했다.
- Gate: focused 댓글 테스트, content/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 결과: `REV-045` 처리 완료. 삭제는 작성자와 관계없이 target 소유 콘텐츠의 해당 row만 soft delete하고 cascade하지 않으며, 이미 비활성인 row는 200 no-op을 유지한다.
**최종 결론:** Phase 3 오디오 콘텐츠 댓글 후속 기능 종결
**다음 Goal:** `P4-R5`.
## 23. UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI 2.2.0 AudioContent 10개 operation, `DEC-UTC-DATE-001`
- 검토 범위: 오디오 생성 request, 상세 GET, 댓글·답글 GET의 controller/facade/DTO/mapper/repository
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-050` — High — 오디오 4개 operation의 timezone/UTC 계약 불일치
- 생성은 현재 레거시 `CreateAudioContentRequest`를 typealias로 사용해 `timezone` body와
`yyyy-MM-dd HH:mm` 로컬 `releaseDate`를 받는다.
- 상세 GET은 필수 `timezone` query를 받지만 facade에서 사용하지 않는다. 상세 `releaseDate`는 미래 예약일에
UTC를 Asia/Seoul로 바꾼 locale 문자열이며 현재/과거는 null이다.
- 댓글·답글 GET도 필수 `timezone` query를 받고 레거시 목록 service/repository의 표시 문자열을 반환한다.
- 승인된 최신 계약은 생성 body와 세 GET에서 `timezone`을 제거하고, 생성의 nullable `releaseDate`, 상세의
기존 nullable `releaseDate`, 댓글의 기존 `date`를 ISO-8601 UTC(`Z`)로 사용한다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| route 수 | 유지 | AudioContent 10개 operation 자체는 변경 없음 |
| 생성 request | 처리 완료 | v2 전용 DTO가 `timezone`을 거부하고 UTC `releaseDate`만 내부 경계에 전달 |
| 상세 response | 처리 완료 | query를 제거하고 미래 예약일만 UTC `Z`로 반환 |
| 댓글·답글 response | 처리 완료 | `page`/`size`만 받고 기존 `date`를 UTC `Z`로 mapping |
| legacy/public 격리 | 충족 | 기존 controller/service의 timezone 계약을 유지 |
| OpenAPI 상태 | 처리 완료 | 영향 4개 operation을 `implemented`로 동기화 |
### plan·goal 전환
- 신규 Task: `Task 3.24` / `P3-R14`
- Gate: `P3-R14-GATE`
- 완료 조건: 생성·상세·댓글·답글 actual endpoint UTC exact JSON, 상세 기존 null/노출 조건과 댓글
pagination/ownership 보존, legacy/public 회귀
- 범위 밖: 오디오 목록 날짜, 로컬 시각+timezone 병행 지원, 신규 dependency·DDL
### `P3-R14` / `P3-R14-GATE` 처리 결과
- RED: create/query/comment focused actual endpoint 테스트는 48개 중 9개가 기존 `timezone` 필수와 legacy 날짜 포맷으로
실패했다.
- GREEN: v2 생성 DTO가 `timezone`을 미지 필드로 거부하고 UTC instant를 `LocalDateTime`으로 한 번 변환해 내부 생성
overload로 전달한다. 상세와 root/reply 목록은 기존 null·pagination·ownership·filter 의미를 유지하면서 `releaseDate`
`date``toUtcIso()`로 반환한다.
- Gate: focused는 `BUILD SUCCESSFUL in 2m 13s`, parent 재실행은 `BUILD SUCCESSFUL in 52s`, content/common·legacy 영향
범위 회귀는 `BUILD SUCCESSFUL in 1m 35s`, `ktlintCheck``BUILD SUCCESSFUL in 23s`였다. 당시 OpenAPI는 34개
`implemented`, 2개 `alignment-required`였고, 후속 `P5-R6` 뒤 36개 모두 `implemented`로 통합됐다. `git diff --check`
출력이 없었다.
**최종 결론:** `REV-050` 처리 완료, Phase 3 UTC 계약 정합화 완료
**다음 Goal:** `P5-R6`.
## 24. 12차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation과 공통 `Page`/`Size`
- 검토 범위: audio controller/facade의 query binding·검증, 관련 댓글 actual endpoint 테스트
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### `REV-052` — High — 오디오 댓글·답글 목록의 optional pagination 계약 위반
- OpenAPI의 `Page`, `Size``required: false`이고 각각 기본값 `0`, `20`이다.
- 댓글 목록과 답글 목록 controller의 `page`, `size`에는 `defaultValue`가 없어 두 query를 생략하면 MVC binding
단계에서 400이 된다.
- facade의 `validateCommentQuery()`도 실제 query 이름 집합이 정확히 `page`, `size` 두 개일 때만 허용하므로,
controller 기본값만 추가해도 전체 또는 부분 생략 요청을 거부한다.
- 현재 댓글 테스트는 query 전체 생략을 400으로 기대해 계약 불일치를 회귀로 고정하고 있다.
- 영향은 두 GET의 정상 요청 가용성에 직접 미치므로 High로 판정한다.
### 검증 근거
| 근거 | 확인 내용 |
|---|---|
| OpenAPI | 두 GET이 공통 optional `Page`/`Size`를 참조 |
| controller | 댓글·답글 모두 기본값 없는 `@RequestParam page`, `size` |
| facade | query 이름의 부분집합이 아니라 정확한 집합 일치 요구 |
| test | query 전체 생략 요청을 400으로 기대 |
### plan 전환
- 신규 Task: `Task 3.25` / `P3-R15`
- Gate: `P3-R15-GATE`
- 최소 수정: 두 controller query 기본값과 facade의 미지 query 거부 조건만 정합화
- 완료 조건: 전체·부분 생략 200/default, 범위 오류·미지 query 400, 기존 UTC/ownership/pagination 회귀
- 범위 밖: OpenAPI·legacy/public API·FanTalk query policy 변경
### `P3-R15` / `P3-R15-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminAudioContentCommentTest` 9건 중 댓글·답글 전체 생략 테스트 2건이 `isOk` 기대에서 실패해 `BUILD FAILED in 45s`였다.
- GREEN: 두 controller 목록의 `page`, `size`에 각각 `0`, `20` 기본값을 적용하고, facade는 `page`, `size`의 부분집합만 허용해 미지 query·음수 page·1 미만 size의 기존 400 `ApiResponse.error` 경계를 유지했다.
- Gate: focused는 `BUILD SUCCESSFUL in 40s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck``BUILD SUCCESSFUL in 37s`였다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 controller/facade와 actual endpoint 변경을 포함하므로 실행하지 않았다.
**현재 결론:** `REV-052` 처리 완료. Phase 3 완료 판정에는 별도 `REV-056` / `P3-R16` 보완이 남아 있다.
### `REV-056` — High — AudioContent multipart request part의 JSON media type 미강제
- OpenAPI와 계약 설명은 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다.
- 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다.
- 실제 `AiCharacterAdminAudioContentControllerTest`와 update 테스트는 유효 JSON을 `text/plain` request part로
보내 200을 기대하므로 계약 불일치가 실행 테스트에도 고정돼 있다.
- 기존 정상 strict parsing·file/series/UTC 의미를 유지하면서 part-level media type만 415로 차단해야 한다.
### 추가 plan 전환
- 신규 Task: `Task 3.26` / `P3-R16`
- Gate: `P3-R16-GATE`
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
S3/DB/processing/event no-side-effect
**최종 결론:** Phase 3은 `REV-052`, `REV-056` 수정 전 완료 판정 불가
**다음 Goal:** `P3-R15` (`P2-R10-GATE` 완료 후).
### `P3-R16` / `P3-R16-GATE` 처리 결과 — 2026-07-29
- RED: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentControllerTest`의 83건 중 text/plain·Content-Type 누락 415 기대 13건이 기존 200으로 실패해 `BUILD FAILED in 1m 15s`였다.
- GREEN: Character `P2-R10``MultipartHttpServletRequest` header 검사 패턴을 AudioContent POST·PUT controller에만 적용했다. 기존 JSON String strict reader와 facade는 변경하지 않았다.
- Gate: POST·PUT actual endpoint는 KO/EN/JA의 text/plain·Content-Type 누락에 localized `ApiResponse.error` 415와 `Accept: application/json`, DB/S3/event 무변경을 확인했다. JSON 정상 경로, 생성 필수 part 400, UTC/file/series 회귀도 유지했다. focused는 `BUILD SUCCESSFUL in 56s`, content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck``BUILD SUCCESSFUL in 16s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 직접 영향 범위 회귀가 변경 slice를 포함하므로 실행하지 않았다.
**처리 결과:** `REV-056` 처리 완료.
## 25. 13차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI `AudioContentCreateMultipart`·`AudioContentUpdateMultipart`
- 검토 범위: AudioContent POST·PUT controller의 multipart binding, 파일 교체 거부와 관련 mutation 테스트
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### `REV-061` — Medium — operation별 허용 목록 밖 multipart part를 일반적으로 거부하지 않음
- OpenAPI는 생성에 `contentFile`, `coverImage`, `request`, 수정에 `coverImage`, `request`만 정의하고 두 schema 모두
`additionalProperties: false`다.
- controller는 전체 part 이름을 검사하지 않는다. 수정의 `audioFile`, `contentFile`만 별도 nullable 인자로 받아 facade에서
거부하므로, `unexpected` 같은 다른 이름의 part는 무시된다.
- 생성·수정 모두 정의되지 않은 part를 포함한 요청이 정상 mutation으로 진행될 수 있어 계약 위반을 확정했다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.27` / `P3-R17` |
| Gate | `P3-R17-GATE` |
| RED | 생성·수정 미정의 part와 S3·DB·processing/event 결과 |
| GREEN | 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` exact allow-list |
| 회귀 | 기존 `audioFile`·`contentFile` 수정 거부, 필수/빈 파일, request part 415 |
**처리 결과 (2026-07-29 / P3-R17):**
- AudioContent POST는 multipart part 이름을 `{contentFile, coverImage, request}`로 제한하고, PUT은 `{coverImage, request}`로 제한하도록 controller 경계에 allow-list를 추가했다.
- PUT controller/facade의 `audioFile`, `contentFile` nullable 인자는 제거했고, 기존 파일 교체 거부는 동일한 미정의 part 검증 경계로 통합했다.
- RED에서 생성·수정 `unexpected` part KO/EN/JA 테스트 6개가 기존 정상 mutation 경로로 실패함을 확인했고, GREEN 후 focused/영향 범위 회귀, `ktlintCheck`, `git diff --check`를 통과했다.
**Gate 결과 (2026-07-29 / P3-R17-GATE):**
- Focused multipart 회귀, Phase 3 content 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 생성·수정 미정의 part 400/no-side-effect, 수정 `audioFile`·`contentFile` 교체 미지원, 정상/필수/빈 파일/request 415 경계가 유지됨을 확인했다.
**최종 결론:** `REV-061` resolved. Phase 3 완료.
**다음 Goal:** `P4-R8`.
## 26. 14차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: OpenAPI AudioContent create/update multipart schema의 operation별 허용 part와
`additionalProperties: false`
- 검토 범위: AudioContent POST·PUT controller의 allow-list와 미정의 part·파일 교체 회귀 테스트
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-066` — Medium — 일반 form-field multipart part가 allow-list 우회
- `AiCharacterAdminAudioContentController.kt:81-88`은 operation별 허용 집합을 받지만 실제 검사는
`fileMap.keys`에 한정한다.
- 기존 create/update 회귀는 각각 `AiCharacterAdminAudioContentCreateTest.kt:251-266`,
`AiCharacterAdminAudioContentUpdateTest.kt:145-159`의 filename이 있는 `MockMultipartFile`만 사용한다.
- filename 없는 일반 form-field `unexpected`는 생성 `{contentFile, coverImage, request}`, 수정
`{coverImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.28` / `P3-R18` |
| Gate | `P3-R18-GATE` |
| RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·processing·event no-side-effect |
| GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 |
| 범위 제한 | 파일 교체 의미·OpenAPI·전역 resolver·legacy/public 변경 없음 |
**최종 결론:** Phase 3 보완 필요 — `REV-066` 확정
**다음 Goal:** `P3-R18` (`P2-R12-GATE` 완료 후).
## 25. 10차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-066`의 AudioContent POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
- 왜: 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 거부되지 않았기 때문이다.
- 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다.
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
**최종 결론:** `REV-066` 처리 완료. Phase 3 후속 Gate 완료.
**남은 항목:** 없음.
## 26. 15차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature D의 기존 creator 동작 재사용, OpenAPI 오디오 생성 preview 필드,
plan의 기존 preview 오류 key 유지 조건
- 검토 범위: `AiCharacterAdminAudioContentFacade.create`, `AudioContentService.createAudioContent` 두 overload,
v2 오디오 생성 actual endpoint 테스트
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 교차 대조했다. 사용자 지시에 따라
컴파일과 테스트는 실행하지 않았다.
### `REV-072` — High — v2 오디오 생성이 기존 preview 시간 검증을 우회
- v2 facade는 strict JSON parse와 UTC `releaseDate` 변환 후 `CreateAudioContentRequest`와 파싱된 날짜를 받는
`AudioContentService.createAudioContent` overload를 호출한다.
- 기존 `previewStartTime`·`previewEndTime`의 쌍, `HH:mm:ss` 형식, 최소 15초 검증 호출은 문자열 request를 받는
legacy overload에만 있다. v2가 호출하는 공유 대상 overload에는 검증 호출이 없다.
- 해당 overload는 검증 없이 DB를 저장하고 cover/audio를 S3에 업로드한 뒤 두 값이 모두 있으면 그대로 metadata에
넣고 event를 발행한다. 따라서 한쪽만 있는 값은 조용히 무시되고, 형식 오류·15초 미만 값은 metadata로 전달될 수 있다.
- v2 테스트에는 정상 `00:00:05`~`00:00:25` 입력만 있고 세 거부 규칙의 actual endpoint 회귀가 없다.
- 기존 creator 검증/parity와 부작용 선검증을 깨뜨리므로 High로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 3.29` / `P3-R19` |
| Gate | `P3-R19-GATE` |
| RED | 시작만 입력, 형식 오류, 15초 미만의 400 및 DB/S3/event no-side-effect |
| GREEN | 기존 검증 호출을 두 경로가 공유하는 parsed request overload로 이동 |
| 회귀 | KO/EN/JA 기존 오류 key, 정상 preview metadata, legacy/public 생성 |
### `REV-072` 처리 결과 — 2026-07-30
- `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)` 시작부로 `validatePreviewTime` 호출을 이동해 legacy
string request 생성과 v2 parsed request 생성이 같은 preview 검증을 정확히 한 번 공유한다.
- v2 actual endpoint에 한쪽만 입력, 형식 오류, 15초 미만 preview의 KO/EN/JA 400 응답과 DB/S3/event no-side-effect를
추가했고, 정상 15초 이상 preview는 audio upload metadata의 `preview_start_time`·`preview_end_time` 보존을 확인했다.
- 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest`
`BUILD SUCCESSFUL in 39s`; Phase 3 content와 legacy AudioContent 영향 범위 회귀 → `BUILD SUCCESSFUL in 1m 22s`;
`./gradlew ktlintCheck``BUILD SUCCESSFUL in 32s`; `git diff --check` → 출력 없음.
**최종 결론:** `REV-072` 처리 완료. Phase 3 완료.
**다음 Goal:** `P7-R11` 통합 재판정 완료.
## 27. 16차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature C, OpenAPI AudioContent 10개 operation
- 검토 범위: 테마·목록·상세·생성·수정, signed URL·UTC 날짜, 댓글 CRUD, owner/actor/parent와 multipart 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- AudioContent 10개 operation과 controller mapping, owner-scoped 조회·수정 및 active 콘텐츠 댓글 경계가 일치한다.
- 생성의 UTC `releaseDate`, 필수 파일, strict request, preview 쌍·형식·최소 15초 검증이 공유 service 경계에 유지된다.
- 댓글 작성의 동일 콘텐츠 활성 원댓글, target AI 수정 권한, row-only soft delete와 UTC 응답 계약이 유지된다.
- 신규 확정 finding이 없어 Phase 3 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 3 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.

View File

@@ -0,0 +1,605 @@
# 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 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.

View File

@@ -0,0 +1,641 @@
# Phase 5 커뮤니티 관리 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 5 / 커뮤니티 3개 operation |
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 5~7 working tree |
| 리뷰 일자 | 2026-07-28 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
### 목적
- PRD Feature E와 OpenAPI Community 3개 operation을 facade/legacy service/test에 대조한다.
- multipart JSON 오류, 미지 필드, pagination과 side-effect 차단 순서를 점검한다.
### 포함 범위
- `AiCharacterAdminCommunityPostController`, `Facade`, `Repository`, DTO
- 관리자 community 테스트와 재사용하는 `CreatorCommunityService`
- OpenAPI Community path/schema와 plan Phase 5
### 제외 범위
- production 수정, legacy/public community 계약 변경, 테스트 실행
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 소유권 우회 또는 데이터 손실 |
| High | 잘못된 요청이 500/side effect로 이어지는 주요 계약 위반 |
| Medium | pagination 또는 제한된 request schema 위반 |
| Low | 유지보수성 또는 문서 정합성 문제 |
## 4. 검토한 근거
| 근거 | 판정 |
|---|---|
| `AiCharacterAdminCommunityPostFacade.kt:36`~`:50` | create는 raw JSON을 legacy service에 전달하고 update는 기본 ObjectMapper로 직접 parse |
| `CreatorCommunityService.kt:73`~`:87` | create JSON을 기본 ObjectMapper로 parse한 뒤 media 검증·side effect 진행 |
| `AiCharacterAdminExceptionHandler.kt:58`~`:71` | Jackson parse 예외 전용 400 변환이 없고 미분류 예외는 500 |
| OpenAPI `:1156`~`:1176` | create/update request는 필수 필드와 `additionalProperties: false`를 정의 |
| `AiCharacterAdminCommunityPostFacade.kt:68`~`:81`, `:141`~`:145` | 목록에 `size in 1..50`을 강제 |
| OpenAPI `Size` parameter `:519` | minimum 1만 있고 maximum은 없음 |
| create/update 테스트 | request part 누락은 검증하지만 malformed/missing JSON field/unknown field는 직접 검증하지 않음 |
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| parse 흐름·예외 handler·schema 정적 추적 | 성공 | parse 예외의 500 가능성과 unknown-field 허용 경계 확인 |
| pagination 계약 대조 | 성공 | runtime 최대 50과 OpenAPI maximum 부재 확인 |
| Gradle/컴파일/테스트 | 미실행 | 사용자 요청에 따라 실행하지 않음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-025` | High | 처리 완료 | multipart JSON parse 오류가 500이 될 수 있고 미지 필드를 허용 | `Task 5.7` | `P5-R1` |
| `REV-026` | Medium | 처리 완료 | OpenAPI에 없는 목록 size 50 상한 | `Task 5.7` | `P5-R1` |
## 6. 발견 사항 상세
### REV-025 — Community JSON 오류 경계 불일치
- **심각도:** High
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature E, 공통 오류/side-effect 계약
- **관련 계약:** 잘못된 request는 400 `common.error.invalid_request`, request schema는 미지 필드 금지
- **소유 Task:** `Task 5.7`, `P5-R1`
**관찰 내용**
create는 raw request 문자열을 legacy service에 넘기고 update는 facade에서 기본 ObjectMapper로 읽는다. 두 경로 모두
`FAIL_ON_UNKNOWN_PROPERTIES`를 활성화하지 않으며 `JsonProcessingException`을 관리자 API 예외로 변환하지 않는다.
**영향**
malformed JSON이나 필수 non-null 필드 누락이 공통 handler의 500 `common.error.unknown`으로 분류될 수 있다. 미지 필드는
무시되어 잘못된 요청이 mutation과 S3/event 경로까지 진행될 수 있다.
**권장 조치**
legacy 호출 전에 create/update DTO를 strict reader로 검증하고 Jackson mapping 오류를
400 `common.error.invalid_request`로 변환한다. malformed, 필수 필드 누락, 미지 필드의 DB/S3/event 0회를 actual
endpoint로 고정한다.
**처리 결과**
create/update `request` part를 legacy service 호출 전에 strict reader로 검증하고 Jackson parse/mapping 오류를
400 `common.error.invalid_request`로 변환했다. create malformed·필수 field 누락·미지 field, update malformed·미지 field의
actual endpoint no-side-effect 테스트를 추가했다. OpenAPI상 update request에는 required field가 없어 update 필수 field 누락
케이스는 계약 밖으로 제외했다.
### REV-026 — 계약에 없는 Community size 상한
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** 레거시 목록 query/pagination 유지
- **관련 계약:** 공통 `Size` parameter는 default 20, minimum 1이며 maximum 없음
- **소유 Task:** `Task 5.7`, `P5-R1`
**관찰 내용**
facade는 `size !in 1..50`을 400으로 거부한다. OpenAPI 단일 원본에는 maximum 50이 없으므로 `size=51`은 계약상 유효하다.
**권장 조치**
OpenAPI를 임의 변경하지 않고 관리자 facade의 상한 guard만 제거한다. page 음수와 size 1 미만 검증은 유지한다.
**처리 결과**
목록의 `size <= 50` 상한 guard만 제거하고 `page < 0`, `size < 1` 검증은 유지했다. `size=51` actual endpoint 요청이
정상 pagination으로 처리되는 테스트를 추가했다.
## 7. plan·goal 전환
`plan-task.md` Phase 5의 `Task 5.7` / `P5-R1``P5-R1-GATE`를 완료 처리했다. 기존 `P5-GATE` 완료 이력은 유지한다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/path 대조 | 충족 | Community 3개 route 존재 |
| JSON 오류/schema 대조 | 충족 | strict parse와 parse 예외 400 변환, unknown-field 거부 회귀 통과 |
| pagination 대조 | 충족 | 계약에 없는 maximum 50 제거, `size=51` 회귀 통과 |
| ownership 선검증 | 충족 | target/post owner 확인은 mutation 전 수행 |
| 실행 검증 | 충족 | focused 테스트, community/common 회귀, `ktlintCheck`, `git diff --check` 통과 |
**최종 결론:** 후속 수정 및 Gate 완료
**남은 항목:** 없음. 다음 Goal은 `P6-R1`이다.
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 검증 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation
- 검토 범위: fixed update facade/legacy service/repository, transaction·lock 경계와 concurrency test
- 검증 방식: 두 병렬 transaction의 count/read/update 순서를 코드와 테스트로 정적 추적했다.
사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### 추가 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-033` | High | 처리 완료 | 최대 고정 3개가 실제 병렬 요청에서 보장되지 않음 | `Task 5.8` | `P5-R2` |
### REV-033 — lock 없는 count-then-update 경쟁 조건
- **심각도:** High
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature E의 최대 고정 게시글 수 3개
- **관련 계약:** plan transaction/concurrency 고려사항과 `Task 5.5`의 동시 요청 완료 증거
- **소유 Task:** `Task 5.8`, `P5-R2`
**관찰 내용**
legacy fixed update는 활성 고정 수를 조회한 뒤 별도 게시글 entity의 `isFixed`를 변경한다. owner 또는 고정 집합을 잠그는
lock/constraint가 없으므로, 고정 2개 상태에서 서로 다른 게시글을 고정하는 두 transaction이 모두 count 2를 읽고 커밋하면
최종 고정 수는 4개가 된다.
`AiCharacterAdminCommunityPostConcurrencyTest`는 이름과 달리 두 요청을 순서대로 호출한다. plan `Task 5.5`도 실제 병렬
재현을 하지 못했다고 기록하면서 Task objective·완료 증거와 Phase acceptance를 완료 처리했다.
**근거**
- 코드: `AiCharacterAdminCommunityPostFacade.kt:62`~`:69`은 lock 없이 legacy fixed update를 호출한다.
- 코드: `CreatorCommunityService.kt:247`~`:262``countBy...` 후 서로 다른 post를 갱신한다.
- 테스트: `AiCharacterAdminCommunityPostConcurrencyTest.kt:55`~`:79`는 세 번째 요청 완료 후 네 번째 요청을 실행하는
순차 시나리오다.
- 계획: `Task 5.5` objective/완료 증거는 동시 요청을 요구하지만 `:2849`~`:2852`에서 실제 병렬 요청은 미검증이라고
명시한다.
- 기존 코드: `MemberRepository.findByIdForUpdate` owner row pessimistic lock을 재사용할 수 있다.
**정적 재현 절차**
1. 같은 owner에 active fixed post 2개와 미고정 post 2개를 준비한다.
2. 두 독립 transaction이 서로 다른 미고정 post를 `isFixed=true`로 수정한다.
3. lock이 없으므로 두 transaction 모두 count 2를 읽을 수 있다.
4. 서로 다른 row를 갱신해 둘 다 커밋하면 최종 active fixed count는 4가 된다.
**영향**
PRD의 최대 3개 데이터 불변식이 깨지고 관리자 목록 정렬·운영 정책이 비결정적이 된다. 단순 순차 회귀는 통과하므로
현재 테스트 통과만으로 문제를 탐지할 수 없다.
**권장 조치**
신규 DDL이나 dependency 없이 기존 `MemberRepository.findByIdForUpdate`로 fixed/unfixed count·update 전에 owner를 잠근다.
두 독립 transaction과 barrier/lock probe를 사용하는 결정적 병렬 테스트로 최종 3개, 한 요청 400, 실패 side effect 0건을
검증하고 sleep·반복 확률 기반 테스트는 사용하지 않는다.
**처리 결과**
fixed 변경 요청에서 legacy count/update 전에 `MemberRepository.findByIdForUpdate`로 owner row를 잠그도록 변경했다.
두 병렬 요청이 같은 count 경계에 진입하는 결정적 회귀 테스트를 추가했고, 최종 고정 수 3개와 한 요청 400을 확인했다.
**판정 기록**
- 2026-07-28 — 코드·plan 완료 증거·테스트 실행 구조로 경쟁 조건을 확정. 테스트는 사용자 요청에 따라 미실행.
### plan·goal 전환
`plan-task.md` Phase 5에 `Task 5.8` / `P5-R2``P5-R2-GATE`를 추가했다. 기존 `Task 5.5` 완료 이력은
되돌리지 않고 실제 동시성 보완을 새 Goal로 추적한다.
`P5-R2``P5-R2-GATE`를 완료 처리했다.
### 2차 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema 대조 | 충족 | Community 3개 mapping과 JSON 경계 유지 |
| 순차 fixed 정책 | 충족 | 세 번째 성공·네 번째 거부 테스트 존재 |
| 실제 동시성 불변식 | 충족 | owner row lock과 결정적 병렬 회귀 통과 |
| plan 반영 | 충족 | `Task 5.8`, `P5-R2`, `P5-R2-GATE` |
| 실행 검증 | 충족 | focused concurrency, community/common 회귀, `ktlintCheck`, `git diff --check` 통과 |
**최종 결론:** 후속 수정 및 Gate 완료
**남은 항목:** 없음. 다음 Goal은 `P7-R2`다.
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation
- 검토 범위: 목록·생성·수정 facade, strict multipart JSON, owner-scoped query, fixed owner lock과 관련 테스트
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | Community 3개 mapping과 OpenAPI 경계 유지 |
| ownership | 충족 | active target과 owner post 검증 유지 |
| JSON/multipart | 충족 | strict parse와 malformed/unknown-field 400 유지 |
| fixed 동시성 | 충족 | owner row lock이 count/update 앞에서 수행됨 |
| plan 전환 | 해당 없음 | Phase 5 신규 Task 불필요 |
**최종 결론:** Phase 5 추가 수정 없음
**남은 항목:** `P7-R3`에서 Community/common 회귀를 통합 재검증한다.
## 11. 4차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation
- 검토 범위: 목록·생성·수정, owner query, strict JSON, 고정 수 lock과 soft delete
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/schema | 충족 | Community 3개 mapping과 OpenAPI 경계 유지 |
| ownership | 충족 | active target과 owner post를 mutation 전에 확인 |
| 고정/soft delete | 충족 | owner row lock과 fixed 상태 동시 해제 유지 |
| 오류/부작용 | 충족 | strict JSON과 target/owner 실패 선검증 유지 |
| plan 전환 | 해당 없음 | Phase 5 신규 Task 불필요 |
**최종 결론:** Phase 5 추가 수정 없음
**남은 항목:** 없음.
## 12. 5차 정적 리뷰 및 판정 — 2026-07-29
### 확인된 문제
#### `REV-043` — 커뮤니티 primitive의 required·null 계약 미강제
- **심각도:** High
- **상태:** 처리 완료
- **계약:** OpenAPI create request는 `isCommentAvailable`, `isAdult`를 required non-null boolean으로 정의하고,
`price`와 update의 `isFixed`도 nullable로 선언하지 않는다.
- **구현:** create DTO의 boolean/price는 Kotlin primitive이고 update `isFixed`는 nullable이라, strict reader가
미지 필드만 거부하면 누락·explicit null을 계약대로 구분하지 못한다.
- **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 create primitive는 false·0으로 보정될 수 있고,
update `isFixed: null`은 필드 생략과 같은 null로 처리된다.
- **영향:** 계약상 invalid 요청이 생성 mutation을 진행하거나 성공 no-op update로 처리될 수 있다.
### 보완 결과
| 항목 | 판정 |
|---|---|
| 신규 Task | `Task 5.9` / `P5-R3` 처리 완료 |
| 시작 조건 | `P4-R4-GATE` 완료 후 실행 |
| Gate | `P5-R3-GATE` 완료 |
| RED | required boolean 누락·null, `price: null`, `isFixed: null`이 400 기대 실패 |
| GREEN | v2 create/update 경계 required/non-null 검증, 생략 의미 유지 |
| 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 |
### 실행 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| invalid primitive RED focused | 실패 확인 | 신규 6건이 400 기대 실패 |
| invalid primitive GREEN focused | 통과 | `BUILD SUCCESSFUL in 41s` |
| create/update focused | 통과 | 리뷰 보완 후 `BUILD SUCCESSFUL in 34s` |
| community/common 영향 범위 회귀 | 통과 | 리뷰 보완 후 `BUILD SUCCESSFUL in 1m 2s` |
| `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck``BUILD SUCCESSFUL in 13s`, diff check 출력 없음 |
**최종 결론:** Phase 5 `REV-043` 보완 완료
**다음 Goal:** `P5-R4`.
## 13. Community 목록 요구사항 변경 판정 — 2026-07-29
### 확정 요구사항
- **추적 ID:** `DEC-P5-LIST-001`
- GET 목록에서 실제 응답 생성에 사용하지 않는 `timezone` query를 제거한다.
- 성공 `data`는 직접 배열 대신
`AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`를 반환한다.
- `totalCount``hasNext`는 target creatorMember 소유 active 게시글만 기준으로 계산한다.
- `items`의 기존 18개 필드와 고정 우선 정렬, owner/inactive 격리, page/size 오류 정책은 유지한다.
### 설계 판정
| 항목 | 판정 | 근거 |
|---|---|---|
| query | `page`, `size`만 유지 | timezone은 facade에서 유효성 검사 외 사용되지 않음 |
| response | 전용 pagination wrapper 추가 | UI가 전체 개수와 추가 로딩 필요 여부를 판단해야 함 |
| count | active owner count query 1개 추가 | totalCount가 필요해 size+1 조회만으로는 충족 불가 |
| hasNext | `pageable.offset + items.size < totalCount` | 마지막·범위 밖 page를 단순하게 처리 |
| 기존 item | 변경 없음 | 요청 범위 밖 schema 변경 방지 |
| 공용 추상화 | 추가하지 않음 | 단일 endpoint 전용 DTO가 최소 변경 |
### plan 전환
- 신규 Task: `Task 5.10` / `P5-R4`
- Gate: `P5-R4-GATE`
- 시작 조건: `P5-R3-GATE` 완료
- 통합 조건: `P7-R5` 시작 전에 `P5-R4-GATE` 완료
### 처리 결과
`P5-R4`에서 controller/facade의 `timezone` query와 미사용 검증을 제거하고, repository에 active owner count query를 추가했다.
성공 `data``AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`로 반환한다. 기존 item 18개 필드,
고정 우선 정렬, owner/inactive 격리, `page < 0`·`size < 1` 오류 정책과 문서에 없는 size 상한 부재는 유지했다. OpenAPI
Community GET status는 `implemented`로 복구했다.
### 실행 검증
| 명령 또는 검증 | 결과 | 핵심 증거 |
|---|---|---|
| query RED focused | 실패 확인 | 신규 3건이 400/직접 배열 응답 차이로 실패 |
| query GREEN focused | 통과 | `BUILD SUCCESSFUL in 1m 28s` |
| query+contract focused | 통과 | `BUILD SUCCESSFUL in 37s` |
| community/common 영향 범위 회귀 | 통과 | `BUILD SUCCESSFUL in 1m 6s` |
| OpenAPI jq assertion | 통과 | `true` |
| `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck``BUILD SUCCESSFUL in 12s`, diff check 출력 없음 |
**최종 결론:** Phase 5 `DEC-P5-LIST-001` 목록 계약 정합화 및 Gate 완료
**다음 Goal:** `P7-R5`.
## 14. 커뮤니티 댓글 후속 검토 — 2026-07-29
### 확인 결과
- **`REV-048` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 커뮤니티 게시글의 댓글·답글
조회/작성/수정/삭제 5개 operation이 없다.
- 조회는 필수 `timezone``page`, `size`, 레거시 `totalCount/items`를 유지한다.
- 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 게시글의 row를
작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다.
- 답글 `parentId`는 같은 게시글의 활성 원댓글이어야 한다.
### plan 전환
- 신규 Task: `Task 5.11` / `P5-R5`
- Gate: `P5-R5-GATE`
- 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 5 커뮤니티 댓글 CRUD 구현 필요
**다음 Goal:** `P5-R5`.
## 15. 커뮤니티 댓글 CRUD 구현 및 Gate — 2026-07-29
- 무엇을: `REV-048`을 처리했다.
- 왜: target AI 소유 커뮤니티 게시글의 댓글·답글 조회/작성/수정/삭제 5개 operation이 신규 v2 관리자 경계에 없었기 때문이다.
- 어떻게:
- RED: `AiCharacterAdminCommunityPostCommentTest`에 root/reply 목록, target AI 작성, parent 검증, target 작성자 수정 제한, row-only soft delete, 요청 오류 계약 테스트 7건을 추가했고 미구현 route로 실패했다.
- GREEN: `AiCharacterAdminCommunityPostController`에 5개 route를 추가하고, facade에서 target/owner/parent/actor를 선검증한 뒤 기존 `CreatorCommunityService` 댓글 조회·작성·수정 의미를 재사용했다.
- Gate: focused 댓글 테스트, community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 결과: `REV-048` 처리 완료. 캐릭터 직접 댓글, hard delete, cascade, legacy/public endpoint 변경은 추가하지 않았다.
**최종 결론:** Phase 5 커뮤니티 댓글 후속 기능 종결
**다음 Goal:** `P6-R2`.
## 16. 커뮤니티 댓글 UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature E, OpenAPI 2.2.0 Community 8개 operation, `DEC-UTC-DATE-001`
- 검토 범위: 커뮤니티 댓글·답글 GET의 controller/facade/repository와 관련 테스트
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-051` — High — 처리 완료: 커뮤니티 댓글 2개 GET의 timezone/UTC 계약 불일치
- 처리 전 댓글·답글 GET은 필수 `timezone` query를 controller/facade/repository로 전달하고, 각 댓글 `date`
요청 timezone에 맞춘 표시 문자열로 반환한다.
- 승인된 최신 계약은 `timezone` query 없이 `page`, `size`만 받고 기존 `totalCount`, `items`, `date` 필드명을
유지하되 `date` 값을 ISO-8601 UTC(`Z`)로 반환한다.
- 댓글 작성·수정·삭제의 actor/owner/parent/soft delete 의미와 커뮤니티 게시글 목록의 pagination wrapper는
변경 대상이 아니다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| route 수 | 유지 | Community 8개 operation 자체는 변경 없음 |
| 댓글·답글 query | 처리 완료 | v2 controller/facade에서 필수 `timezone`을 제거하고 `page`·`size`만 사용 |
| 댓글 `date` | 처리 완료 | owner-scoped 조회 결과를 `createdAt.toUtcIso()`로 재매핑해 UTC `date-time` 반환 |
| 기존 댓글 의미 | 유지 | pagination·ownership·block/secret·mutation 정책 변경 없이 영향 범위 회귀 통과 |
| legacy/public 격리 | 충족 | 기존 community 댓글 timezone service/repository 계약을 변경하지 않음 |
| OpenAPI 상태 | 처리 완료 | 영향 2개 operation을 `implemented`로 동기화 |
### plan·goal 전환
- 신규 Task: `Task 5.12` / `P5-R6`
- Gate: `P5-R6-GATE`
- 시작 조건: `P3-R14-GATE`
- 완료 조건: 댓글·답글 actual GET UTC exact JSON, 기존 pagination/ownership/block/secret 의미와
legacy/public 회귀
### `P5-R6` / `P5-R6-GATE` 처리 결과
- RED: production 변경 전 `AiCharacterAdminCommunityPostCommentTest`는 timezone 없는 root/reply GET이 400을 반환해 2건 실패했고 `BUILD FAILED in 38s`였다.
- GREEN: controller/facade의 timezone 입력·검증을 제거하고 legacy 조회 결과의 `date``createdAt.toUtcIso()`로 재매핑한 뒤 같은 focused 테스트는 `BUILD SUCCESSFUL in 43s`였다.
- Gate: community/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 11s`, `ktlintCheck``BUILD SUCCESSFUL in 26s`였고, OpenAPI는 36개 `implemented`와 0개 `alignment-required`, `git diff --check`는 출력 없음을 확인했다.
**최종 결론:** `REV-051` 처리 완료, Phase 5 UTC 계약 정합화 완료
**다음 Goal:** `P7-R7`.
## 17. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature E, OpenAPI Community 8개 operation
- 검토 범위: Community controller의 multipart/JSON mapping, 댓글 facade와 관련 actual endpoint 테스트
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### `REV-053` — High — 처리 완료
- OpenAPI는 댓글 POST와 PUT의 requestBody media type을 `application/json` 하나로 정의하고 415 response를 선언한다.
- 검토 당시 두 controller mapping에는 `consumes = [MediaType.APPLICATION_JSON_VALUE]`가 없었다.
- body를 `String`으로 받으므로 mapping 단계에서 media type을 제한하지 않으면 `text/plain` 같은 요청이
`HttpMediaTypeNotSupportedException`으로 차단되지 않고 handler/parser까지 진입할 수 있다.
- 기존 댓글 테스트는 정상·오류 JSON 요청을 모두 `application/json`으로만 보내 미지원 media type과
415 `Accept` header/no-side-effect를 고정하지 않는다.
- 외부 HTTP 요청 수용 범위와 명시된 415가 달라 High로 판정한다.
### plan 전환
- 신규 Task: `Task 5.13` / `P5-R7`
- Gate: `P5-R7-GATE`
- 최소 수정: 댓글 POST·PUT mapping에 JSON `consumes` 추가
- 완료 조건: 정상 JSON 회귀, 미지원 media type의 KO/EN/JA 415 envelope, `Accept` header,
작성 insert/event 0회와 수정 row 불변
- 범위 밖: facade/parser·댓글 actor/owner/parent 의미, OpenAPI·legacy/public API 변경
### 처리 결과
- 댓글 POST·PUT mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가했다.
- actual endpoint 회귀에서 KO/EN/JA `text/plain` 요청의 localized 415 `ApiResponse.error`, `Accept: application/json`,
작성 insert/event 0회와 수정 row 불변을 확인했다.
### `REV-058` — High — 처리 완료
- 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을
정적으로 확인할 수 있다.
### 추가 plan 전환
- 신규 Task: `Task 5.14` / `P5-R8`
- Gate: `P5-R8-GATE`
- 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제
- 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header,
S3/DB/event no-side-effect
### `P5-R8` / `P5-R8-GATE` 처리 결과
- RED: production 변경 전 `AiCharacterAdminCommunityPostCreateTest``AiCharacterAdminCommunityPostUpdateTest`
KO/EN/JA `text/plain` 및 Content-Type 누락 `request` part 415 matrix를 추가했고, focused 명령은 12개 invocation이
415 기대 실패로 `BUILD FAILED in 56s`였다.
- GREEN: controller POST·PUT 경계에서 `request` part의 JSON 호환 media type만 확인하도록 추가했다. facade strict reader,
media/fixed/owner 의미, OpenAPI schema는 변경하지 않았다.
- Gate: 같은 focused 명령은 `BUILD SUCCESSFUL in 59s`, community/common 영향 범위와
`AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 55s`였다.
**최종 결론:** `REV-053`, `REV-058` 처리 완료, Phase 5 HTTP media type 계약 정합화 완료
**다음 Goal:** `P6-R3`.
## 18. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature E, OpenAPI `CommunityPostCreateMultipart`·`CommunityPostUpdateMultipart`
- 검토 범위: Community POST·PUT controller의 multipart binding과 media/fixed/owner mutation 테스트
- 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### `REV-063` — Medium — 생성·수정의 서로 다른 허용 part 집합을 강제하지 않음
- OpenAPI는 생성에 `audioFile`, `postImage`, `request`, 수정에 `postImage`, `request`만 허용하고 두 schema 모두
`additionalProperties: false`다.
- controller는 각 `@RequestPart``request` media type만 처리하며 전체 part 이름을 검사하지 않는다.
- 특히 수정 요청에 OpenAPI가 금지한 `audioFile`이나 임의 `unexpected` part를 추가해도 해당 part가 무시된 채 게시글
mutation이 진행될 수 있다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 5.15` / `P5-R9` |
| Gate | `P5-R9-GATE` |
| RED | 생성·수정 미정의 part, 수정 `audioFile`, S3·DB·event 결과 |
| GREEN | 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` exact allow-list |
| 회귀 | 정상 media/fixed/owner, request part 누락·415 |
### `P5-R9` / `P5-R9-GATE` 처리 결과
- RED: production 변경 전 Community POST `unexpected`, PUT `unexpected`·`audioFile` actual endpoint 테스트를 추가했고,
focused 명령은 3개 케이스 모두 400 기대 실패로 `BUILD FAILED in 3m 23s`였다.
- GREEN: Community controller POST는 `{audioFile, postImage, request}`, PUT은 `{postImage, request}` exact allow-list를
적용해 초과 part를 400 `common.error.invalid_request`로 거부한다. media/fixed/owner 의미와 OpenAPI schema는 변경하지 않았다.
- Gate: focused 명령은 `BUILD SUCCESSFUL in 2m 30s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 44s`,
`ktlintCheck``BUILD SUCCESSFUL in 55s`였다.
**최종 결론:** `REV-063` 처리 완료, Phase 5 multipart part 이름 계약 정합화 완료
**다음 Goal:** `P7-R9`.
## 19. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: OpenAPI Community create/update multipart schema의 operation별 허용 part와
`additionalProperties: false`
- 검토 범위: Community POST·PUT controller와 미정의 part·media/fixed/owner 회귀 테스트
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-069` — Medium — 일반 form-field multipart part가 allow-list 우회
- `AiCharacterAdminCommunityPostController.kt:63-69`는 operation별 allow-list를 받지만 실제 검사는
`fileMap.keys`에 한정한다.
- 기존 create/update 회귀는 `AiCharacterAdminCommunityPostCreateTest.kt:202-210`
`AiCharacterAdminCommunityPostUpdateTest.kt:256-268`에서 filename이 있는 `MockMultipartFile`만 사용한다.
- filename 없는 일반 form-field `unexpected`는 생성 `{audioFile, postImage, request}`, 수정
`{postImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 5.16` / `P5-R10` |
| Gate | `P5-R10-GATE` |
| RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·event no-side-effect |
| GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 |
| 범위 제한 | media/fixed/concurrency·OpenAPI·전역 resolver·legacy/public 변경 없음 |
**최종 결론:** Phase 5 보완 필요 — `REV-069` 확정
**다음 Goal:** `P5-R10` (`P4-R10-GATE` 완료 후).
## 20. 8차 후속 수정 및 Gate — 2026-07-29
- 무엇을: `REV-069`의 Community POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다.
- 왜: 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 mutation 전 거부되지 않았기 때문이다.
- 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다.
- 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다.
**최종 결론:** `REV-069` 처리 완료. Phase 5 후속 Gate 완료.
**남은 항목:** 없음.
## 21. 9차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F의 Community 요구사항, OpenAPI Community 8개 operation
- 검토 범위: 게시글 목록·생성·수정, 댓글 CRUD, owner/actor/parent, multipart·concurrency 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정 및 plan 전환
- Community 8개 operation과 target owner, 댓글 actor/parent, exact multipart, 고정 제한 동시성 경계를 대조했다.
- 기존 완료 finding 이후 신규 확정 finding은 없다.
- Phase 5 신규 Task/Gate 없음.
**최종 결론:** Phase 5 추가 수정 없음.
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
## 22. 10차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature E, OpenAPI Community 8개 operation
- 검토 범위: 게시글 목록·생성·수정, 고정 동시성, 댓글 CRUD, owner/actor/parent와 multipart 경계
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- Community 8개 operation과 controller mapping, active owner pagination wrapper·고정 우선 정렬이 일치한다.
- 게시글 생성·수정의 strict request와 operation별 multipart part, 최대 고정 3개 owner lock·soft delete 정리가 유지된다.
- 댓글의 target AI 작성/수정, 동일 게시글 활성 원댓글, row-only soft delete와 UTC 응답 계약이 유지된다.
- 신규 확정 finding이 없어 Phase 5 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 5 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.

View File

@@ -0,0 +1,488 @@
# Phase 6 FanTalk 관리 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 6 / FanTalk 2개 operation |
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 6~7 working tree |
| 리뷰 일자 | 2026-07-28 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
### 목적
- PRD Feature F와 OpenAPI FanTalk 2개 operation을 관리자/public v2 query policy, controller, facade, 테스트에 대조한다.
- pagination 보정과 reply JSON/side-effect 경계를 점검한다.
### 포함 범위
- 관리자 FanTalk controller/facade/repository/DTO와 관련 테스트
- 공개 v2 `CreatorChannelFanTalkQueryPolicy`
- OpenAPI FanTalk path/parameter/schema와 plan Phase 6
### 제외 범위
- 공개 v2 정책 변경
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | cross-owner reply 또는 데이터 손실 |
| High | 승인된 공개 v2 parity나 주요 조회 계약 위반 |
| Medium | reply request schema·오류 계약의 제한된 위반 |
| Low | 유지보수성 또는 문서 정합성 문제 |
## 4. 검토한 근거
| 근거 | 판정 |
|---|---|
| OpenAPI `FanTalkPage`/`FanTalkSize` `:520`~`:521` | page는 0 이상, size는 20..50으로 보정 |
| `CreatorChannelFanTalkQueryPolicy.kt:8`~`:12`, `:23`~`:28` | 공개 v2가 실제로 같은 보정을 수행 |
| `AiCharacterAdminFanTalkFacade.kt:30`~`:55` | 관리자는 범위 밖 값을 400으로 거부하고 size 1도 허용 |
| `AiCharacterAdminFanTalkQueryTest.kt:120`~`:189` | size 1 성공과 -1/0/51 거부를 테스트가 반대 계약으로 고정 |
| OpenAPI `FanTalkReplyCreateRequest` `:1238`~`:1242` | `content` required, `additionalProperties: false` |
| `AiCharacterAdminFanTalkController.kt:27`~`:33` | reply를 기본 `@RequestBody` DTO binding으로 수신 |
| reply contract test `:50`~`:100` | blank/malformed/missing은 검증하지만 미지 필드는 검증하지 않음 |
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| query policy·facade·test 정적 대조 | 성공 | pagination 구현과 테스트가 PRD/OpenAPI에 반대임을 확인 |
| reply schema/binding 정적 대조 | 성공 | unknown-field strict 경계 누락 확인 |
| Gradle/컴파일/테스트 | 실행 | `P6-R1` RED/GREEN focused test 수행 |
| `P6-R1` RED | 성공 | pagination 400과 reply unknown-field 허용으로 4건 실패 확인 |
| `P6-R1` GREEN | 성공 | focused 재실행 `BUILD SUCCESSFUL in 3m 14s` |
| `P6-R1-GATE` FanTalk/common 회귀 | 성공 | `BUILD SUCCESSFUL in 1m 36s` |
| `P6-R1-GATE` lint/diff | 성공 | `ktlintCheck` `BUILD SUCCESSFUL in 35s`, `git diff --check` 출력 없음 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-027` | High | 처리 완료 | 관리자 목록 pagination이 공개 v2 보정 정책과 반대 | `Task 6.6` | `P6-R1-GATE` |
| `REV-028` | Medium | 처리 완료 | reply body가 미지 JSON 필드를 허용 | `Task 6.6` | `P6-R1-GATE` |
## 6. 발견 사항 상세
### REV-027 — FanTalk pagination 보정 불일치
- **심각도:** High
- **상태:** 처리 완료
- **관련 요구사항:** PRD Feature F, 공개 v2 응답/query policy parity
- **관련 계약:** page default 0·최소 0 보정, size default 20·20..50 보정
- **소유 Task:** `Task 6.6`, `P6-R1`
**관찰 내용**
관리자 facade는 음수 page, size 0, size 51을 400으로 거부하고 size 1을 허용한다. 공개 v2 policy와 OpenAPI는 각각
page 0, size 20, size 50으로 보정해야 하며 size 1도 20으로 올려야 한다. 현재 query 테스트가 잘못된 구현을 의도한
동작으로 고정한다.
**후속 수정 결과**
관리자 목록 facade가 공개 v2 `CreatorChannelFanTalkQueryPolicy`를 재사용하도록 변경되어 `page < 0 -> 0`,
`size < 20 -> 20`, `size > 50 -> 50` 보정이 actual endpoint 테스트로 고정됐다.
**영향**
OpenAPI client가 보정 계약을 신뢰하면 관리자 endpoint에서 예상하지 못한 400을 받으며, size 1 요청의 응답 metadata도
계약과 달라진다.
**권장 조치**
공개 v2 query policy와 동일한 정규화를 적용하고 기존 pagination 테스트를 경계값 기반으로 교정한다.
### REV-028 — FanTalk reply unknown-field 미거부
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** OpenAPI request schema 준수, 잘못된 request no-side-effect
- **관련 계약:** `FanTalkReplyCreateRequest.additionalProperties: false`
- **소유 Task:** `Task 6.6`, `P6-R1`
**관찰 내용**
controller의 기본 DTO binding은 malformed/missing content는 거부하지만 계약 밖 필드를 무시한다. repository 전역 설정에
unknown property 실패 설정이 없고 현재 contract test도 extra field를 다루지 않는다.
**후속 수정 결과**
reply controller는 raw JSON 문자열을 facade로 넘기고, facade가 `FAIL_ON_UNKNOWN_PROPERTIES` strict reader로
`AiCharacterAdminFanTalkReplyRequest`를 역직렬화한다. 미지 필드 요청은 400 `common.error.invalid_request`, reply insert
0건, `LanguageDetectEvent` 0회로 actual endpoint 테스트에 고정됐다.
**권장 조치**
reply body만 strict parse하고 미지 필드가 있으면 400 `common.error.invalid_request`, reply insert 0건,
`LanguageDetectEvent` 0회를 actual endpoint로 고정한다.
## 7. plan·goal 전환
`plan-task.md` Phase 6에 두 finding을 함께 처리하는 `Task 6.6` / `P6-R1``P6-R1-GATE`를 추가했다. 기존
`P6-GATE` 완료 이력은 유지한다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| operation/path 대조 | 충족 | FanTalk 2개 route 존재 |
| 공개 v2 pagination parity | 충족 | 공개 v2 query policy 재사용과 경계값 actual test 통과 |
| reply ownership/storage 추적 | 충족 | active owner root 선검증과 target creator 저장 확인 |
| reply JSON schema | 충족 | unknown-field 400/no insert/no event actual test 통과 |
| 실행 검증 | 충족 | focused, FanTalk/common 회귀, lint, diff 성공 |
**최종 결론:** Phase 6 후속 리뷰 종료
**남은 항목:** 없음. 다음 Goal은 `P7-R1`이다.
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature F, plan Phase 6, OpenAPI FanTalk 2개 operation
- 검토 범위: 관리자 root/reply query, 공개 v2 pagination policy, reply strict JSON·owner/root/active 검증,
writer/creator 저장과 언어 감지 event 테스트
- 검증 방식: 코드·문서·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 목록 query/pagination | 충족 | 공개 v2 `CreatorChannelFanTalkQueryPolicy` 재사용 |
| reply JSON | 충족 | strict reader와 blank/malformed/unknown-field 거부 |
| root/ownership | 충족 | active owner root만 조회하고 nested/cross-owner를 저장 전 차단 |
| writer/event | 충족 | target creator를 writer/creator로 저장하고 언어 감지 event 발행 |
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
**최종 결론:** Phase 6 추가 수정 없음
**남은 항목:** 없음. `P7-R2` 통합 재판정에서 기존 FanTalk/common 회귀만 확인한다.
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature F, plan Phase 6, OpenAPI FanTalk 2개 operation
- 검토 범위: 관리자 목록·답변 facade, 공개 v2 pagination policy, strict JSON, root ownership과 event 테스트
- 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 목록 query | 충족 | 공개 v2 page/size 보정 정책 재사용 |
| reply JSON | 충족 | malformed/blank/unknown-field 저장 전 거부 |
| root/ownership | 충족 | active owner root만 허용하고 nested/cross-owner 차단 |
| writer/event | 충족 | target creator 저장과 언어 감지 event 유지 |
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
**최종 결론:** Phase 6 추가 수정 없음
**남은 항목:** `P7-R3`에서 FanTalk/common 회귀를 통합 재검증한다.
## 11. 4차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Feature F, plan Phase 6, OpenAPI FanTalk 2개 operation
- 검토 범위: 관리자 root/reply 목록, pagination policy, strict reply JSON, root ownership·event
- 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
확정 발견 사항 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 목록/pagination | 충족 | 공개 v2 query policy와 root/reply owner query 유지 |
| reply JSON | 충족 | malformed·blank·unknown field를 저장 전에 거부 |
| root/ownership | 충족 | active owner root만 답변 허용 |
| writer/event | 충족 | target creator를 writer/creator로 저장하고 언어 감지 발행 |
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
**최종 결론:** Phase 6 추가 수정 없음
**남은 항목:** 없음.
## 12. 5차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위
- OpenAPI FanTalk 2개 operation과 controller/facade/query 구현
- reply request의 required/non-null, malformed·unknown·blank 입력 처리
- pagination 보정, root ownership, writer/event 경계
### 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 목록/pagination | 충족 | 공개 v2 query policy 보정과 owner root/reply query 유지 |
| reply JSON | 충족 | request는 non-null `String content` 하나이며 null/malformed/blank/unknown을 저장 전 거부 |
| root/ownership | 충족 | active owner root만 답변 허용 |
| primitive nullability | 해당 없음 | FanTalk JSON request에 primitive 필드가 없음 |
| plan 전환 | 해당 없음 | Phase 6 신규 Task 불필요 |
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 6 신규 수정 없음
**남은 항목:** 없음.
## 13. 팬 작성 FanTalk 원글 삭제 후속 검토 — 2026-07-29
### 확인 결과
- **`REV-049` / High / 구현 대기:** 신규 v2 관리자 경계에 target 채널의 팬 작성 FanTalk root를 삭제할
operation이 없다.
- 삭제는 팬 작성 root row만 `isActive=false`로 변경하고 연결 creator reply row는 유지한다.
- target AI가 작성한 row, reply row, 다른 채널 root는 거부하며 이미 비활성인 같은 target 팬 root는 성공 no-op이다.
- 캐릭터 직접 댓글 삭제는 v2 미사용 API로 별도 구현하지 않는다.
### plan 전환
- 신규 Task: `Task 6.7` / `P6-R2`
- Gate: `P6-R2-GATE`
- 범위 밖: hard delete·cascade, FanTalk 원글 작성, public v2 endpoint 변경
사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** Phase 6 팬 작성 FanTalk 원글 삭제 구현 필요
**다음 Goal:** `P6-R2`.
## 14. 팬 작성 FanTalk 원글 삭제 구현 검토 — 2026-07-29
### 구현 결과
- `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`를 추가했다.
- target 채널의 팬 작성 root만 `CreatorCheers.isActive=false`로 변경한다.
- 연결 creator reply row는 변경하지 않고, 목록·`fanTalkCount`에서는 삭제된 root가 제외된다.
- target AI 작성 root, reply row, 다른 채널 root, 비활성 target, 누락 ID는 400/no mutation으로 거부한다.
- 같은 target의 이미 비활성인 팬 root는 200 no-op으로 처리한다.
### 실행한 검증
| 명령 | 결과 | 핵심 증거 |
|---|---|---|
| `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkDeleteTest` | 성공 | RED 6건 미구현 route 실패 확인 후 GREEN focused `BUILD SUCCESSFUL in 29s` |
| `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest` | 성공 | DELETE 인가 matrix 보강 후 `BUILD SUCCESSFUL in 29s` |
| `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` | 성공 | FanTalk/common 영향 범위 회귀 `BUILD SUCCESSFUL in 58s` |
| `./gradlew ktlintCheck` | 성공 | `BUILD SUCCESSFUL in 14s` |
| `git diff --check` | 성공 | 출력 없음 |
**최종 결론:** `REV-049` 처리 완료. Phase 6 후속 Gate 완료.
**다음 Goal:** `P7-R6`.
## 15. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F, OpenAPI FanTalk 3개 operation
- 검토 범위: FanTalk controller의 query/JSON mapping, reply strict parser·저장 경계와 관련 테스트
- 기준 상태: 현재 working tree
- 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### `REV-054` — High — 처리 완료
- OpenAPI는 reply POST의 requestBody media type을 `application/json` 하나로 정의하고 415 response를 선언한다.
- reply controller mapping에는 `consumes = [MediaType.APPLICATION_JSON_VALUE]`가 없다.
- body를 `String`으로 받으므로 미지원 media type이 mapping 단계에서 차단되지 않고 handler/parser까지 진입할 수 있다.
- 기존 reply 계약·생성·ownership 테스트는 `application/json` 요청만 사용해 415 `Accept` header와
insert/event no-side-effect를 고정하지 않는다.
- 외부 HTTP 요청 수용 범위와 명시된 415가 달라 High로 판정한다.
### plan 전환
- 신규 Task: `Task 6.8` / `P6-R3`
- Gate: `P6-R3-GATE`
- 최소 수정: reply POST mapping에 JSON `consumes` 추가
- 완료 조건: 정상 JSON 축약 응답 회귀, 미지원 media type의 KO/EN/JA 415 envelope, `Accept` header,
reply insert/event 0회
- 범위 밖: strict parser·root/ownership·언어 감지, 목록/삭제, OpenAPI·legacy/public API 변경
### `P6-R3` / `P6-R3-GATE` 처리 결과
- RED: production 변경 전 `AiCharacterAdminFanTalkReplyContractTest`에 KO/EN/JA `text/plain` reply POST 415 matrix를
추가했고, focused 명령은 3개 invocation이 415 기대 실패로 `BUILD FAILED in 33s`였다.
- GREEN: reply POST mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`만 추가했다. strict parser,
root/ownership, 언어 감지, 목록/삭제, OpenAPI schema는 변경하지 않았다.
- Gate: 같은 focused 명령은 `BUILD SUCCESSFUL in 41s`, FanTalk/common 영향 범위와
`AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 47s`였다.
**최종 결론:** `REV-054` 처리 완료. Phase 6은 `P6-R4` 완료 전 종결할 수 없다.
**다음 Goal:** `P6-R4`.
## 16. FanTalk 답변 수정 계약 검토 — 2026-07-29
### 리뷰 범위와 근거
- 요청: FanTalk 답변을 수정하는 V2 관리자 API 추가, 레거시 `PUT /explorer/profile/cheers` 계약 유지
- 레거시 근거: `ExplorerController.modifyCheers`, `ExplorerService.modifyCheers`, `PutWriteCheersRequest`,
`CreatorChannelFanTalkResponse`
- 현재 V2 근거: FanTalk controller/facade/repository/DTO와 목록·답변 작성·팬 원글 삭제 3개 operation
- 검증 방식: 문서·레거시·현재 V2 코드 정적 대조와 `./gradlew tasks --all` 프로젝트 인식 확인. 사용자 지시에 따라
컴파일·테스트·lint는 실행하지 않았다.
### `REV-059` — High — FanTalk 답변 수정 V2 관리자 operation 부재
- 처리 전 V2 관리자 FanTalk에는 선택한 AI 캐릭터가 작성한 기존 reply의 내용이나 활성 상태를 수정할 route가 없었다.
- 레거시 request는 `cheersId`와 optional/nullable `content`, `isActive`를 받고 non-null 값만 반영한다. 두 필드를
함께 입력할 수 있고 `{}` 또는 explicit null은 성공 no-op이다.
- 레거시는 비활성 row도 조회하므로 `isActive=true` 재활성화가 가능하고, 수정 시 `languageCode`와 event를 변경하지 않는다.
- 성공 `data``CreatorChannelFanTalkResponse`이며 reply row를 매핑하므로 `fanTalkId`는 reply ID,
`creatorReplies`는 빈 배열이다.
- 관리자 V2에서는 위 계약에 `characterId`, root `fanTalkId`, `replyId` path를 적용하고 target AI가 writer이자
creator이며 지정한 활성 root의 direct child인 reply로 소유 경계를 강화해야 한다.
### 확정 계약과 plan 전환
- 신규 operation:
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`
- request: optional/nullable `content`, `isActive`; 동시 입력과 빈 객체 no-op 허용, JSON-only·미지 필드 거부
- response: 레거시 `CreatorChannelFanTalkResponse` 필드 형태
- inactive reply 재활성화 허용, inactive root·cross-target/root·팬 작성 row·direct-parent mismatch는 400/no mutation
- 신규 Task: `Task 6.9` / `P6-R4`
- Gate: `P6-R4-GATE`
- OpenAPI 상태: 전체 37개 operation 모두 `implemented`
### 구현 결과와 Gate
- RED: `AiCharacterAdminFanTalkReplyUpdateTest``AiCharacterAdminFanTalkReplyUpdateContractTest` 신규 15건이
미구현 route 404로 `BUILD FAILED in 49s`였다.
- GREEN: 신규 PUT route, JSON `consumes`, strict request DTO, active root와 target AI writer/creator direct reply를
검증하는 repository query, non-null field만 반영하는 facade를 추가했다.
- Gate: focused 재실행은 `BUILD SUCCESSFUL in 42s`, FanTalk/common/legacy 영향 범위 회귀는
`BUILD SUCCESSFUL in 1m 2s`, OpenAPI status 집계는 37개 모두 `implemented`, `ktlintCheck`
`BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다.
**최종 결론:** `REV-059` 처리 완료. Phase 6의 P6-R3/P6-R4 후속 보완은 완료됐다.
**다음 Goal:** `P7-R8`.
## 17. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F, OpenAPI FanTalk 4개 operation
- 검토 범위: 목록 pagination, 답변 작성·수정 JSON 경계, 팬 root 삭제, target/root/reply ownership
- 검증 방식: 현재 working tree의 문서·코드·관련 테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는
실행하지 않았다.
### 판정
| 항목 | 결과 | 근거 |
|---|---|---|
| route/operation | 충족 | FanTalk 4개 OpenAPI operation과 controller mapping 일치 |
| JSON request | 충족 | 답변 작성·수정의 JSON-only mapping과 strict unknown-field 거부 유지 |
| ownership/state | 충족 | active root, target AI direct reply, fan root soft delete 조건 유지 |
| 7차 multipart finding 영향 | 없음 | FanTalk에는 multipart request가 없음 |
### finding 및 plan 전환
- 신규 Phase 6 finding 없음.
- Phase 6 신규 Task/Gate 없음.
**최종 결론:** Phase 6 추가 수정 없음
**남은 항목:** `P7-R9` 통합 재판정.
## 18. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F/Edge Cases, OpenAPI FanTalk DELETE description, `api-contract.md`
- 검토 범위: FanTalk root delete facade/repository와 `AiCharacterAdminFanTalkDeleteTest`
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### `REV-070` — Low — 비활성 팬 root 삭제 설명 상충
- OpenAPI `api-contract.openapi.json:785`
`AiCharacterAdminFanTalkDeleteTest.kt:76-101`은 같은 target의 이미 비활성인 팬 root 삭제를 성공 no-op으로
정의한다. `api-contract.md:229-231`도 같은 결과를 설명한다.
- 반면 PRD `prd.md:227-228`은 비활성 root를 400 거부 대상으로 묶고, `api-contract.md:41`도 “활성 root만”이라고
적어 같은 문서 안에서 뒤쪽 no-op 설명과 상충한다.
- 기계 계약인 OpenAPI와 현재 구현·회귀가 일치하므로 runtime 변경보다 설명 문서를 no-op 계약에 맞추는 최소 보완이
적절하다. 실행 오류가 아니라 문서 불일치이므로 Low로 판정한다.
- 이 판정은 OpenAPI를 기계 계약 원본으로 두고 구현·테스트와 일치하는 쪽을 유지한 결과다. PRD의 400 문장이 최신 제품
의도라면 `P6-R5`를 실행하기 전에 OpenAPI와 runtime/test까지 변경하는 별도 범위로 재확정해야 한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 6.10` / `P6-R5` |
| Gate | `P6-R5-GATE` |
| 변경 | PRD와 `api-contract.md`의 상충 문장만 OpenAPI/runtime no-op 계약에 동기화 |
| TDD 예외 | 문서 전용 Task이며 OpenAPI·구현·test 소스 정적 대조로 검증 |
| 범위 제한 | runtime/test/OpenAPI·legacy/public 변경 없음 |
**최종 결론:** Phase 6 문서 보완 필요 — `REV-070` 확정
**다음 Goal:** `P6-R5` (`P5-R10-GATE` 완료 후).
## 19. 8차 후속 문서 정합화 및 Gate — 2026-07-29
- 무엇을: `REV-070`의 FanTalk 비활성 팬 root 삭제 설명 상충을 정리했다.
- 왜: OpenAPI·구현·`AiCharacterAdminFanTalkDeleteTest`는 같은 target의 이미 비활성인 팬 root 삭제를 200 `data:null` no-op으로 고정하지만 PRD 일부 문장이 400 거부로 설명했기 때문이다.
- 어떻게: PRD Edge Cases와 `api-contract.md` 삭제 설명을 같은 target 비활성 팬 root no-op, creator root·reply·다른 target·미존재 root 400으로 동기화했다. runtime/test/OpenAPI는 변경하지 않았다.
- 결과: 문서-only 보완으로 `REV-070` 처리 완료. 정적 대조와 diff check 결과는 `P7-R10-GATE`에 통합 기록한다.
**최종 결론:** `REV-070` 처리 완료. Phase 6 후속 Gate 완료.
**남은 항목:** 없음.
## 20. 9차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F/Edge Cases, OpenAPI FanTalk 4개 operation
- 검토 범위: 목록, 답변 작성·수정, 팬 root 삭제, target/root/direct reply ownership
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정 및 plan 전환
- FanTalk 4개 operation과 pagination, strict JSON, target AI reply ownership, root 삭제 no-op 계약을 대조했다.
- 기존 완료 finding 이후 신규 확정 finding은 없다.
- Phase 6 신규 Task/Gate 없음.
**최종 결론:** Phase 6 추가 수정 없음.
**남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정.
## 21. 10차 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F, OpenAPI FanTalk 4개 operation
- 검토 범위: 목록, creator reply 작성·수정, 팬 root 삭제와 target/root/direct reply ownership
- 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과
테스트는 실행하지 않았다.
### 판정
- FanTalk 4개 operation과 controller mapping, 공개 v2 page/size 보정·응답 필드가 일치한다.
- 답변 작성은 active root와 target creator writer/creator를, 수정은 target의 active root direct reply를 검증한다.
- 팬 root row-only soft delete와 동일 target 비활성 root 성공 no-op 계약이 문서·구현에 일치한다.
- 신규 확정 finding이 없어 Phase 6 회귀 수정 Task/Gate를 추가하지 않는다.
**최종 결론:** Phase 6 요구사항 충족, 추가 수정 없음.
**남은 항목:** 없음.

View File

@@ -0,0 +1,759 @@
# Phase 7 통합·문서 정합성 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 7 / 23개 operation 통합 상태와 문서 추적성 |
| 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 2~7 working tree |
| 리뷰 일자 | 2026-07-28 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.md`, `api-contract.openapi.json` |
| 리뷰 상태 | 후속 수정 및 Gate 완료 |
## 2. 리뷰 목적과 범위
### 목적
- Phase 7 완료 기록과 실제 신규 prefix controller/OpenAPI operation 수를 대조한다.
- 계획, 사람이 읽는 계약 설명, OpenAPI 구현 상태 metadata가 현재 구현 상태를 정확히 나타내는지 확인한다.
### 포함 범위
- 신규 prefix controller mapping 전체
- OpenAPI operation과 `x-implementation-status`
- plan 현재 상태/Endpoint Contract Summary/Phase 7 Progress
- `api-contract.md` 구현 현황
### 제외 범위
- Phase 2~6 finding의 production 수정, API schema 변경
## 3. 판정 기준
| 심각도 | 기준 |
|---|---|
| Blocker | 인수·배포 판정을 무효화하는 미구현 핵심 기능 |
| High | operation 누락 또는 공개 schema 불일치 |
| Medium | 완료 상태·생성 client 판단에 영향을 주는 metadata/문서 불일치 |
| Low | 비핵심 설명·형식 정합성 |
## 4. 검토한 근거
| 근거 | 판정 |
|---|---|
| OpenAPI 정적 집계 | 23개 operation: Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2 |
| controller mapping 정적 집계 | 후속 Gate 후 Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2로 총 23개 |
| OpenAPI status 정적 집계 | 후속 수정 후 23개 `implemented` |
| `api-contract.md:9`~`:12` | 후속 수정 후 endpoint 23개, route 구현 23개, 구현 완료 23개, 예정 0개 |
| plan Endpoint Contract Summary | 후속 수정 후 5개 domain 모두 구현 완료로 표시 |
| plan `P7-GATE`와 후속 기록 | 23개 operation 구현 완료로 판정 |
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `jq` operation/status 집계 | 성공 | operation 23, status 9/14 확인 |
| controller annotation `rg` 집계 | 성공 | mapping 24, 초과 1개는 Series DELETE |
| `P7-R1` 후속 `jq` operation/status 집계 | 성공 | operation 23, implemented 23 |
| `P7-R1` 후속 controller annotation 집계 | 성공 | mapping 23 |
| OpenAPI validate/client 생성/compile | 성공 | validate 이슈 없음, TypeScript compile exit 0 |
| Gradle/문서 diff | 성공 | `./gradlew tasks --all` 성공, `git diff --check` 출력 없음 |
| `P7-R1-GATE` 최종 대조 | 성공 | OpenAPI 23개 implemented, controller mapping 23개, 미처리 finding 0건 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-029` | Medium | 처리 완료 | 구현 완료 기록과 계약 metadata/현황 문서 불일치 | `Task 7.3` | `P7-R1-GATE` |
## 6. 발견 사항 상세
### REV-029 — 구현 상태 metadata와 완료 기록 불일치
- **심각도:** Medium
- **상태:** 처리 완료
- **관련 요구사항:** Phase 7 API contract·diff·문서 추적성 완료 조건
- **관련 계약:** OpenAPI 23개 operation과 실제 controller mapping 일치
- **소유 Task:** `Task 7.3`, `P7-R1`
**관찰 내용**
Phase 7 완료 기록은 모든 operation 구현을 선언하지만 `api-contract.md`, plan Endpoint Contract Summary,
OpenAPI `x-implementation-status`는 계약 확정 당시의 9개 정합화 필요/14개 예정 상태를 유지한다. 실제 controller는
23개가 아니라 계약 밖 Series DELETE를 포함한 24개다.
**후속 수정 결과**
`P4-R1-GATE`에서 계약 밖 Series DELETE route를 제거했고, `P7-R1`에서 plan/API 설명/OpenAPI status를 실제 구현 상태와
동기화했다. OpenAPI는 23개 operation 모두 `implemented`이며, 신규 prefix controller mapping도 23개로 일치한다.
**영향**
문서 독자와 생성 도구가 구현 완료 여부를 다르게 판단하며, Phase 7의 “OpenAPI와 route 일치” 완료 증거를 현재 정적 집계로
재현할 수 없다.
**권장 조치**
먼저 `P4-R1-GATE`에서 계약 밖 route를 제거해 controller를 23개로 맞춘다. 나머지 Phase 후속 Gate가 끝난 뒤
`P7-R1`에서 plan/API 설명/OpenAPI status를 모두 23개 `implemented`로 동기화하고 validator와 client 생성을 재검증한다.
path/request/response schema는 변경하지 않는다.
## 7. plan·goal 전환
`plan-task.md` Phase 7에 `Task 7.3` / `P7-R1``P7-R1-GATE`를 추가했다. `P7-R1`은 Phase 2~6 후속 Gate가 모두
끝난 뒤 실행한다.
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| OpenAPI operation 수 | 충족 | 23개 |
| controller mapping 수 | 충족 | 후속 Gate 후 23개 |
| 구현 상태 문서 | 충족 | plan/API 설명/OpenAPI status 모두 23개 구현 완료로 동기화 |
| dependency/DDL 신규 변경 | 신규 finding 없음 | 정적 변경 범위에서 관련 추가 없음 |
| 실행 검증 | 충족 | jq, validator, TypeScript client 생성·compile, Gradle tasks, diff check 성공 |
**최종 결론:** Phase 7 후속 리뷰 종료
**남은 항목:** 없음.
## 9. 2차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Acceptance Criteria, plan Phase 7, OpenAPI 23개 operation
- 검토 범위: Phase별 신규 finding 종결 상태, controller/OpenAPI operation 수, 구현 status, dependency/DDL·최종 Gate 조건
- 검증 방식: `rg`, `jq`, `git diff` 기반 정적 점검. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
Phase 7 자체의 신규 독립 결함은 없다. 정적 집계는 OpenAPI 23개 operation과 23개 `implemented`, controller mapping
23개를 유지한다. Phase 3~5의 `REV-030`~`REV-033`은 모두 처리 완료됐고, targeted·전체 회귀·lint·diff 검증도 통과했다.
### plan·goal 전환
`plan-task.md` Phase 7에 `Task 7.4` / `P7-R2``P7-R2-GATE`를 추가했다. 이 Task는 독립 production 수정이 아니라
`P3-R10-GATE`, `P4-R2-GATE`, `P5-R2-GATE` 뒤 targeted·전체 회귀와 문서/operation 상태를 재판정한다.
`P7-R2``P7-R2-GATE`를 완료 처리했고, plan 상태를 `구현 완료`로 되돌렸다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| OpenAPI operation/status | 충족 | 23개 operation, 23개 `implemented` |
| controller mapping | 충족 | Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 |
| 미처리 finding | 충족 | `REV-030`~`REV-033` 처리 완료 |
| 최종 Gate | 충족 | `P7-R2`, `P7-R2-GATE` 완료 |
| 실행 검증 | 충족 | targeted, 전체 회귀, lint, OpenAPI/controller/diff 점검 통과 |
**최종 결론:** 통합 재판정 및 Gate 완료
**남은 항목:** 없음.
## 16. 후속 기능 통합 최종 판정 — 2026-07-29
### Phase별 결과
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 변경 없음 | 공통 ADMIN·resolver·오류 경계 재사용 |
| 2 | 처리 완료 | `REV-044`, `P2-R9` / `P2-R9-GATE` |
| 3 | 처리 완료 | `REV-045`, `P3-R13` / `P3-R13-GATE` |
| 4 | 처리 완료 | `REV-046`~`REV-047`, `P4-R5`~`P4-R6-GATE` |
| 5 | 처리 완료 | `REV-048`, `P5-R5` / `P5-R5-GATE` |
| 6 | 처리 완료 | `REV-049`, `P6-R2` / `P6-R2-GATE` |
| 7 | 통합 재판정 완료 | `P7-R6` / `P7-R6-GATE` |
### 통합 판정
- 기존 23개 route와 후속 13개 operation을 합쳐 OpenAPI 계약은 36개다.
- 현재 상태는 36개 operation 모두 `implemented`다.
- 캐릭터 직접 댓글 API는 v2 미사용 결정에 따라 operation과 Task를 추가하지 않는다.
- Phase 2~6 신규 Gate 완료 뒤 36개 operation/mapping/`implemented`, 공통 보안·오류, actor·ownership,
row-only soft delete와 legacy/public 회귀를 `Task 7.8`에서 재판정했다.
targeted 회귀, 전체 `./gradlew test`, `ktlintCheck`, OpenAPI/controller/diff 정적 검증이 모두 성공했다.
**최종 결론:** 36개 operation 통합 재판정 및 Gate 완료
**남은 항목:** 없음.
## 12. 4차 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Acceptance Criteria, plan Phase 1~7, OpenAPI 23개 operation
- 검토 범위: Phase별 runtime 경계, operation/status/mapping, 완료 상태표·Task header, dependency/DDL 범위
- 검증 방식: `sed`, `rg`, `jq`, `git diff` 기반 정적 점검. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-038` | Low | 처리 완료 | 완료된 후속 Task 헤더와 상단 완료 상태가 모순됨 | `Task 7.6` | `P7-R4` |
| `REV-039` | Low | 처리 완료 | Phase 4 콘텐츠 해제 설명이 OpenAPI/controller route와 다름 | `Task 7.6` | `P7-R4` |
### `REV-038` — 완료 Task 헤더와 현재 상태 불일치
- **심각도:** Low
- **상태:** 처리 완료
- **처리 상태:** 처리 완료 (`P7-R4`)
- **관련 요구사항:** 작업절차의 구현 완료 즉시 Task 체크박스 갱신, 문서유지보수의 완료 상태 동기화
- **관련 계약:** plan 상단 현재 상태·Goal Progress·Phase별 Task 완료 증거
- **소유 Task:** `Task 7.6`, `P7-R4`
**관찰 내용**
`Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5`는 하위 단계·Gate·2026-07-29 검증 기록에서 완료됐지만,
Task 헤더는 `[ ]`다. 반면 상단 표는 각 Phase를 전체 완료로 표시해 동일 문서 안의 상태가 모순된다.
**근거**
- 계획: 네 Task header의 `[ ]`
- Gate: `P2-R7-GATE`, `P3-R11-GATE`, `P4-R3-GATE`, `P7-R3-GATE``[x]`
- Progress: 2026-07-29 후속 수정·통합 검증 완료 기록
**영향**
후속 agent가 이미 완료된 기능 Task를 다시 실행하거나 Phase 완료 조건을 잘못 판정할 수 있다.
**권장 조치**
기존 완료 증거를 삭제하지 않고 네 Task header, 상단 상태표와 Progress만 같은 완료 상태로 동기화한다.
### `REV-039` — Phase 4 DELETE 설명의 stale path/body
- **심각도:** Low
- **상태:** 처리 완료
- **처리 상태:** 처리 완료 (`P7-R4`)
- **관련 요구사항:** OpenAPI를 request/response의 기계 검증 가능한 단일 기준으로 사용
- **관련 계약:** `removeAiCharacterSeriesContent`
- **소유 Task:** `Task 7.6`, `P7-R4`
**관찰 내용**
plan Phase 4 endpoint 설명은 콘텐츠 해제를 `DELETE /series/{seriesId}/contents`
`RemoveContentToTheSeriesRequest(contentId)` body로 적는다. OpenAPI와 실제 controller는
`DELETE /series/{seriesId}/contents/{contentId}`이며 request body가 없다.
**근거**
- 계획: Phase 4 `API endpoint와 request/response contract`
- OpenAPI: operationId `removeAiCharacterSeriesContent`
- 코드: `AiCharacterAdminSeriesController.removeContent`
**영향**
runtime은 올바르지만 계획만 읽는 후속 구현·클라이언트 작업이 폐기된 body route를 사용할 수 있다.
**권장 조치**
production/OpenAPI는 변경하지 않고 Phase 4 설명만 현재 path parameter 계약으로 정정한다.
### plan·goal 전환
두 항목은 모두 문서 정합성이고 같은 파일에서 최소 수정할 수 있어 `plan-task.md` Phase 7의
`Task 7.6` / `P7-R4`로 묶었다.
### 실행한 정적 검증
- `jq empty api-contract.openapi.json` — 성공.
- OpenAPI 23개 operation, 고유 operationId 23개, `implemented` 23개, 200 response 누락 0개 — 성공.
- controller mapping — Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2, 합계 23개.
- `git diff --check` — 출력 없음.
- 신규 dependency/DDL 파일 변경 — 없음.
- Gradle·컴파일·테스트 — 사용자 요청에 따라 실행하지 않음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| Phase 1~6 runtime | 충족 | 신규 기능 finding 없음 |
| OpenAPI operation/status | 충족 | 23개 operation·고유 ID·implemented 유지 |
| controller mapping | 충족 | domain별 합계 23개 |
| 완료 상태 문서 | 충족 | `REV-038` 처리 완료 |
| Phase 4 route 설명 | 충족 | `REV-039` 처리 완료 |
| plan 반영 | 충족 | `Task 7.6`, `P7-R4` 추가 |
**최종 결론:** 기능 추가 수정 없음, 문서 정합성 goal 완료
**남은 항목:** 없음.
### `P7-R4` 처리 결과
- `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5`, `Task 7.6` 헤더를 완료 상태로 동기화했다.
- Phase 4 시리즈 콘텐츠 해제 설명을 `DELETE /series/{seriesId}/contents/{contentId}`와 request body 없음으로 정정했다.
- 검증: `./gradlew tasks --all``BUILD SUCCESSFUL in 767ms`, OpenAPI 23개 operation/status `jq` assertion은 `true`, 미완료 Task header `rg``git diff --check`는 출력 없음, controller mapping은 23개였다.
### `P7-R2` 실행 검증
- `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'``BUILD SUCCESSFUL in 2m 45s`.
- `./gradlew test``BUILD SUCCESSFUL in 7m 58s`.
- `./gradlew ktlintCheck``BUILD SUCCESSFUL in 1s`.
- OpenAPI operation/status `jq` assertion — `true`.
- controller mapping 23개 assertion, dependency/DDL diff, `git diff --check` — 출력 없이 통과.
## 10. 3차 정적 리뷰 및 판정 — 2026-07-28
### 리뷰 정보와 범위
- 기준 commit/working tree: `2f93e2c9` + 현재 working tree
- 기준 문서: PRD Acceptance Criteria, plan Phase 7, OpenAPI 23개 operation
- 검토 범위: Phase별 신규 finding, operation/status/mapping, 최종 완료 Gate와 dependency/DDL 범위
- 검증 방식: `rg`, `jq`, `git diff` 기반 정적 점검. 컴파일과 테스트는 실행하지 않았다.
### 발견 사항과 판정
Phase 7 자체의 신규 독립 결함은 없다. OpenAPI는 23개 operation과 23개 `implemented` status를 유지하고 controller
mapping 수도 23개다. 다만 Phase 2~4의 `REV-034`~`REV-037`이 미처리이므로 현재 최종 완료 판정은 유지할 수 없다.
### plan·goal 전환
`plan-task.md` Phase 7에 검증 전용 `Task 7.5` / `P7-R3``P7-R3-GATE`를 추가했다. 이 Goal은
`P2-R7-GATE`, `P3-R11-GATE`, `P4-R3-GATE` 완료 후 targeted·전체 회귀와 문서/operation 상태를 fresh 재판정한다.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| OpenAPI operation/status | 충족 | 23개 operation, 23개 `implemented` 유지 |
| controller mapping | 충족 | Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 |
| 독립 Phase 7 결함 | 없음 | route/schema/dependency/DDL 추가 문제 없음 |
| 최종 완료 상태 | 보류 | `REV-034`~`REV-037` 미처리 |
| plan 반영 | 충족 | `Task 7.5`, `P7-R3`, `P7-R3-GATE` 추가 |
| 실행 검증 | 미실행 | 사용자 요청에 따라 컴파일·테스트 미실행 |
**최종 결론:** Phase 7 통합 재판정 요청(당시 판정, 15절에서 처리 완료)
**남은 항목:** `P2-R7-GATE``P3-R11-GATE``P4-R3-GATE``P7-R3``P7-R3-GATE`.
## 11. 3차 통합 재판정 및 Gate — 2026-07-29
### 발견 사항과 판정
Phase 7 자체의 신규 독립 결함은 없다. `REV-034`~`REV-037`은 각 소유 Phase에서 처리 완료됐고, OpenAPI 23개 operation과 23개 `implemented` status 및 controller mapping 23개를 유지한다.
### 실행 검증
- `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'``BUILD SUCCESSFUL in 2m 25s`.
- `./gradlew test``BUILD SUCCESSFUL in 6m 54s`.
- `./gradlew ktlintCheck``BUILD SUCCESSFUL in 18s`.
- OpenAPI operation/status `jq` assertion — `true`.
- controller mapping count — 23.
- `git diff --check` — 출력 없음.
- 변경 파일명 점검 결과 신규 dependency/DDL 파일 변경 없음.
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| `REV-034`~`REV-037` | 충족 | Phase 2~4 후속 Gate 처리 완료 |
| OpenAPI operation/status | 충족 | 23개 operation, 23개 `implemented` |
| controller mapping | 충족 | Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 |
| targeted/전체 회귀 | 충족 | targeted와 전체 Gradle test 성공 |
| lint/diff/dependency/DDL | 충족 | ktlint 성공, diff check 출력 없음, 신규 dependency/DDL 없음 |
**최종 결론:** 통합 재판정 및 Gate 완료
**남은 항목:** 없음.
## 13. 5차 정적 리뷰 및 판정 — 2026-07-29
### Phase별 결과
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 신규 finding 없음 | 공통 보안·resolver·오류 경계 유지 |
| 2 | 후속 처리 요청(당시 판정) | `REV-040`, `P2-R8` / `P2-R8-GATE` |
| 3 | 후속 처리 요청(당시 판정) | `REV-041`, `P3-R12` / `P3-R12-GATE` |
| 4 | 후속 처리 요청(당시 판정) | `REV-042`, `P4-R4` / `P4-R4-GATE` |
| 5 | 후속 처리 요청(당시 판정) | `REV-043`, `P5-R3` / `P5-R3-GATE` |
| 6 | 신규 finding 없음 | FanTalk request에는 primitive 필드 없음 |
| 7 | 통합 재판정 요청(당시 판정) | `P7-R5` / `P7-R5-GATE` |
### 통합 판정
- OpenAPI operation과 controller mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2,
합계 23개를 유지한다.
- 신규 finding은 route 수가 아니라 Phase 2~5 multipart JSON의 primitive required/non-null 의미에 있다.
- 전역 Jackson 정책은 legacy endpoint까지 영향을 넓히므로 각 v2 request 경계의 최소 보완으로 계획했다.
- 네 Phase Gate 완료 전에는 문서의 `구현 완료` 최종 판정을 유지하지 않는다.
- 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
**최종 결론:** `REV-040`~`REV-043` 처리 뒤 통합 재판정 요청(당시 판정, 15절에서 처리 완료)
**당시 Goal:** `P2-R8`.
## 14. Community 목록 계약 변경 영향 판정 — 2026-07-29
### 변경 영향
- `DEC-P5-LIST-001`에 따라 Community GET의 route 수는 유지되지만 query와 성공 response schema가 변경됐다.
- OpenAPI operation은 runtime 정합화 전까지 `implemented-contract-alignment-required`로 표시한다.
- `P7-R5`는 기존 `REV-040`~`REV-043`뿐 아니라 `P5-R4-GATE`의 timezone 제거, pagination wrapper,
active owner count·hasNext 증거를 함께 대조해야 한다.
- 구현 완료 뒤 OpenAPI 23개 operation이 모두 `implemented`로 복구됐는지 확인한다.
**최종 결론:** Phase 7 통합 재판정 시작 조건에 `P5-R4-GATE` 추가
**다음 Goal:** 기존 실행 순서대로 `P2-R8`.
## 15. 5차 통합 재판정 및 Gate — 2026-07-29
### Phase별 결과
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 신규 finding 없음 | 공통 보안·resolver·오류 경계 유지 |
| 2 | 처리 완료 | `REV-040`, `P2-R8` / `P2-R8-GATE` |
| 3 | 처리 완료 | `REV-041`, `P3-R12` / `P3-R12-GATE` |
| 4 | 처리 완료 | `REV-042`, `P4-R4` / `P4-R4-GATE` |
| 5 | 처리 완료 | `REV-043`, `P5-R3` / `P5-R3-GATE`, `DEC-P5-LIST-001`, `P5-R4` / `P5-R4-GATE` |
| 6 | 신규 finding 없음 | FanTalk request에는 primitive 필드 없음 |
| 7 | 통합 재판정 완료 | `P7-R5` / `P7-R5-GATE` |
### 통합 판정
- Phase 2~5 focused와 package/common 영향 범위 회귀 증거가 모두 완료 상태다.
- Community 목록은 `timezone` query 없이 `totalCount/page/size/hasNext/items` wrapper를 반환하며 active owner count와 `hasNext` 계약을 유지한다.
- targeted 통합 test와 전체 `./gradlew test`, `ktlintCheck`가 성공했다.
- OpenAPI는 23개 operation과 23개 `implemented`를 유지하고 controller mapping도 23개다.
- 변경 파일 중 신규 dependency, migration, DDL, `.sql` 경로는 없다.
**최종 결론:** 통합 재판정 및 Gate 완료
**남은 항목:** 없음.
## 17. UTC 날짜 계약 변경 통합 판정 — 2026-07-29
### Phase별 결과
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 영향 없음 | 공통 보안·resolver·오류 계약 변경 없음 |
| 2 | 영향 없음 | Character 계약 변경 없음 |
| 3 | 처리 완료 | `REV-050`, `P3-R14` / `P3-R14-GATE` |
| 4 | 영향 없음 | Series 계약 변경 없음 |
| 5 | 처리 완료 | `REV-051`, `P5-R6` / `P5-R6-GATE` |
| 6 | 영향 없음 | FanTalk 계약 변경 없음 |
| 7 | 통합 재판정 완료 | `P7-R7` / `P7-R7-GATE` |
### 통합 판정
- route와 operation 수는 36개로 유지된다.
- 최신 OpenAPI 상태는 `implemented` 36개, `alignment-required` 0개, `planned` 0개다.
- 영향 operation은 오디오 생성·상세·댓글·답글 4개와 커뮤니티 댓글·답글 2개다.
- OpenAPI의 query parameter/schema `Timezone`은 0개이고 생성 request의 `timezone` property도 제거했다.
- 생성 nullable `releaseDate`, 상세 nullable `releaseDate`, 댓글 `date`는 기존 필드명을 유지한
ISO-8601 UTC(`Z`) `date-time` 계약이다.
- 기존 `P7-R6-GATE`의 36개 구현 완료 판정에 UTC 날짜 계약 6개 operation 정합화 결과를 누적했다.
- 오디오 focused 재실행은 `BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks` 재실행은
`BUILD SUCCESSFUL in 4m 33s`였고, 각 Gate의 영향 범위 회귀·lint·diff 성공 기록과 OpenAPI/controller 정적 집계를
대조했다.
**최종 결론:** UTC 날짜 계약 36개 operation 통합 재판정 및 Gate 완료
**남은 항목:** 없음.
## 18. 6차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 36개 operation
- 검토 범위: Phase별 controller/facade/test, operation/mapping 수, request media type, pagination,
dependency·DDL 변경 범위
- 기준 상태: 현재 working tree
- 검증 방식: `jq`, `rg`, diff 기반 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
### Phase별 결과
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 신규 finding 없음 | 공통 ADMIN 인가·resolver·오류/CORS 경계 유지 |
| 2 | 보완 필요 | `REV-055`, `P2-R10` / `P2-R10-GATE` |
| 3 | 보완 필요 | `REV-052`, `REV-056`, `P3-R15`~`P3-R16-GATE` |
| 4 | 보완 필요 | `REV-057`, `P4-R7` / `P4-R7-GATE` |
| 5 | 보완 필요 | `REV-053`, `REV-058`, `P5-R7`~`P5-R8-GATE` |
| 6 | 보완 필요 | `REV-054`, `P6-R3` / `P6-R3-GATE` |
| 7 | 재판정 대기 | `P7-R8` / `P7-R8-GATE` |
### 정적 검증 결과
- OpenAPI JSON 문법은 유효하고 operationId는 36개 모두 고유하다.
- 실제 controller mapping도 36개이며 신규 dependency·migration·DDL 변경은 없다.
- 영향 operation은 오디오 댓글·답글 GET 2개, 커뮤니티 댓글 POST·PUT 2개, FanTalk reply POST 1개와
Character·AudioContent·Series·Community multipart 생성·수정 8개로 총 13개다.
- OpenAPI의 36개 `x-implementation-status`는 모두 `implemented`지만 위 13개 HTTP 경계가 아직 계약과 달라
전체 구현 완료 판정은 보류한다.
### plan 전환 및 종결 조건
- 소유 Phase 순서: `P2-R10``P3-R15``P3-R16``P4-R7``P5-R7``P5-R8``P6-R3`
- 통합 재판정: `Task 7.10` / `P7-R8`, Gate `P7-R8-GATE`
- 종결 조건: `REV-052`~`REV-058` 처리 완료, 영향 13개 operation 회귀, 36개 contract/mapping 유지,
lint·diff와 dependency/DDL 무변경 확인
**최종 결론:** Phase 7 완료 판정 보류, 일곱 HTTP 계약 보완 후 통합 재판정 필요
**다음 Goal:** `P2-R10`.
## 19. FanTalk 답변 수정 계약 통합 영향 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD Feature F, OpenAPI 2.3.0, plan Phase 6·7
- 검토 범위: 신규 FanTalk 답변 수정 계약, 기존 36개 operation/mapping 상태, `P7-R8` 종결 조건
- 검증 방식: 문서·OpenAPI·controller mapping 정적 대조와 `./gradlew tasks --all` 프로젝트 인식 확인. 사용자 지시에
따라 컴파일·테스트·lint는 실행하지 않았다.
### 통합 판정
- OpenAPI 계약은 기존 36개 `implemented` operation에 FanTalk 답변 수정 `planned` operation 1개를 추가해 총 37개다.
- 현재 controller mapping은 36개이므로 신규 PUT이 구현되기 전 전체 계약 완료로 판정할 수 없다.
- `REV-059`는 Phase 6 `Task 6.9` / `P6-R4``P6-R4-GATE`가 소유한다.
- 기존 미처리 `REV-052`~`REV-058`과 함께 최종 `P7-R8`에서 37개 operation/고유 operationId와 controller 37개
mapping, 영향 14개 operation, 공통 ADMIN·오류·CORS, dependency·DDL·legacy/public 무변경을 재판정한다.
- 별도 Phase 7 Task를 추가하지 않고 아직 미실행인 `Task 7.10` / `P7-R8`의 시작 조건과 완료 증거를 확장했다.
**최종 결론:** Phase 7 완료 판정 보류. `P6-R4-GATE`를 포함한 여덟 소유 Gate 후 37개 operation을 통합 재판정한다.
**다음 Goal:** `P2-R10`.
## 20. HTTP 경계 최종 통합 재판정 및 Gate — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD acceptance criteria, OpenAPI 2.3.0, plan `P2-R10`~`P7-R8-GATE`
- 검토 범위: `REV-052`~`REV-059`, 37개 operation/mapping, 영향 14개 operation의 pagination·JSON-only·multipart part-level JSON/415·FanTalk 답변 수정 계약, dependency·DDL·legacy/public 변경 범위
- 검증 방식: OpenAPI/controller 정적 대조, focused 회귀, 전체 회귀, lint, diff 확인
### 통합 판정
- OpenAPI는 operationId 37개, 고유 operationId 37개, `x-implementation-status=implemented` 37개다.
- controller mapping은 37개로 OpenAPI operation 수와 일치한다.
- `REV-052`~`REV-059`는 모두 소유 Phase Gate와 회귀 검증으로 처리 완료 상태다.
- dependency·DDL 추가와 legacy/public API 변경은 없다.
### 검증 결과
- 영향 14개 operation과 공통 error/authorization focused 회귀: `BUILD SUCCESSFUL in 1m 26s`
- 전체 회귀: `./gradlew test``BUILD SUCCESSFUL in 8m 8s`
- lint: `./gradlew ktlintCheck``BUILD SUCCESSFUL in 1s`
- OpenAPI/controller 정적 대조: 37개 operationId/status와 37개 controller mapping 일치
- `git diff --check`: 출력 없음
**최종 결론:** Phase 7 HTTP 경계 통합 재판정 및 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료.
**다음 Goal:** 없음.
## 23. 8차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD, plan, OpenAPI 37개 operation, API 계약 설명, Phase 1~6 최신 구현·테스트 소스
- 검토 범위: endpoint/controller 집계, multipart 8개 operation, Phase별 신규 finding과 plan 상태
- 기준 상태: 현재 working tree
- 리뷰어/상태: Codex / 판정 완료
- 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
### Phase별 판정
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 충족 | 신규 finding 없음 |
| 2 | 보완 필요 | `REV-065` / `P2-R12` |
| 3 | 보완 필요 | `REV-066` / `P3-R18` |
| 4 | 보완 필요 | `REV-067`, `REV-068` / `P4-R9`, `P4-R10` |
| 5 | 보완 필요 | `REV-069` / `P5-R10` |
| 6 | 문서 보완 필요 | `REV-070` / `P6-R5` |
| 7 | 통합 보완 필요 | `REV-071` / `P7-R10` |
### `REV-071` — Low — 완료 Gate와 finding 상태 불일치
- `P2-R11-GATE`, `P3-R17-GATE`, `P4-R8-GATE`, `P5-R9-GATE`, `P7-R9-GATE`는 완료 기록이 있고
OpenAPI/controller/API 계약 설명도 37개 구현 완료로 동기화돼 있다.
- 그러나 `plan-task.md` finding 표의 `REV-060`~`REV-062`, `REV-064`는 여전히 `확정`으로 남아 완료 상태와
모순된다. 기존 완료 이력을 다시 열지 않고 8차 후속 Gate가 끝난 뒤 상태 표만 정리해야 한다.
### plan 전환
| 항목 | 내용 |
|---|---|
| 신규 Task | `Task 7.12` / `P7-R10` |
| Gate | `P7-R10-GATE` |
| 선행조건 | `P2-R12-GATE`, `P3-R18-GATE`, `P4-R9-GATE`, `P4-R10-GATE`, `P5-R10-GATE`, `P6-R5-GATE` |
| 통합 검증 | 8개 multipart operation, Series 장르 ID, FanTalk 문서, OpenAPI/controller 37개, finding/Phase 상태 |
| 범위 제한 | 신규 기능·route/schema·legacy/public·dependency·DDL 변경 없음 |
### 현재 통합 집계
- OpenAPI: 37개 operationId, 37개 `implemented`
- controller mapping: Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4 = 37
- 이번 리뷰의 production/test/OpenAPI 변경: 없음
- 이번 리뷰에서 실행한 컴파일·테스트: 없음
- 문서 검증: `./gradlew tasks --all``BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력 없이 성공
**최종 결론:** Phase 7 보완 필요 — Phase 2~6의 6개 소유 Task와 통합 Task 완료 전에는 전체 구현 완료로
재판정할 수 없다.
**다음 Goal:** `P2-R12`부터 실행하고 마지막에 `P7-R10`으로 통합한다.
## 21. 7차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 37개 operation
- 검토 범위: Phase별 controller/facade/test, 8개 multipart schema와 runtime part binding,
operation/mapping/status 및 계약 설명 문서
- 검증 방식: `jq`, `rg`, diff 기반 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
### Phase별 결과
| Phase | 판정 | finding / 후속 Goal |
|---:|---|---|
| 1 | 신규 finding 없음 | 공통 ADMIN 인가·resolver·오류/CORS 유지 |
| 2 | 보완 필요 | `REV-060`, `P2-R11` / `P2-R11-GATE` |
| 3 | 보완 필요 | `REV-061`, `P3-R17` / `P3-R17-GATE` |
| 4 | 보완 필요 | `REV-062`, `P4-R8` / `P4-R8-GATE` |
| 5 | 보완 필요 | `REV-063`, `P5-R9` / `P5-R9-GATE` |
| 6 | 신규 finding 없음 | FanTalk 4개 operation 계약 유지 |
| 7 | 보완·재판정 필요 | `REV-064`, `P7-R9` / `P7-R9-GATE` |
### `REV-064` — Low — 구현 현황 설명이 실제 37개 구현 상태보다 오래됨
- OpenAPI는 37개 operationId가 모두 고유하고 `x-implementation-status=implemented` 37개다.
- 실제 controller mapping도 37개이며 FanTalk 답변 수정 PUT이 구현돼 있다.
- 그러나 `plan-task.md` Endpoint Contract Summary는 여전히 36개 구현과 답변 수정 1개 `planned`,
선행 보완 완료 전 상태를 기술한다.
- `api-contract.md`도 상단 집계, endpoint 표, client 생성 설명에서 route 36개·구현 예정 1개로 남아 있다.
- 후속 작업자가 완료 상태를 잘못 판단할 수 있지만 runtime 결함은 아니므로 Low로 판정한다.
### multipart 통합 판정
- OpenAPI의 Character·AudioContent·Series·Community 생성·수정 8개 schema는 모두
`additionalProperties: false`다.
- Phase 2~5 controller는 request part media type은 확인하지만 전체 part 이름 집합을 operation별 허용 목록과
비교하지 않아 `REV-060`~`REV-063`을 확정했다.
- OpenAPI와 production route/schema는 변경하지 않고 각 소유 Phase controller 경계에서 최소 보완한다.
### plan 전환 및 종결 조건
- 소유 Phase 순서:
`P2-R11``P2-R11-GATE``P3-R17``P3-R17-GATE``P4-R8``P4-R8-GATE`
`P5-R9``P5-R9-GATE`
- 통합 재판정: `Task 7.11` / `P7-R9`, Gate `P7-R9-GATE`
- 종결 조건: 미정의 multipart part 400/no-side-effect, 기존 정상/필수/415 회귀, 37개
operation/mapping/implemented 일치, `plan-task.md`·`api-contract.md` 구현 상태 동기화
**최종 결론:** Phase 7 완료 판정 보류. `REV-060`~`REV-064` 처리 후 통합 재판정이 필요하다.
**다음 Goal:** `P2-R11`.
## 22. multipart part 이름·문서 상태 최종 통합 재판정 및 Gate — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD acceptance criteria, OpenAPI 37개 operation, plan `P2-R11`~`P7-R9-GATE`
- 검토 범위: `REV-060`~`REV-064`, 8개 multipart schema의 part allow-list, 37개 operation/mapping/status, 계약 설명 문서 상태
- 검증 방식: OpenAPI/controller 정적 대조, Phase 2~5 소유 Gate 증거 대조, 문서 diff 확인
### 통합 판정
- `REV-060`~`REV-063`은 각 소유 Phase Gate에서 처리 완료됐다.
- OpenAPI의 8개 multipart schema는 모두 `additionalProperties: false`이며, runtime allow-list와 actual endpoint 회귀가 이를 따른다.
- OpenAPI는 operation 37개, 고유 operationId 37개, `implemented` 37개, `alignment-required` 0개, `planned` 0개다.
- controller mapping은 Character 5, AudioContent 10, Series 10, Community 8, FanTalk 4로 총 37개다.
- `api-contract.md`의 상단 집계, FanTalk 답변 수정 endpoint 상태, client 생성 설명을 37개 구현 완료/예정 0개로 동기화했다.
- dependency·DDL 추가와 legacy/public API 변경은 없다.
### 검증 결과
- OpenAPI operation/status 집계: `operations=37 uniqueOperationIds=37 implemented=37 alignmentRequired=0 planned=0`
- controller mapping 집계: Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4 = 37
- 8개 multipart schema 집계: Character/Series `{image, request}`, AudioContent create `{contentFile, coverImage, request}`, AudioContent update `{coverImage, request}`, Community create `{audioFile, postImage, request}`, Community update `{postImage, request}`, 모두 `additionalProperties=false`
- Phase 2~5 Gate 회귀: focused/영향 범위 회귀와 `ktlintCheck` 성공 기록 대조 완료
- `git diff --check`: 출력 없음
**최종 결론:** Phase 7 multipart part 이름·문서 상태 통합 재판정 및 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료.
## 24. 8차 후속 통합 Gate 완료 — 2026-07-29
### 통합 판정
- `REV-065`~`REV-069`는 Phase 2~5 controller가 파일 map과 servlet 전체 part 이름을 모두 operation별 allow-list와 대조하도록 보완해 처리 완료됐다.
- `REV-068`은 Series 생성·수정의 `genreId <= 0`을 legacy 호출 전 400으로 거부하도록 보완해 처리 완료됐다.
- `REV-070`은 FanTalk 비활성 팬 root 삭제를 OpenAPI·구현·테스트와 같은 200 no-op 계약으로 PRD와 `api-contract.md`에 동기화해 처리 완료됐다.
- `REV-071`은 plan finding 표와 상단 Phase 상태를 실제 완료 상태로 동기화해 처리 완료됐다.
### 검증 결과
- RED: Phase 2~5 multipart 일반 form-field와 Phase 4 `genreId <= 0` focused RED 묶음에서 신규 multipart/genre 36건 실패.
- GREEN: 같은 focused 묶음 재실행 `BUILD SUCCESSFUL in 1m 17s`.
- 통합 회귀: ai-character admin character/content/series/community/fantalk focused와 authorization/error/token 회귀 `BUILD SUCCESSFUL in 4m 11s`.
- 정적 검증: OpenAPI `operations=37 uniqueOperationIds=37 implemented=37 alignmentRequired=0 planned=0`, controller mapping 37개, FanTalk 삭제 no-op 정적 대조 완료.
- 정적 품질: `./gradlew ktlintCheck` `BUILD SUCCESSFUL in 51s`, `git diff --check` 출력 없음.
**최종 결론:** Phase 7 8차 후속 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료 상태와 문서 상태가 일치한다.
**남은 항목:** 없음.
**다음 Goal:** 없음.
## 25. 9차 통합 정적 리뷰 및 판정 — 2026-07-29
### 리뷰 범위와 방식
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 37개 operation
- 검토 범위: Phase별 최신 production/test 소스, operation/controller 집계, 신규·미처리 finding과 문서 상태
- 검증 방식: `rg`·`sed`·`jq` 기반 정적 대조. 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다.
### Phase별 결과
| Phase | 판정 | 신규 finding/후속 |
|---:|---|---|
| 1 | 추가 수정 없음 | 없음 |
| 2 | 추가 수정 없음 | 없음 |
| 3 | 완료 | `REV-072` 처리 완료 |
| 4 | 추가 수정 없음 | 없음 |
| 5 | 추가 수정 없음 | 없음 |
| 6 | 추가 수정 없음 | 없음 |
| 7 | 완료 | `P7-R11` / `P7-R11-GATE` 완료 |
### 통합 판정
- OpenAPI JSON 문법, operation 37개, 고유 operationId 37개, `x-implementation-status=implemented` 37개는
정적으로 확인했다.
- `REV-072`는 Phase 3에서 처리 완료됐다. v2 actual endpoint의 preview 오류 3종 KO/EN/JA, no-side-effect,
정상 preview metadata, legacy 생성 회귀가 통과했다.
- OpenAPI implemented count는 37개이고 controller mapping은 파일별 4/5/10/9/8/1 합계 37개다. 신규 dependency·DDL 변경은 없다.
- 기존 완료 이력은 변경하지 않고 `Task 3.29`와 그 후속 `Task 7.13`만 완료로 동기화했다.
**최종 결론:** Phase 7 통합 재판정 및 Gate 완료. AI 캐릭터 관리자 API 37개 operation 구현 완료.
**다음 Goal:** 없음.
## 26. 10차 통합 정적 리뷰 및 판정 — 2026-07-30
### 리뷰 범위와 방식
- 기준 문서: PRD acceptance criteria, plan Phase 1~7, OpenAPI 37개 operation
- 검토 범위: Phase별 최신 production/test 소스, operation/controller 집계, 내부 `$ref`, finding·Task 상태
- 검증 방식: `rg`·`sed`·`jq` 기반 정적 대조. 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다.
### Phase별 결과
| Phase | 판정 | 신규 finding/후속 |
|---:|---|---|
| 1 | 충족 | 없음 |
| 2 | 충족 | 없음 |
| 3 | 충족 | 없음 |
| 4 | 충족 | 없음 |
| 5 | 충족 | 없음 |
| 6 | 충족 | 없음 |
| 7 | 완료 유지 | 없음 |
### 통합 판정
- OpenAPI JSON과 내부 `$ref`가 유효하고 operation 37개·고유 operationId 37개·`implemented` 37개다.
- controller mapping은 Character 5, AudioContent 10, Series 10, Community 8, FanTalk 4로 총 37개다.
- 기존 `REV-001`~`REV-072`는 모두 `처리 완료`이며 신규 확정 finding과 미완료 Task/Gate가 없다.
- Phase별 완료 수와 계획 상태가 실제 구현 현황과 일치하므로 신규 회귀 수정 Task/Goal을 추가하지 않는다.
**최종 결론:** Phase 7 통합 완료 상태 유지. AI 캐릭터 관리자 API는 문서 기준 37개 operation 구현 완료다.
**다음 Goal:** 없음.