Files
sodalive-backend-spring-boot/docs/20260724_AI캐릭터_관리자_API/plan-task.md

7764 lines
676 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AI 캐릭터 관리자 API Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `ADMIN`이 AI 캐릭터용 Member로 로그인하지 않고 `characterId` 기준으로 캐릭터, 콘텐츠·댓글, 시리즈,
커뮤니티·댓글, FanTalk 목록·답변 작성·수정·팬 원글 삭제와 등록용 원작·장르 조회를 안전하게 대리 관리하는 신규 v2
관리자 API를 구현한다.
**Architecture:** 신규 외부 경계는 `/api/v2/admin/ai-characters` 하위 controller/facade/application에 둔다. 공통 target resolver가 `characterId -> ChatCharacter.creatorMember`를 해석하고 `CREATOR + AI_CHARACTER` 불변식과 ownership을 먼저 검증한 뒤, 각 domain vertical slice가 기존 entity/repository/S3/CloudFront/event 컴포넌트를 테스트로 고정해 선택적으로 재사용한다.
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Security, JPA/Hibernate, QueryDSL, MySQL, Gradle Wrapper, JUnit5.
| 문서 항목 | 내용 |
|---|---|
| 상태 | 구현 완료 |
| 작성일 | 2026-07-24 |
| 요구사항 기준 | `docs/20260724_AI캐릭터_관리자_API/prd.md` |
| API 기준 | `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` |
| 현재 Phase | 완료 |
| 현재 활성 Goal | 완료 |
## 현재 상태
| Phase | 상태 | 완료/전체 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | 7/7 | 완료 | 10차 정적 리뷰 신규 finding 없음 |
| 2 | 완료 | 18/18 | 완료 | 16차 정적 리뷰 신규 finding 없음 |
| 3 | 완료 | 29/29 | 완료 | 16차 정적 리뷰 신규 finding 없음 |
| 4 | 완료 | 16/16 | 완료 | 10차 정적 리뷰 신규 finding 없음 |
| 5 | 완료 | 16/16 | 완료 | 10차 정적 리뷰 신규 finding 없음 |
| 6 | 완료 | 10/10 | 완료 | 10차 정적 리뷰 신규 finding 없음 |
| 7 | 완료 | 13/13 | 완료 | 10차 통합 정적 리뷰 신규 finding 없음 |
- Phase는 결과와 의존성을 묶는 문서 단위다. `create_goal`에는 Task 또는 Phase Gate 하나만 등록한다.
- 동시에 하나의 미완료 goal만 운용하고, 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다.
- 기존 `[x]` Task와 검증 기록은 당시 완료 이력으로 보존한다. 후속 리뷰에서 발견한 문제는 기존 Task를 다시 열지 않고 새 review/fix
Goal로 처리한다.
- 각 Goal은 체크박스, focused test, 완료 증거와 Progress 기록이 모두 충족된 뒤에만 `complete`로 갱신한다.
- 검증은 focused test와 영향받는 slice/legacy 회귀를 우선한다. 전체 `./gradlew test`는 공통 경계·여러 Phase 영향, targeted
결과만으로 영향 범위를 판단할 수 없는 실패 또는 최종 release 판정에 실제로 필요하다고 기록한 경우에만 실행한다.
- 전체 회귀를 생략하면 생략 근거와 대신 실행한 focused/영향 범위 회귀 명령을 Progress와 검증 기록에 남긴다.
- 같은 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화나 독립 작업도 불가능할 때만 `blocked`로 갱신한다.
---
## Source of Truth
- 요구사항 원본: `.omx/specs/deep-interview-ai-character-admin-api.md`
- 2026-07-24 후속 확정 정책: 신규 prefix는 JWT `ROLE_ADMIN` + 현재 DB `Member.role == ADMIN` 이중 인가를 적용하고,
stale ADMIN claim은 403으로 거부한다. 신규 prefix의 API application/controller/security filter 오류는 정확한 비2xx status +
`ApiResponse.error` + `Accept-Language` 기반 KO/EN/JA message를 반환한다. 이 후속 정책이 원본과 충돌하면 후속 정책을
우선한다.
- 2026-07-25 후속 확정 정책: 신규 prefix의 CORS는 현재 코드에 정의된 캐릭터 관리자 frontend Origin
`http://localhost:8888`, `https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net`
허용한다. 기존 범용 관리자 frontend와 creator frontend Origin은 허용하지 않는다.
- 2026-07-25 2차 리뷰 후속 확정 정책: 공유 `/admin/member/login`, `/member/logout`는 기존 전역 Origin과 캐릭터 관리자
Origin의 합집합을 path-specific으로 허용한다. CORS 정책 거부 403 body는 API 오류 envelope 계약에서 제외한다. 신규 prefix의
406은 `common.error.invalid_request`, `MissingPathVariableException`은 500 `common.error.unknown`으로 처리하고, 405 `Allow`
415 `Accept` 표준 header를 유지한다.
- 문서 작성 규칙: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md`
- 기존 AI 캐릭터 연결 문서: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/{prd.md,plan-task.md}`
- 2026-07-28 후속 확정 정책: 신규 endpoint의 JSON request/response는 레거시 필드명·타입·optional/nullable·기본값과
성공 `data` 형태를 그대로 유지한다. path로 이동한 ID만 body에서 제거한다. FanTalk 답변 작성만 계획의 축약 응답을
유지하고, FanTalk 목록은 공개 v2 `CreatorChannelFanTalkTabResponse` 필드 형태의 관리자 전용 endpoint로 제공한다.
따라서 `DEC-P2-T5-001`의 캐릭터 `isActive=false` 단독 입력 제한은 계약 차원에서 폐기하고, 레거시처럼 다른 optional
field와 동시 입력을 허용하되 비활성화만 반영한다.
- 2026-07-29 후속 기능 확정 정책: 오디오 콘텐츠 댓글 CRUD, 커뮤니티 댓글 CRUD, 팬 작성 FanTalk 원글 삭제,
캐릭터 등록용 원작 검색, 시리즈 등록용 장르 목록을 관리자 전용 endpoint로 추가한다. 캐릭터에 직접 달리는 레거시 댓글
삭제 API는 v2 전환 뒤 사용하지 않으므로 구현하지 않는다. 시리즈 상세 `data`는 목록 `items`의 단일 객체와 동일한
필드·타입으로 정합화한다.
- 2026-07-29 UTC 날짜 계약 확정 정책: 신규 관리자 오디오 생성 request의 `timezone` body와 오디오 상세·오디오
댓글/답글·커뮤니티 댓글/답글 GET의 `timezone` query를 제거한다. 생성의 nullable `releaseDate`는 클라이언트가
ISO-8601 UTC(`Z`)로 변환해 보내고, 상세 `releaseDate`와 댓글 `date`는 기존 필드명 및 null/노출 조건을 유지한
ISO-8601 UTC(`Z`)로 반환한다. 기존 로컬 시각+timezone 입력은 병행 지원하지 않으며 legacy/public API 계약은 변경하지 않는다.
- 2026-07-29 FanTalk 답변 수정 확정 정책:
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`는 레거시
`PUT /explorer/profile/cheers`에서 path로 이동한 `cheersId`만 제거한다. optional/nullable `content`, `isActive`, 빈
객체 no-op, 비활성 reply 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data`를 유지하되, target AI가 작성하고
target의 활성 root에 직접 연결된 reply로 한정한다.
- 기계 검증 가능한 API 계약 원본:
`docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- 계약 근거와 예외 설명:
`docs/20260724_AI캐릭터_관리자_API/api-contract.md`
## Endpoint Contract Summary
신규 API prefix는 `/api/v2/admin/ai-characters`로 한다. 기존 `/admin/*`, `/creator-admin/*`, 공개
`/api/v2/creator-channels/*`의 성공·오류 status/body/message 계약은 변경하지 않는다.
모든 성공 응답은 기존 관례처럼 `ApiResponse.ok(...)` wrapper를 사용한다. API application/controller/security filter 오류는
오류 의미에 맞는 HTTP status와 `ApiResponse.error(...)` wrapper를 사용한다.
`characterId`는 target resource endpoint의 외부 대상 식별자다. 캐릭터 목록/검색은 선택된 target이 없어 `characterId`를 받지 않고, 캐릭터 생성은 새 `ChatCharacter`를 만드는 endpoint라 path `characterId`를 받지 않는다.
목록/검색 endpoint의 query와 pagination은 `api-contract.openapi.json`에 명시한 각 레거시 API를 따른다. FanTalk 관리자
목록만 공개 v2 query policy의 `page` 기본값 0, `size` 기본값 20, 최소 20, 최대 50 보정을 적용한다.
정식 endpoint, request/response schema, 타입, required/optional/nullable, 기본값과 multipart encoding은
`api-contract.openapi.json`만 기준으로 사용한다. 사람이 읽는 레거시 근거와 생성 방법은 `api-contract.md`를 참고한다.
| Domain | operation | 구현 상태와 소유 Goal |
|---|---:|---|
| Character | 5 | 5 route 구현 및 multipart request part media type 정합화 완료 (`P2-R10`) |
| AudioContent | 10 | 10 route 구현 및 pagination·multipart part 계약 정합화 완료 (`P3-R15`, `P3-R16`) |
| Series | 10 | 10 route 구현 및 multipart part media type 정합화 완료 (`P4-R7`) |
| Community | 8 | 8 route 구현 및 JSON·multipart part 계약 정합화 완료 (`P5-R7`~`P5-R9`) |
| FanTalk | 4 | 4 route 구현 및 JSON media type·답변 수정 계약 정합화 완료 (`P6-R3`, `P6-R4`) |
- 신규 path에 포함된 `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`, `replyId`만 레거시 request body에서
제거한다.
- 그 밖의 JSON 필드명·타입·optional/nullable·기본값과 성공 `data` 형태는 레거시 API를 유지한다.
- 레거시 mutation의 성공 `data``null`이고 오디오 콘텐츠 생성만 `CreateAudioContentResponse(contentId)`를 반환한다.
- FanTalk 답변 작성만 승인된 축약 응답 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다.
- FanTalk 답변 수정은 레거시 `PutWriteCheersRequest`의 optional/nullable `content`, `isActive`
`CreatorChannelFanTalkResponse` 성공 `data`를 유지한다. target AI가 작성하고 path의 활성 root에 직접 연결된 reply만
수정하며 비활성 reply 재활성화와 빈 객체 no-op을 허용한다.
- 캐릭터 수정의 `isActive=false`는 다른 optional JSON field와 함께 받을 수 있으며 레거시 의미대로 비활성화만 반영한다.
- 커뮤니티 목록은 2026-07-29 사용자 확정에 따라 레거시 직접 배열의 예외로 둔다. `timezone` query 없이
`totalCount`, `page`, `size`, `hasNext`, `items` pagination wrapper를 반환한다.
- 오디오 생성 request는 `timezone` 없이 nullable `releaseDate`를 ISO-8601 UTC(`Z`)로 받는다. 오디오 상세는
`timezone` query 없이 기존 nullable `releaseDate`를 UTC로 반환한다.
- 오디오 콘텐츠와 커뮤니티 댓글의 조회는 `timezone` 없이 `page`, `size`와 레거시 `totalCount`, `items`를 유지하고
`date`를 ISO-8601 UTC(`Z`)로 반환한다. 작성은 target AI 캐릭터 명의로 수행하고 수정은 target AI가 작성한
댓글/답글만 허용한다. 삭제는 target 소유 리소스의 댓글/답글이면 작성자와 무관하게 해당 row만 soft delete한다.
- 댓글 생성의 optional `parentId`가 없으면 원댓글, 있으면 같은 리소스의 활성 원댓글에 대한 답글이다. 삭제는 cascade하지
않으며 이미 비활성인 row 삭제는 성공 no-op이다.
- 팬 작성 FanTalk 삭제는 target 채널의 활성 root만 soft delete하고 creator reply row는 유지한다.
- 캐릭터에 직접 달리는 레거시 댓글 삭제는 v2 미사용 API라 구현 범위에서 제외한다.
- 원작 검색은 필수 `searchTerm``OriginalWorkResponse` 직접 배열, 장르 목록은 활성 장르의
`id`, `genre`, `isAdult` 직접 배열을 사용한다.
- 시리즈 상세 `data`는 배열 wrapper 없이 목록 `items`의 단일 객체와 동일한 11개 필드를 반환하고 기존 상세 전용
`genre`, `keywords`는 제거한다.
- multipart의 `request` part는 `application/json`이고 각 파일 part의 이름과 required 여부는 OpenAPI encoding을 따른다.
- 공통 오류는 400/401/403/404/405/406/415/500과 `ApiResponse.error`를 사용한다. 405의 `Allow`, 415의 `Accept`,
미지원 `Accept-Language`의 KO fallback과 Spring CORS 정책 거부 403 예외를 유지한다.
- 기존 23개 operation route와 구현된 후속 14개 operation route를 유지한다. OpenAPI 37개 모두
`x-implementation-status``implemented`다. 2026-07-29 6차·7차 정적 리뷰에서 확인한 optional pagination,
JSON-only, multipart media type과 operation별 multipart part 이름 보완은 각 소유 Goal에서 완료했으며,
`P7-R9`에서 전체 문서 상태를 통합 재판정한다.
---
## 과거 구현 계약 이력 (비규범)
아래 축약 예시는 2026-07-28 레거시 계약 확정 전 Phase 2·3 구현과 계획 변경 이력을 보존하기 위한 자료다.
클라이언트 개발, 신규 구현, 테스트의 계약으로 사용하지 않으며 위 Endpoint Contract Summary와
`api-contract.openapi.json`이 항상 우선한다.
```json
{
"success": true,
"message": null,
"data": {}
}
```
공통 API 오류 응답은 `success=false`, 현지화된 `message`, `data=null`, `errorProperty=null`을 포함하고 2xx로 normalize하지 않는다.
`Accept-Language: ko|en|ja`에 따라 KO/EN/JA를 반환하며, 없거나 지원하지 않는 언어는 KO로 fallback한다. security filter 단계도
MVC interceptor에 의존하지 않고 header를 직접 해석한다.
| 오류 | HTTP status | message key |
|---|---:|---|
| JWT 없음·잘못됨·만료·폐기 | 401 | `common.error.bad_credentials` |
| JWT role 비ADMIN | 403 | `common.error.access_denied` |
| JWT ADMIN + 현재 DB role 비ADMIN stale claim | 403 | `common.error.access_denied` |
| request binding·target 미존재·creatorMember 누락·role/memberKind 불변식 위반 | 400 | `common.error.invalid_request` |
| 신규 prefix 미매핑 경로 | 404 | `common.error.invalid_request` |
| 지원하지 않는 HTTP method | 405 | `common.error.invalid_request` |
| 지원하지 않는 응답 media type | 406 | `common.error.invalid_request` |
| 지원하지 않는 요청 media type | 415 | `common.error.invalid_request` |
| `MissingPathVariableException`·예상하지 못한 서버 오류 | 500 | `common.error.unknown` |
405 응답은 표준 `Allow` header를, 415 응답은 표준 `Accept` header를 유지한다.
```json
{
"success": false,
"message": "Invalid request.",
"data": null,
"errorProperty": null
}
```
신규 prefix는 캐릭터 관리자 Origin `http://localhost:8888`, `https://test-character-admin.sodalive.net`,
`https://character-admin.sodalive.net`만 허용한다. 공유 `/admin/member/login`, `/member/logout`는 기존 전역 Origin과 캐릭터
관리자 Origin의 합집합만 path-specific으로 허용하며, 다른 legacy/public 경로의 허용 범위는 변경하지 않는다. 허용되지 않은
Origin, method 또는 header가 Spring CORS 계층에서 403으로 정책 거부되면 handler 진입 전 종료되는 브라우저 보안 경계이므로
그 응답의 body, content type, 현지화 및 `ApiResponse.error` envelope는 외부 계약으로 고정하지 않는다.
Phase 2~6에서 추가되는 domain/client/server 오류는 구현 전에 각 Task에서 정확한 비2xx status와 KO/EN/JA message key를
고정하고 같은 envelope를 적용한다. 신규 prefix 전용 오류 처리는 legacy/public endpoint에 적용하지 않는다.
#### 캐릭터 목록/검색
`GET /api/v2/admin/ai-characters?search=루나&page=0&size=20`
Query parameters:
```json
{
"search": "루나",
"page": 0,
"size": 20
}
```
Response `data`:
```json
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"characterId": 101,
"name": "루나",
"description": "달빛을 좋아하는 AI 캐릭터",
"imageUrl": "https://cdn.example.com/characters/luna.png",
"creatorMemberId": 9001,
"creatorNickname": "루나",
"originalWorkId": 31,
"externalCharacterId": "ext-luna-001",
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z"
}
]
}
```
#### 캐릭터 상세
`GET /api/v2/admin/ai-characters/{characterId}`
Response `data`:
```json
{
"characterId": 101,
"name": "루나",
"description": "달빛을 좋아하는 AI 캐릭터",
"imageUrl": "https://cdn.example.com/characters/luna.png",
"creatorMemberId": 9001,
"creatorNickname": "루나",
"creatorProfileImageUrl": "https://cdn.example.com/characters/luna.png",
"creatorIntroduce": "달빛을 좋아하는 AI 캐릭터",
"originalWorkId": 31,
"externalCharacterId": "ext-luna-001",
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z",
"updatedAtUtc": "2026-07-24T00:00:00Z"
}
```
#### 캐릭터 생성
`POST /api/v2/admin/ai-characters`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"image": "File | optional",
"request": {
"name": "루나",
"systemPrompt": "루나는 달빛을 좋아하는 AI 캐릭터입니다.",
"description": "달빛을 좋아하는 AI 캐릭터",
"originalWorkId": 31
}
}
```
Response `data`: 캐릭터 상세와 동일하다.
#### 캐릭터 수정/비활성화
`PUT /api/v2/admin/ai-characters/{characterId}`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"image": "File | optional",
"request": {
"name": "루나",
"description": "수정된 소개",
"originalWorkId": 31
}
}
```
`externalCharacterId`는 response 전용이며 request에 포함하면 400이다. 일반 수정은 `image`와 함께 보낼 수 있으며 image가 없으면 기존
이미지를 유지한다. soft delete는 `request: {"isActive": false}`만 허용하고 일반 수정 field 또는 image와 혼합하면 400이다.
Response `data`: 캐릭터 상세와 동일하다. `isActive=false`는 soft delete 의미다.
#### 오디오 콘텐츠 목록/검색
`GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20`
Query parameters:
```json
{
"search": "밤",
"status": "OPEN | SCHEDULED",
"page": 0,
"size": 20
}
```
Response `data`:
```json
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"contentId": 501,
"title": "밤 산책",
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
"audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
"price": 1000,
"isAdult": false,
"isActive": true,
"releaseDateUtc": "2026-07-25T00:00:00Z",
"status": "OPEN"
}
]
}
```
#### 오디오 콘텐츠 상세
`GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
Response `data`:
```json
{
"contentId": 501,
"title": "밤 산책",
"description": "조용한 밤 산책 오디오",
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
"audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
"price": 1000,
"isAdult": false,
"isActive": true,
"releaseDateUtc": "2026-07-25T00:00:00Z",
"status": "OPEN",
"seriesIds": [701],
"createdAtUtc": "2026-07-24T00:00:00Z",
"updatedAtUtc": "2026-07-24T00:00:00Z"
}
```
#### 오디오 콘텐츠 생성
`POST /api/v2/admin/ai-characters/{characterId}/audio-contents`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"coverImage": "File",
"audioFile": "File",
"request": {
"title": "밤 산책",
"description": "조용한 밤 산책 오디오",
"price": 1000,
"isAdult": false,
"isActive": true,
"themeId": 11,
"releaseDateUtc": "2026-07-25T00:00:00Z",
"seriesIds": [701]
}
}
```
Response `data`: 오디오 콘텐츠 상세와 동일하다.
#### 오디오 콘텐츠 수정/soft delete
`PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"coverImage": "File | optional",
"audioFile": "미지원",
"request": {
"title": "밤 산책 수정",
"description": "수정된 설명",
"price": 1200,
"isAdult": false,
"isActive": false,
"releaseDateUtc": null,
"seriesIds": [701]
}
}
```
Response `data`: 오디오 콘텐츠 상세와 동일하다.
#### 시리즈 목록
`GET /api/v2/admin/ai-characters/{characterId}/series?page=0&size=20`
Query parameters:
```json
{
"page": 0,
"size": 20
}
```
Response `data`:
```json
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"seriesId": 701,
"title": "루나의 밤",
"introduction": "밤을 주제로 한 시리즈",
"coverImageUrl": "https://cdn.example.com/series/701.png",
"genreId": 3,
"isAdult": false,
"state": "OPEN",
"isActive": true,
"orders": 1
}
]
}
```
#### 시리즈 상세
`GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`
Response `data`:
```json
{
"seriesId": 701,
"title": "루나의 밤",
"introduction": "밤을 주제로 한 시리즈",
"coverImageUrl": "https://cdn.example.com/series/701.png",
"publishedDaysOfWeek": ["MONDAY", "WEDNESDAY"],
"genreId": 3,
"keywords": ["밤", "산책"],
"isAdult": false,
"state": "OPEN",
"isActive": true,
"writer": "루나",
"studio": "소다라이브",
"orders": 1
}
```
#### 시리즈 생성
`POST /api/v2/admin/ai-characters/{characterId}/series`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"image": "File | optional",
"request": {
"title": "루나의 밤",
"introduction": "밤을 주제로 한 시리즈",
"publishedDaysOfWeek": ["MONDAY", "WEDNESDAY"],
"genreId": 3,
"keywords": ["밤", "산책"],
"isAdult": false,
"state": "OPEN",
"writer": "루나",
"studio": "소다라이브"
}
}
```
Response `data`: 시리즈 상세와 동일하다.
#### 시리즈 수정/soft delete
`PUT /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"image": "File | optional",
"request": {
"title": "루나의 밤 수정",
"introduction": "수정된 소개",
"publishedDaysOfWeek": ["FRIDAY"],
"genreId": 3,
"keywords": ["밤"],
"isAdult": false,
"state": "OPEN",
"isActive": false,
"writer": "루나",
"studio": "소다라이브"
}
}
```
Response `data`: 시리즈 상세와 동일하다.
#### 시리즈 콘텐츠 조회
`GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents?search=밤&page=0&size=20`
Query parameters:
```json
{
"search": "밤",
"page": 0,
"size": 20
}
```
#### 시리즈 콘텐츠 연결 해제
`DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}`
Request body: 없음. `RemoveContentToTheSeriesRequest``seriesId`, `contentId`는 path variable로 이동한다.
Response `data`:
```json
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"contentId": 501,
"title": "밤 산책",
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
"isAdult": false,
"orders": 1
}
]
}
```
#### 시리즈 콘텐츠 연결
`POST /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents`
Request body:
```json
{
"contentIdList": [501, 502]
}
```
Response `data`: `null`
#### 시리즈 콘텐츠 연결 해제
`DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents`
Request body:
```json
{
"contentId": 501
}
```
Response `data`: `null`
#### 시리즈 순서 변경
`PUT /api/v2/admin/ai-characters/{characterId}/series/orders`
Request body:
```json
{
"seriesIds": [701, 702, 703]
}
```
Response `data`: 시리즈 목록과 동일하다.
#### 커뮤니티 게시글 목록
`GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=0&size=20`
Query parameters:
```json
{
"page": 0,
"size": 20
}
```
Response `data`:
```json
{
"totalCount": 1,
"page": 0,
"size": 20,
"hasNext": false,
"items": [
{
"postId": 801,
"content": "오늘의 소식입니다.",
"imageUrl": "https://cdn.example.com/community/801.png",
"audioSignedUrl": null,
"price": 0,
"isAdult": false,
"isFixed": true,
"fixedAtUtc": "2026-07-24T00:00:00Z",
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z"
}
]
}
```
#### 커뮤니티 게시글 등록
`POST /api/v2/admin/ai-characters/{characterId}/community-posts`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"image": "File | optional",
"audioFile": "File | optional",
"request": {
"content": "오늘의 소식입니다.",
"price": 0,
"isAdult": false,
"isFixed": false,
"isActive": true
}
}
```
Response `data`:
```json
{
"postId": 801,
"content": "오늘의 소식입니다.",
"imageUrl": "https://cdn.example.com/community/801.png",
"audioSignedUrl": null,
"price": 0,
"isAdult": false,
"isFixed": false,
"fixedAtUtc": null,
"isActive": true,
"createdAtUtc": "2026-07-24T00:00:00Z",
"updatedAtUtc": "2026-07-24T00:00:00Z"
}
```
#### 커뮤니티 게시글 수정/고정/soft delete
`PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`
Content-Type: `multipart/form-data`
Form fields:
```json
{
"image": "File | optional",
"audioFile": "File | optional",
"request": {
"content": "수정된 소식입니다.",
"price": 0,
"isAdult": false,
"isFixed": false,
"isActive": false
}
}
```
Response `data`: 커뮤니티 게시글 등록 응답과 동일하다. `isActive=false`이면 `isFixed=false`, `fixedAtUtc=null`이어야 한다.
#### FanTalk 답변 작성
`POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`
Request body:
```json
{
"content": "응원해줘서 고마워요!"
}
```
Response `data`:
```json
{
"fanTalkId": 901,
"replyId": 902,
"creatorMemberId": 9001,
"content": "응원해줘서 고마워요!",
"createdAtUtc": "2026-07-24T00:00:00Z"
}
```
---
## 구현 Phase
### Phase 1: 공통 ADMIN 인증과 AI 캐릭터 target resolver 기반
#### 공통 Task 실행 규칙
- 각 구현 Task는 `RED: 실패 테스트 작성/실패 확인`, `GREEN: 최소 구현/통과 확인`, `REFACTOR: 정리/회귀 확인`을 포함한다.
- 테스트 작성이 현실적으로 불가능한 검증 전용 Task는 `TDD 예외 사유``대체 검증 방법`을 Task에 명시한다.
- 기존 business method 재사용 전 특성화/회귀 테스트는 신규 v2 use-case RED 테스트와 분리한다. 특성화 테스트는 기존 legacy/creator-admin 구현을 대상으로 먼저 통과해 baseline을 고정하고, 그 결과를 신규 v2 RED 기대값으로 옮긴다.
- Phase 2~6의 모든 신규 오류 RED는 정확한 비2xx status, `ApiResponse.error` shape, `Accept-Language` KO/EN/JA message를
함께 검증한다. 새 오류 분기는 status, message key, 3개 언어 message, 테스트가 모두 정해지기 전 완료 처리하지 않는다.
#### 목표
모든 신규 API가 공유할 JWT ADMIN + 현재 DB ADMIN 이중 인가, prefix 전용 오류 envelope/i18n, `characterId` 기반 target 해석,
`CREATOR + AI_CHARACTER` 불변식, ownership no-side-effect 검증 기반을 만든다.
#### 범위와 비범위
- 포함: 신규 v2 admin package 골격, JWT role + 현재 DB role 이중 인가, prefix 전용 security/application 오류 처리, 공통 target
resolver/use-case, no-side-effect 테스트 fixture.
- 제외: 캐릭터/콘텐츠/시리즈/커뮤니티/FanTalk 실제 domain 기능 구현.
#### 선행 Phase 및 의존성
- 선행 Phase 없음.
- 기존 `ChatCharacter.creatorMember`, `MemberRole.CREATOR`, `MemberKind.AI_CHARACTER`가 존재해야 한다.
#### API endpoint와 request/response contract
- 모든 후속 endpoint에 공통 적용한다.
- target resource request는 JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN`을 모두 만족하는 principal과 path/query/body의
`characterId`를 받는다. 캐릭터 목록/검색과 생성은 Endpoint Contract Summary의 예외를 따른다.
- resolver output은 내부 전용 `AiCharacterAdminTarget(characterId, chatCharacter, creatorMember)`로 계획한다.
- Phase 1 API 실패 응답은 Endpoint Contract Summary의 400/401/403/404/405/406/415/500과
`ApiResponse.error`/KO·EN·JA 계약을 따르며 domain side effect가 없어야 한다. Spring CORS 정책 거부 403 body는 해당
envelope 계약의 예외다.
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: `ChatCharacterRepository``characterId``creatorMember``LEFT JOIN FETCH`하는 query를 추가해 반환 직후
연관 객체가 초기화되도록 한다.
- Service/Application: `AiCharacterAdminTargetResolver` 생성.
- Controller/Facade: `kr.co.vividnext.sodalive.v2.api.admin.aicharacter` 하위 신규 controller/facade 패키지 생성.
#### DB migration
- 없음. 신규 DDL, 신규 migration 파일을 만들지 않는다.
#### transaction과 concurrency 고려사항
- target resolver는 read-only transaction으로 동작한다.
- 후속 write use-case는 target 검증을 write transaction 시작부에서 먼저 수행한다.
- target 검증 실패는 transaction rollback-only가 필요 없는 사전 실패로 끝나야 한다.
#### 보안 및 개인정보 위험
- JWT `ROLE_ADMIN`만 신뢰하지 않고 `TokenProvider`가 이미 조회한 `MemberAdapter.member.role`을 신규 prefix 인가에서 함께
확인한다. `TokenProvider`의 전역 authority 계산은 변경하지 않는다.
- JWT ADMIN + 현재 DB 비ADMIN stale claim과 `MemberAdapter`가 아닌 principal은 target resolver 실행 전에 403으로 거부한다.
- `creatorMember`를 인증 principal로 교체하지 않는다.
- `creatorMemberId`를 관리자 입력값으로 신뢰하지 않는다.
- Phase 1 resolver의 잘못된 target 요청은 Hibernate 통계로 DB insert/update/delete 0건을 검증한다. resolver는 S3, 외부 API,
이벤트 발행 의존성을 갖지 않으며 Phase 2~6 write slice에서 각 외부 부작용 0건을 별도 검증한다.
#### acceptance criteria
- JWT ADMIN + 현재 DB ADMIN 요청만 유효한 AI character target을 resolver로 해석할 수 있다.
- JWT 없음·잘못됨·만료·폐기는 401, JWT 비ADMIN 또는 현재 DB 비ADMIN은 403, character 미존재·creatorMember 미존재·target
role/memberKind 불일치는 400이다.
- 위 API 오류는 모두 `ApiResponse.error``Accept-Language`에 따른 KO/EN/JA message를 반환한다. Spring CORS 정책 거부 403
body는 해당 envelope 계약의 예외다.
- 405는 `Allow`, 415는 `Accept` header를 유지하고, 지원하지 않는 응답 media type은 406
`common.error.invalid_request`, `MissingPathVariableException`은 500 `common.error.unknown`으로 반환한다.
- Phase 1 resolver 실패는 DB insert/update/delete가 0건이다. S3, 외부 API, 이벤트 부작용은 해당 의존성이 처음 도입되는
Phase 2~6 write slice에서 검증한다.
#### targeted test
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt`
- Integration Test:
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt`
- RED: resolver 미구현 상태에서 `shouldResolveAiCharacterCreatorMemberForAdminTarget`, `shouldRejectMissingCharacterWithoutSideEffect`, `shouldRejectHumanCreatorMemberWithoutSideEffect` 테스트를 작성해 실패를 확인한다.
- GREEN: resolver와 최소 repository query를 구현해 resolver 테스트를 통과시킨다.
- Controller 권한 테스트: `AiCharacterAdminAuthorizationTest`에서 JWT role × 현재 DB role 매트릭스와 stale claim을 고정하고,
Phase 2~6 controller test에서 신규 endpoint 전체가 같은 이중 인가를 공유하는지 검증한다.
- 오류 계약 테스트: `AiCharacterAdminErrorContractTest`에서 400/401/403/404/405/406/415/500, 405 `Allow`, 415 `Accept`, JWT
filter 예외, KO/EN/JA body와 legacy fallback을 검증한다. CORS는 미매핑 fallback뿐 아니라 실제 mapped endpoint와
`/admin/member/login`, `/member/logout`의 허용·거부 Origin/preflight를 검증하고, 정책 거부 403 body에는 envelope를 요구하지
않는다.
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`
#### 전체 회귀 테스트 영향
- 기존 endpoint를 건드리지 않아야 한다.
- 신규 package 추가로 component scan과 security 설정 충돌이 없어야 한다.
#### rollback 전략
- 신규 v2 admin controller/facade/resolver/error/security package와 관련 테스트를 제거한다.
- `SecurityConfig.kt`, `WebConfig.kt`, `ExceptionHandlerFilter.kt`, `TokenProvider.kt`, `ChatCharacterRepository.kt`의 Phase 1 변경을
함께 되돌린다.
- DB rollback은 없다.
#### 권장 commit 경계
- `feat: add ai character admin target resolver`
- [x] **Task 1.1: resolver RED 테스트 작성**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt`
- RED: 유효 target, missing character, wrong role, wrong memberKind, missing creatorMember, cross-owner fixture를 먼저 작성하고 실패를 확인한다.
- GREEN: 구현 전 Task라 production code를 변경하지 않는다.
- REFACTOR: fixture 중복만 정리하고 테스트 의미는 약화하지 않는다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest`
- 검증 기록: 무엇: resolver RED 테스트. 왜: resolver와 `findByIdWithCreatorMember` 미구현을 실제 실패로 고정하기 위해. 어떻게:
위 명령을 실행했다. 결과: `compileTestKotlin`이 두 미구현 항목으로 실패했다.
- [x] **Task 1.2: resolver 최소 구현**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/application/AiCharacterAdminTargetResolver.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt`
- RED: Task 1.1 실패 테스트가 같은 실패 이유로 남아 있음을 확인한다.
- GREEN: `characterId``ChatCharacter``creatorMember`를 조회하고 strict validation을 적용한다.
- REFACTOR: resolver/repository naming과 예외 메시지를 인접 v2 관례에 맞추고 회귀 테스트를 재실행한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest`
- 검증 기록: 무엇: resolver 최소 구현과 repository/ownership 통합 검증. 왜: mock 기반 테스트만으로 실제 조회 동작을 확인할 수
없었기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest`
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest`를 실행했다.
결과: 두 실행 모두 `BUILD SUCCESSFUL`이었다.
- [x] **Task 1.3: ADMIN 권한 controller smoke 테스트**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- RED: 최소 sample controller 또는 Phase 2 첫 controller 기준 anonymous/non-admin 접근 실패와 admin 접근 성공 테스트를 먼저 작성해 실패를 확인한다.
- GREEN: 공통 security 설정 또는 controller annotation을 최소 구현해 테스트를 통과시킨다.
- REFACTOR: Phase 2~6의 모든 신규 endpoint controller test가 같은 권한 매트릭스를 따르도록 test helper를 정리한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`
- 검증 기록: 무엇: sample route ADMIN 권한 smoke. 왜: 신규 prefix의 ADMIN rule 적용 전후를 확인하기 위해. 어떻게: 위 명령으로
권한 테스트를 실행했다. 결과: 적용 전 권한 실패를 확인했고, `/api/v2/admin/ai-characters/**` ADMIN rule 적용 후
`BUILD SUCCESSFUL`이었다.
- [x] **Task 1.4: JWT claim + 현재 DB ADMIN 이중 인가**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/jwt/TokenProviderTest.kt`
- RED: `UsernamePasswordAuthenticationToken(MemberAdapter(currentMember), token, jwtAuthorities)` fixture로 JWT ADMIN + DB ADMIN
200, JWT 비ADMIN + DB ADMIN 403, JWT ADMIN + DB 비ADMIN stale claim 403, ADMIN authority + 비`MemberAdapter` principal
403을 작성한다. stale claim은 현재 구현에서 200이므로 이 실패를 확인한다.
- GREEN: 신규 prefix의 matcher 하나에서 아래 세 조건을 AND로 묶고 `MemberAdapter`, legacy matcher는 변경하지 않는다.
`TokenProvider`는 JWT subject 누락/비숫자 값이 500으로 누수되지 않도록 `common.error.bad_credentials`로만 보정하며,
전역 authority 계산과 token 저장소 검증 의미는 변경하지 않는다.
```kotlin
.antMatchers("/api/v2/admin/ai-characters/**")
.access(
"hasRole('ADMIN') and " +
"principal instanceof T(kr.co.vividnext.sodalive.member.MemberAdapter) and " +
"principal.member.role == T(kr.co.vividnext.sodalive.member.MemberRole).ADMIN"
)
```
- REFACTOR: production `SecurityConfig` matcher의 비확산은 full-context `AiCharacterAdminAuthorizationTest`의
`/phase1-legacy-sample`로 확인한다. 기존 `AdminAgentReadControllerSecurityTest`, `AdminContentControllerSecurityTest`는 각자의
자체 security chain을 사용하는 controller 회귀 증거로 구분하며 production matcher 비확산의 증거로 해석하지 않는다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest`
- 검증 기록: 무엇: JWT claim과 현재 DB ADMIN 이중 인가. 왜: stale ADMIN claim과 비`MemberAdapter` principal이 허용되면 안
되기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`를
실행했다. 결과: 6개 중 해당 2개가 403 기대 대비 200으로 실패한 RED를 확인했고, 세 조건을 AND로 적용한 뒤 같은 명령이
`BUILD SUCCESSFUL`이었다.
- [x] **Task 1.5: 신규 prefix 오류 envelope/status/i18n 계약**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminApiException.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminExceptionHandler.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminErrorResponseWriter.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/security/AiCharacterAdminSecurityErrorHandler.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/application/AiCharacterAdminTargetResolver.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/i18n/Lang.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt`
- RED: legacy anonymous/revoked-token/지원하지 않는 method 오류의 현재 status/body를 먼저 통과하는 특성화 baseline으로
고정한다. 신규 prefix의 anonymous·잘못된 JWT·폐기 JWT 401, JWT/DB role 불충족 403, invalid request/target 400, 미매핑 경로
404, 지원하지 않는 method 405, 지원하지 않는 media type 415, 예상하지 못한 controller/JWT filter 오류 500을 KO/EN/JA로
parameterized 검증한다. 확정된 캐릭터 관리자 Origin의 404/405/415와 실제 JWT header를 요청하는 미매핑 경로
preflight에 CORS 응답 header가 적용되는지 확인한다. 허용된 Origin의 API 오류는 `success=false`, localized `message`, JSON
content type을 확인하고, 기존 범용 관리자와 creator frontend Origin의 CORS 정책 거부는 403과 CORS 허용 header 부재만
확인한다. 현재 `sendError`, hardcoded KO, 기본 Spring error body 때문에 실패하는 것을 확인한다.
- GREEN: `AiCharacterAdminErrorResponseWriter`가 `Lang.fromAcceptLanguage`와 `SodaMessageSource`로 `ApiResponse.error`를 만들고
JSON을 기록하게 한다. `AiCharacterAdminSecurityErrorHandler`는 401 `common.error.bad_credentials`와 403
`common.error.access_denied`를 위 writer에 위임한다. `SecurityConfig`는 신규 prefix matcher에만 이 handler를 선택하고 기존
`JwtAuthenticationEntryPoint`/`JwtAccessDeniedHandler`를 fallback으로 유지한다. `ExceptionHandlerFilter`도 신규 prefix에서 잡은
JWT 예외만 신규 401 handler로 위임하고 legacy branch는 그대로 둔다.
- GREEN: target resolver는 400 + `common.error.invalid_request`를 가진 `AiCharacterAdminApiException`을 던진다. URI matcher로 신규
prefix에만 적용되는 `AiCharacterAdminExceptionHandler`는 controller 선택 전 오류까지 처리해 명시적 API 예외, request binding
400, method 405, media type 415, controller `AccessDeniedException` 403, 예상하지 못한 오류 500을 각각 정확한 status와
localized `ApiResponse.error`로 반환한다.
낮은 우선순위의 prefix fallback handler는 미매핑 경로를 404로 반환하고 캐릭터 관리자 Origin 전용 CORS 설정을 적용한다.
단순
`SodaException` 교체나 전역
`SodaExceptionHandler` 변경은 하지 않는다.
- GREEN: `ExceptionHandlerFilter`가 잡은 폐기 JWT 등 알려진 인증 실패만 401로 보내고, JWT 처리 중 예상하지 못한 예외는 위
prefix exception handler에 위임해 500 `common.error.unknown`으로 반환한다.
- REFACTOR: raw message key 노출, MVC `LangInterceptor` 의존, legacy 오류 응답 변경이 없는지 확인하고 신규/legacy contract
테스트를 함께 재실행한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAccessDeniedErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest`
- 검증 기록: 무엇: 신규 prefix 오류 envelope/status/i18n 계약. 왜: 신규 401/403/400/500 응답이 기존 body·message와 달랐기
때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`를
실행했다. 결과: 최초 15개 중 13개의 status/content type/message 불일치 RED를 확인했고, prefix 기반 handler와 writer 적용 후
44개 invocation이 모두 통과해 `BUILD SUCCESSFUL`이었다.
- [x] **Task 1.6: Phase 1 코드 리뷰 후속 보완**
- Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md`,
`docs/20260724_AI캐릭터_관리자_API/plan-task.md`,
`src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminExceptionHandler.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/jwt/TokenProviderTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt`
- RED: 서명과 subject는 유효하지만 `auth` claim이 누락, 공백 또는 빈 분할 항목을 포함한 JWT가
`common.error.bad_credentials`로 거부되지 않는 테스트를 작성하고 실패를 확인한다.
- GREEN: `TokenProvider`가 `auth` claim을 authority로 변환하기 전에 문자열 타입, 공백 여부와 각 분할 항목을 검증하고
잘못된 claim은 `common.error.bad_credentials`로 거부하는 최소 구현을 적용한다.
- REFACTOR: Phase 1 테스트 전용 controller를 각 테스트 클래스 내부 nested class로 이동하고 `@TestComponent`로
component scan에서 제외한 뒤 해당 테스트 context에만 명시적으로 import한다. 테스트 fixture 범위만 바꾸는 구조
정리이므로 별도 동작 RED 대신 targeted/full-context 회귀와 application context 시작 성공으로 검증한다.
- CORS: 현재 코드의 캐릭터 관리자 Origin `http://localhost:8888`,
`https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net`만 허용하고 기존 범용
관리자/creator Origin은 거부하는 정책으로 PRD/plan과 CORS 계약 테스트를 동기화한다.
- 2차 리뷰 RED: 신규 prefix의 실제 mapped endpoint와 공유 `/admin/member/login`, `/member/logout`에서 캐릭터 관리자 Origin
요청/preflight가 허용되지 않는 실패를 확인한다. 응답 media type 협상 실패 406, 405 `Allow` header, 415 `Accept` header,
`MissingPathVariableException` 500 계약 테스트를 추가해 현재 동작과의 불일치를 확인한다.
- 2차 리뷰 GREEN: 신규 prefix는 캐릭터 관리자 Origin만 허용하는 기존 정책을 유지하고, 두 공유 인증 경로에만 기존 전역
Origin과 캐릭터 관리자 Origin의 합집합을 적용한다. `AiCharacterAdminExceptionHandler`는 406을
`common.error.invalid_request`, `MissingPathVariableException`을 500 `common.error.unknown`으로 분류하고 405/415 표준
header를 보존한다. Spring CORS 정책 거부 403 body는 localized `ApiResponse.error` envelope 계약에서 제외한다.
- 2차 리뷰 REFACTOR: 실제 mapped endpoint, 두 공유 인증 경로와 미매핑 fallback의 허용·거부 Origin/preflight를 함께
회귀하고, path-specific CORS 확장이 다른 legacy/public 경로로 확산되지 않았는지 확인한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`
- 검증 기록: 무엇: malformed `auth` claim, CORS, HTTP 오류, 테스트 fixture 격리 후속 보완. 왜: claim 누수와 실제 mapped/shared
path CORS·405/406/415/500 계약 누락을 해소하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest`를 실행했다. 결과: malformed claim 7개 invocation RED 후
9개가 통과했고, fallback 보완 후 targeted+legacy 136개가 모두 통과해 `BUILD SUCCESSFUL`이었다. 추가
로그인/로그아웃 CORS 보완 뒤 `AiCharacterAdminLoginJwtIntegrationTest`도 `BUILD SUCCESSFUL`이었다.
- [x] **Task 1.7: Phase 1 후속 리뷰 전체 반영**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`,
`src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/security/AiCharacterAdminSecurityErrorHandler.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt`,
`src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt`
- RED: full-context `AiCharacterAdminLoginJwtIntegrationTest`에 허용 Origin의 `PROPFIND` 400/i18n/CORS, raw double slash 400/CORS,
미허용 Origin 403/`Access-Control-Allow-Origin` 부재와 legacy `RequestRejectedException` 유지 계약을 추가하고, 신규 prefix 세
요청이 `RequestRejectedException`으로 실패하며 legacy fallback은 통과하는 production-before RED를 확인한다.
- GREEN: `SecurityConfig`가 기존 `AiCharacterAdminSecurityErrorHandler`를 global `RequestRejectedHandler`로 등록하고, handler는
신규 prefix에만 400/CORS 계약을 적용한다. `setUnsafeAllowAnyHttpMethod(true)` 없이
허용된 캐릭터 관리자 Origin에는 CORS header를 포함한 400 `common.error.invalid_request`와 현지화된 `ApiResponse.error`를,
미허용 Origin에는 기존 Spring CORS 정책과 같은 body 계약 없는 403을 반환한다. legacy/public은
`DefaultRequestRejectedHandler`에 위임해 기존 `RequestRejectedException` 동작을 유지한다. Spring 5.3의 비표준 method enum
한계는 CORS 검사 request에만 `GET` wrapper를 사용해 우회하고 실제 firewall method 허용 범위는 확장하지 않는다.
- REFACTOR: `AiCharacterAdminErrorContractTest`, `AiCharacterAdminAuthorizationTest`,
`AiCharacterAdminAccessDeniedErrorContractTest`를 production `@SpringBootTest` + MockMvc + EmbeddedRedis full context로 전환한다.
기존 ErrorContract의 표준 `POST` -> GET-only mapping 405, `Allow: GET`, CORS 계약도 full context에서 회귀한다.
`AiCharacterAdminLoginJwtIntegrationTest`는 `MemberTokenRepository.deleteAll()`을 `@AfterEach`에 실행해 Redis token fixture를
cleanup한다. `AiCharacterAdminTargetResolverIntegrationTest`는 repository 조회 직후
`Hibernate.isInitialized(found.creatorMember)`를 단언해 production `LEFT JOIN FETCH`가 실제 회귀 방지에 필요함을 고정한다.
Phase 1 production에는 Bean Validation provider를 추가하지 않고, `MethodArgumentNotValidException`은 test-only endpoint에서
의존성 없이 직접 던져 handler 분기를 검증한다. 이 Task의 실행 명령과 결과는 먼저 이 Task 아래에 기록하고, phase/전체
aggregate만 문서 하단 검증 기록에 누적한다.
- Verify: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest`
- 검증 기록(RED): 무엇: 신규 prefix firewall와 legacy fallback 계약. 왜: production firewall 거부가 신규 API 오류 계약 밖으로
탈출하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest`를
실행했다. 결과: 17개 중 신규 prefix 3개가 `RequestRejectedException`으로 실패했고 legacy fallback 테스트는 통과했다.
- 검증 기록(GREEN): 무엇: prefix-aware global `RequestRejectedHandler`. 왜: 신규 prefix만 400/i18n/CORS로 변환하고 legacy/public
동작을 보존하기 위해. 어떻게: RED와 동일한 명령을 실행했다. 결과: 17/17, `BUILD SUCCESSFUL`을 확인했다.
- 검증 기록(REFACTOR): 무엇: 네 core controller security/error 클래스의 production full-context 계약과 Redis fixture 격리.
왜: slice 설정이 아닌 실제 security/CORS/filter 구성을 검증하기 위해. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAccessDeniedErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest`를
실행했다. 결과: 115/115, failure/error 0, `BUILD SUCCESSFUL`을 확인했다.
- 검증 기록(FETCH JOIN): 무엇: repository 조회 직후 `creatorMember` 초기화의 non-vacuous 회귀 계약. 왜: resolver transaction
내부 접근만으로 fetch join 누락이 가려지는 것을 막기 위해. 어떻게: production query의 `LEFT JOIN FETCH`를 임시로
`LEFT JOIN`으로 바꾸고
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest`를 실행했다.
결과: 4개 중 fetch 테스트 1개가 line 66에서 실패해 `BUILD FAILED`(38초)을 확인했다. 즉시 `LEFT JOIN FETCH`를 복원했고,
복원 상태는 하단 최신 canonical 154/154에 포함되어 통과했다.
- 검증 기록(직접 400 분기): 무엇: malformed JSON의 `HttpMessageNotReadableException`, test-only endpoint에서 의존성 없이 직접
던진 `MethodArgumentNotValidException`, 실제 multipart 필수 part 누락의 `MissingServletRequestPartException` 각 KO/EN/JA 총
9 invocation. 왜: exact `resolvedException` 타입과 localized 400 envelope를 각 handler 분기에서 직접 고정하기 위해. 어떻게:
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest' --rerun-tasks`를 실행했다.
결과: 98/98, failure/error/skipped 0, `BUILD SUCCESSFUL`을 확인했고 production/build dependency 변경은 없었다.
---
### Phase 2: AI 캐릭터 관리 vertical slice
#### 목표
AI 캐릭터 목록/검색/상세/생성/수정/비활성화와 등록용 원작 검색을 신규 ADMIN v2 API로 제공하고 레거시 관리자 동작
parity를 고정한다.
#### 범위와 비범위
- 포함: character CRUD API, 등록용 원작 검색, 외부 캐릭터 API 연동, 이미지 저장, 원작 연결, 언어 감지/번역 이벤트,
creatorMember 생성/표시 정보 동기화 parity.
- 제외: hard delete, cascade delete, 기존 legacy admin endpoint 변경.
#### 선행 Phase 및 의존성
- Phase 1 resolver와 ADMIN 권한 기반이 선행되어야 한다.
- 기존 `ChatCharacterService`, `ChatCharacterCreatorMemberService`, image/S3/event 관련 컴포넌트 동작을 특성화해야 한다.
#### API endpoint와 request/response contract
- 정식 schema는 `api-contract.openapi.json`의 Character operation을 따른다.
- `GET /api/v2/admin/ai-characters?searchTerm=&page=&size=` ->
`ChatCharacterListPageResponse(totalCount, content)` 또는 동일 필드의 검색 response.
- `GET /api/v2/admin/ai-characters/{characterId}` -> nested 필드 전체를 포함한 `ChatCharacterDetailResponse`.
- `POST /api/v2/admin/ai-characters` multipart 필수 `image`, 필수 `request: ChatCharacterRegisterRequest` -> `data: null`.
- `PUT /api/v2/admin/ai-characters/{characterId}` multipart optional `image`, 필수
`request: ChatCharacterUpdateRequest`에서 `id` 제외 -> `data: null`.
- `GET /api/v2/admin/ai-characters/original-works/search?searchTerm=` ->
soft delete를 제외한 `List<OriginalWorkResponse>`.
- update request의 `isActive=false`는 레거시 soft delete 의미다.
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: character 목록/검색용 query adapter 추가 가능.
- Service: 신규 `AiCharacterAdminCharacterFacade/ApplicationService`에서 기존 하위 service를 선택적으로 재사용한다.
- DTO: 신규 admin v2 전용 request/response DTO 생성.
#### DB migration
- 없음.
#### transaction과 concurrency 고려사항
- 생성/수정은 단일 transaction에서 character, relation, creatorMember 표시 정보 동기화를 완료한다.
- 외부 API/S3/event 순서는 기존 레거시 동작 특성화 결과를 따른다.
- 중복 이름 검증은 기존 정책을 유지하며 동시 생성 시 DB/서비스 레벨 실패가 부분 저장을 남기지 않아야 한다.
#### 보안 및 개인정보 위험
- 목록/상세 응답에 AI creatorMember 로그인 credential, token, private storage path를 노출하지 않는다.
- ADMIN 외 접근을 허용하지 않는다.
#### acceptance criteria
- 목록/검색/상세는 AI 캐릭터 관리자 화면에 필요한 필드를 반환한다.
- 생성/수정은 레거시 관리자와 동일한 business side effect를 만든다.
- 비활성화는 `isActive=false`이며 row와 연결 Member/콘텐츠를 삭제하지 않는다.
#### targeted test
- Characterization: `LegacyChatCharacterAdminCharacterizationTest`에서 기존 character admin create/update/soft delete 결과와 외부 API·S3·event failure order, transaction/compensation 계약을 통과 상태로 고정한다.
- V2 RED/GREEN: `AiCharacterAdminCharacterControllerTest`, `AiCharacterAdminCharacterServiceTest`.
- Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`
#### 전체 회귀 테스트 영향
- 기존 `AdminChatCharacterController` 및 public character 조회 응답이 변하지 않아야 한다.
- creatorMember 동기화 기존 테스트가 계속 통과해야 한다.
#### rollback 전략
- 신규 character admin v2 route/facade만 제거한다.
- 이미 생성/수정된 정상 데이터는 기존 관리자와 같은 domain 데이터라 별도 schema rollback이 없다.
#### 권장 commit 경계
- `feat: add ai character admin character slice`
- [x] **Task 2.1: 기존 character parity 특성화 baseline 고정**
- **Goal 이력 `P2-H1`:** 기존 character admin 동작을 신규 v2 구현의 비교 기준으로 고정했다.
- **완료 증거:** 아래 특성화 테스트 RED/GREEN/REFACTOR 기록과 실제 명령 결과.
- **범위 밖:** 신규 v2 endpoint 구현.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
- CHARACTERIZE: 기존 character admin 구현을 대상으로 중복 이름, 외부 API·S3·event 호출/실패 순서와 transaction/compensation, original work 연결, 언어 감지/번역 이벤트, creatorMember 표시 정보 동기화, `isActive=false` 및 연결 Member/콘텐츠 미삭제 결과를 고정한다.
- BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- REFACTOR: fixture와 assertion naming만 정리하고 parity baseline은 변경하지 않는다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest`
- 검증 기록(RED): 무엇: 기존 character admin baseline 특성화 테스트. 왜: 신규 v2 production code 전에 기존 생성/수정/비활성화/원작/언어 이벤트/creatorMember·콘텐츠 보존 계약을 고정하기 위해. 어떻게: 위 테스트를 추가하고 동일 명령을 실행했다. 결과: 이벤트 baseline 추가 직후 Mockito/S3 stub과 request JSON 누락으로 2회 실패해 RED를 확인했다.
- 검증 기록(GREEN): 무엇: Task 2.1 baseline 통과. 왜: 기존 구현이 현재 특성화 계약을 만족하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest`를 실행했다. 결과: `BUILD SUCCESSFUL`을 확인했다.
- 검증 기록(REFACTOR): 무엇: Kotlin style과 리뷰 gate. 왜: 테스트-only baseline의 품질과 계획 준수 여부를 확인하기 위해. 어떻게: `./gradlew ktlintCheck`와 spec/code-quality read-only review를 실행했다. 결과: `BUILD SUCCESSFUL`, 두 리뷰 모두 `APPROVED`였다.
- [x] **Task 2.2: character controller/facade/DTO 구현**
- **Goal 이력 `P2-H2`:** 캐릭터 목록·상세·생성·수정의 최초 v2 API 구현을 제공했다.
- **완료 증거:** 아래 controller test와 `ktlintCheck` 기록. 이 이력만으로 Phase 2 Gate 통과를 의미하지 않는다.
- **범위 밖:** 후속 심층 리뷰에서 확정되는 누락·회귀 보완.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/*`
- RED: Task 2.1 baseline에서 옮긴 `AiCharacterAdminCharacterServiceTest`와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
- RED: 현재 신규 dependency 금지 제약에 따라 기존 Spring binding 또는 수동 validation 전략을 우선하고, 실제 DTO의 invalid
요청 통합 계약을 추가한다. Bean Validation provider가 반드시 필요하면 구현 전에 PRD/계획과 dependency 허용 범위를
명시적으로 변경하고 승인을 받는다.
- GREEN: endpoint contract summary의 character endpoint를 구현하고 목록/검색 `page/size` 기본값·최소·최대 보정을 적용한다.
- REFACTOR: 신규 DTO가 legacy/public DTO를 외부 계약으로 재노출하지 않는지 확인하고 회귀 테스트를 재실행한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`
- 검증 기록(RED): 무엇: v2 character controller 계약 테스트 7개. 왜: 신규 route가 목록/검색/상세/생성/수정과 ADMIN 경계를 제공하지 않음을 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`를 실행했다. 결과: 모든 기대 endpoint가 미구현 route의 404를 반환해 7개가 실패했다.
- 검증 기록(GREEN): 무엇: 목록/검색 page-size 보정, target resolver 상세 거부, multipart 생성/수정, AI creatorMember 동기화, soft delete, invalid JSON, ADMIN 인가. 왜: Task 2.2 API 계약과 레거시 부작용 재사용을 확인하기 위해. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. 결과: controller 8개와 Task 2.1 baseline 7개가 모두 통과했다.
- 검증 기록(REFACTOR): 무엇: 신규 v2 DTO 경계와 Kotlin style. 왜: legacy/public DTO 비노출과 코드 스타일을 확인하기 위해. 어떻게: `./gradlew ktlintCheck`를 실행했다. 결과: `BUILD SUCCESSFUL`을 확인했다.
- [x] **Task 2.3: Phase 2 요구사항·계약·코드 리뷰**
**Goal 실행 `P2-R1`:** PRD Feature B와 Endpoint Contract Summary를 Phase 2 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다.
- **추적 review ID:** `REV-001`, `REV-002`, `REV-003`, `REV-007`, `REV-008`.
- **시작 조건:** `P2-H1`, `P2-H2` 산출물과 검증 기록 존재.
- **완료 증거:** `docs/sample/sample-review.md` 형식의 리뷰 문서, endpoint/side-effect 추적표, 모든 후보의 확정·오탐·보류 판정,
후속 Goal 연결과 Progress 기록.
- **범위 밖:** 리뷰 도중 production code 수정, Phase 3 이후 기능 검토.
- **TDD 예외 사유:** 구현이 아닌 read-only 리뷰 Task다.
- **대체 검증 방법:** PRD·계약·production·test를 대조하고 현재 focused test를 실행해 관찰 결과를 리뷰 문서에 기록한다.
**Files:**
- Create: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- [x] 목록·검색·상세·생성·수정·비활성화 endpoint와 DTO 필드를 PRD/계약에 1:1로 추적한다.
- [x] 중복 이름, 외부 API, S3, 원작, 언어 이벤트, creatorMember 동기화와 실패 순서를 코드·test에 추적한다.
- [x] ADMIN 이중 인가, pagination, multipart/binding, KO/EN/JA 오류, private 정보 비노출 계약을 확인한다.
- [x] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`의 실제 결과를 기록한다.
- [x] 후보를 확정·오탐·보류로 판정하고 확정 항목을 아래 세부 Goal에 연결하거나 새 회귀 수정 Goal을 계획에 먼저 추가한다.
- 검증 기록: 무엇: `P2-R1` Phase 2 character slice read-only 리뷰. 왜: `P2-H1`, `P2-H2` 완료 이력만으로 Phase 2 Gate를 통과할 수 있는지 판정하기 위해. 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`를 작성하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 51s`였고, `REV-001`~`REV-003`, `REV-007`, `REV-008`은 기존 `P2-T3`~`P2-T6` 보완 Goal에 연결된 확정 finding으로 판정했다.
- [x] **Task 2.4: 캐릭터 목록·검색·상세 보완**
**Goal 실행 `P2-T3`:** 캐릭터 조회 API의 검색·pagination·응답·target 계약을 독립적으로 검증하고 확정된 누락을 최소 수정한다.
- **추적 review ID:** `REV-007` 중 목록 응답 field set과 DTO 경계.
- **시작 조건:** `P2-R1` 완료와 관련 review ID 확정. 확정 finding이 없으면 Decision Log에 `해당 없음` 근거를 남긴다.
- **완료 증거:** 조회 전용 RED/GREEN, focused test, DTO 비노출 점검과 Progress 기록.
- **범위 밖:** 생성, 수정, 외부 API·S3 mutation.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
- [x] **RED:** 목록 item의 exact JSON key를 고정하고 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`가 노출되는 현재 동작을 실패로 재현한다.
- [x] **RED 확인:** `page=0`, `size` 기본 20·최소 20·최대 50, 검색·`hasNext`와 상세 target 불변식의 경계 test를 실행해 의도한 assertion 실패를 확인한다.
- [x] **GREEN:** 목록 전용 DTO와 mapper를 최소 구현해 계약 field만 반환하고 credential·token·private path를 노출하지 않는다.
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 exact 목록 field set과 pagination·target 계약이 모두 통과하는지 확인한다.
- [x] **REFACTOR:** 상세 UTC field 계약을 유지하면서 목록/상세 DTO 의존 방향을 점검하고 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 목록 item의 상세 전용 field 비노출 계약. 왜: 목록 응답이 상세 DTO를 재사용해 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerTest`에 `doesNotExist()` assertion 3개를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`를 실행했다. 결과: `목록은 음수 page와 최소 미만 size를 기본값으로 보정한다`가 line 66에서 실패해 RED를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: 목록 전용 DTO와 mapper 분리. 왜: 상세 응답 계약을 유지하면서 목록 field set만 Endpoint Contract Summary에 맞추기 위해. 어떻게: `AiCharacterAdminCharacterListItemResponse`와 `toListItemResponse`를 추가하고 목록 mapping만 교체한 뒤 같은 focused test와 `./gradlew ktlintCheck`를 실행했다. 결과: 둘 다 `BUILD SUCCESSFUL`이었다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`
- [x] **Task 2.5: 캐릭터 생성 흐름 보완**
**Goal 실행 `P2-T4`:** 캐릭터 생성의 중복 검증, 외부 API, 이미지, 원작, creatorMember와 이벤트 흐름을 parity 기준으로 완결한다.
- **추적 review ID:** `REV-002`, `REV-003`, `REV-007` 중 생성 request·`characterType` 계약.
- **시작 조건:** `P2-T3` 완료와 생성 관련 확정 review ID.
- **완료 증거:** 정상 생성 및 실패 지점별 RED/GREEN, DB/S3/외부 API/event 결과, focused/legacy test와 Progress 기록.
- **범위 밖:** 기존 external character API 계약 변경, 캐릭터 수정·비활성화.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterExternalApiClient.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterImageStorage.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt`
- [x] `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 invalid `characterType`의 canonical request 계약을 Endpoint Contract Summary·legacy 특성화 결과로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다.
- [x] **RED:** 확정된 문서 JSON의 역직렬화·반영, 중복 이름, 외부 API 실패, S3 실패와 존재하지 않는 `originalWorkId` 실패를 각각 재현한다.
- [x] **RED 확인:** 실패 지점별 DB row·creatorMember·원작 연결·S3 객체·외부 캐릭터·event 결과와 호출 순서를 단언해 현재 부분 저장 또는 고아 부작용을 확인한다.
- [x] **GREEN:** 모든 DB 참조를 외부 부작용 전에 검증하고, legacy parity에 맞는 최소 보상/정리 경계로 정상 생성과 실패 원자성을 통과시킨다.
- [x] **GREEN 확인:** 같은 생성 focused/characterization test를 다시 실행해 정상 결과와 실패 지점별 잔존 상태가 확정 계약과 일치하는지 확인한다.
- [x] **REFACTOR:** creatorMember 표시 정보와 언어 이벤트를 포함한 focused/legacy characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: v2 생성 request와 실패 순서 계약. 왜: 외부 ID·생성 활성 상태가 무시되고, 존재하지 않는 원작이 외부 생성 뒤에 실패했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 서버 소유 field, invalid `characterType`, 중복 이름, 원작, 외부 API, S3 실패와 creatorMember·언어 이벤트 assertion을 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 서버 소유 field는 400 기대 대비 200, 원작 실패는 외부 요청 0 기대 대비 1로 실패했다. S3 DB assertion의 최초 실패는 class-level test transaction 관찰 오류였으므로 해당 test만 transaction 밖에서 재실행해 DB rollback을 확인했다.
- 검증 기록(GREEN): 무엇: 서버 소유 create field 거부와 원작 선검증. 왜: `externalCharacterId`/생성 `isActive`를 client 입력으로 받지 않고, 존재하지 않는 원작에서 외부 캐릭터를 만들지 않기 위해. 어떻게: create DTO의 수신 field를 명시적으로 거부하고 facade에서 원작을 외부 호출 전에 조회한 뒤 같은 focused test를 실행했다. 결과: 10개 test가 `BUILD SUCCESSFUL`이었다.
- 검증 기록(REFACTOR): 무엇: 생성 focused/legacy parity 회귀와 formatting. 왜: 정상 원작 연결, AI creatorMember 표시 정보, 언어 감지 event, duplicate·외부 API·S3 실패의 결과를 legacy 특성화와 함께 유지하기 위해. 어떻게: 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused/legacy 명령은 `BUILD SUCCESSFUL in 40s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 11s`였다. 전체 `./gradlew test`는 task 범위가 character create slice이고 focused/legacy 명령으로 직접 영향 범위를 확인하므로 실행하지 않았다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest`
- [x] **Task 2.6: 캐릭터 수정·비활성화 흐름 보완**
**Goal 실행 `P2-T5`:** 캐릭터 수정과 `isActive=false`가 표시 정보를 동기화하고 연결 Member·콘텐츠를 보존하도록 완결한다.
- **추적 review ID:** `REV-002`, `REV-003`, `REV-007` 중 수정 request·응답·soft-delete parity.
- **시작 조건:** `P2-T4` 완료와 수정·비활성화 관련 확정 review ID.
- **완료 증거:** 수정·이미지 유지/교체·soft delete RED/GREEN, 보존/no-partial-update 검증과 Progress 기록.
- **범위 밖:** hard delete, cascade delete, 복원 API.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterExternalApiClient.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterImageStorage.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt`
- [x] `externalCharacterId`, 일반 수정과 `isActive=false` 혼합 요청, invalid `characterType`의 canonical update 계약을 확정하고 충돌 시 Decision Log를 먼저 갱신한다.
- [x] **RED:** 문서 PUT JSON의 field 반영, soft delete와 일반 수정 혼합, image 동시 요청, 외부 수정 성공 후 S3/DB 실패, `updatedAtUtc`의 flush 전 mapping을 각각 재현한다.
- [x] **RED 확인:** soft delete 성공·실패에서 row·Member·콘텐츠 보존, 미참조 S3 객체 0건, 외부/DB 상태 일치와 응답 timestamp가 후속 GET과 같은지 확인한다.
- [x] **GREEN:** 확정 계약에 맞춰 혼합 요청을 명시적으로 처리하고, 불필요한 upload를 차단하며 외부/S3/DB 보상 경계와 flush 후 response mapping을 최소 구현한다.
- [x] **GREEN 확인:** 같은 수정 focused/characterization test를 다시 실행해 field 반영, 보상 결과, soft-delete 보존과 timestamp가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** creatorMember 표시 정보·번역 event와 legacy `characterType` 동작을 포함한 focused/characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: PUT의 서버 소유 external ID, `isActive=false` 혼합 image, 존재하지 않는 원작과 flush 전 timestamp 계약. 왜: 기존 구현이 client external ID를 무시하고 soft delete 전에 image를 업로드하며, 원작 검증 후 외부 수정과 이전 `updatedAtUtc`를 반환했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 해당 회귀, image 유지·교체, S3 실패 경계, creatorMember·콘텐츠 보존과 번역 event test를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 16건 중 server-owned ID, mixed soft delete, missing original work, post-flush timestamp 4건이 의도대로 실패했다.
- 검증 기록(GREEN/REFACTOR): 무엇: canonical update 계약과 post-flush 응답. 왜: soft delete의 고아 image를 막고 정상 수정의 creatorMember 동기화·번역 event·timestamp 및 외부/S3/DB 실패 경계를 고정하기 위해. 어떻게: `externalCharacterId` 명시 거부, `isActive=false` 혼합 거부, 원작 선검증, `flush()` 후 response mapping을 적용한 뒤 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: 모두 `BUILD SUCCESSFUL`이었다. 전체 `./gradlew test`는 변경 범위가 character update slice이고 지정 focused/legacy 회귀가 직접 영향 범위를 포함하므로 실행하지 않았다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest`
- [x] **Task 2.7: Phase 2 보안·오류·회귀 보완**
**Goal 실행 `P2-T6`:** Phase 2의 모든 endpoint가 공통 ADMIN·오류·CORS 계약을 공유하고 legacy/public 계약을 회귀시키지 않음을 고정한다.
- **추적 review ID:** `REV-001`, `REV-007`, `REV-008`.
- **시작 조건:** `P2-T5` 완료 또는 앞선 Goal의 근거 있는 `해당 없음` 판정.
- **완료 증거:** endpoint 권한 매트릭스, 정확한 오류 status/key/KO·EN·JA, legacy 회귀와 Progress 기록.
- **범위 밖:** Phase 1 공통 security/error 구조 재설계, Phase 3 기능.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **RED:** `/api/v2/admin/ai-characters/unmapped-path`가 detail `Long` binding에 잡혀 404 대신 400이 되는 KO/EN/JA·허용 Origin CORS 4건을 현재 Phase 1 오류 계약 test로 재현한다.
- [x] **RED:** 목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim과 binding·multipart·domain/client/server 오류의 exact status/key/KO·EN·JA를 parameterized test로 고정한다.
- [x] **RED 확인:** 오류 계약과 실제 endpoint matrix를 실행해 404 회귀 4건과 누락된 인가·i18n assertion이 의도대로 실패하는지 확인한다.
- [x] **GREEN:** numeric `characterId`만 resource handler에 매핑되도록 최소 수정하고, Phase 2 오류 의미를 확정된 message key와 `ApiResponse.error`로 반환한다.
- [x] **GREEN 확인:** 같은 오류·인가 focused test를 다시 실행해 실제 endpoint의 status/header/envelope와 KO/EN/JA가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminCharacterServiceTest` 참조 및 과거 test 수 기록은 삭제하지 않고 정정 기록을 누적한다.
- [x] 기존 admin/public character contract, 신규 DTO 의존 방향과 Phase 2 focused test·`ktlintCheck` 결과를 Progress에 기록한다.
- 정정 기록: `AiCharacterAdminCharacterServiceTest`는 현재 존재하지 않는 과거 계획 참조다. P2-T6의 실제 범위는 `AiCharacterAdminCharacterControllerTest`, `AiCharacterAdminCharacterControllerMutationTest`, 공통 `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`이며 기존 완료 이력과 과거 test 수 기록은 삭제하지 않았다.
- 검증 기록(RED): 무엇: 실제 character controller가 문자열 미매핑 경로를 404 fallback으로 넘기는지와 endpoint matrix. 왜: `/{characterId}`의 `Long` binding이 fallback 404 계약을 400으로 바꾸고 있었기 때문이다. 어떻게: 아래 Verify 명령을 production 변경 전 실행했다. 결과: 전체 146건 중 `unmapped-path` KO/EN/JA와 허용 Origin CORS 4건만 404 기대 대비 400으로 실패했고, 새 목록 binding·상세 target·생성/수정 multipart i18n 및 실제 endpoint non-ADMIN/CORS assertion은 통과했다.
- 검증 기록(GREEN/REFACTOR): 무엇: numeric path 제약과 Phase 2 ADMIN/error/CORS/legacy 회귀. 왜: 문자열 segment는 fallback 404로, 숫자 resource와 기존 public/legacy는 기존 계약으로 유지해야 하기 때문이다. 어떻게: controller의 GET/PUT path를 `[0-9]+`로 제한한 뒤 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused command는 `BUILD SUCCESSFUL in 1m 26s`, ktlint는 `BUILD SUCCESSFUL in 34s`였고, P2-GATE는 이 Task 범위 밖으로 미완료 상태를 유지한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`
#### Phase 2 Gate
**Goal 실행 `P2-GATE`:** Phase 2 캐릭터 관리의 PRD 추적성, 정상·실패 흐름과 legacy 회귀를 최종 판정한다.
- [x] **`P2-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P2-R1`, `P2-T3`~`P2-T6` 완료 또는 근거 있는 `해당 없음` 판정.
- **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록.
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, Phase 3 기능 수정.
- [x] `REV-001`~`REV-003`, `REV-007`, `REV-008`의 계약 결정·failure matrix·수정 test와 실제 결과가 각 소유 Goal의 Progress에 연결됐다.
- [x] 캐릭터 목록 exact key, 문서 mutation JSON, 동시 중복 결과, 외부/S3/DB 보상, post-flush `updatedAtUtc`와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다.
- [x] 완료 이력의 누락 test 파일·test 수·failure-order 증거는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다.
- 검증 기록: 무엇: `P2-GATE` Phase 2 최종 판정. 왜: `P2-R1`, `P2-T3`~`P2-T6`의 확정 finding 처리와 Gate 명령 성공을 확인하기 위해. 어떻게: 아래 세 Gate 명령을 실행했다. 결과: character focused 명령은 최초 병렬 실행 중 XML test result write 충돌로 실패했으나 동일 명령 단독 재실행은 `BUILD SUCCESSFUL in 1m 11s`였다. authorization/error 명령은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. `git diff --check`도 통과했다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
**Expected:** 모든 명령 exit code 0, Phase 2 review의 후보·확정 미처리 항목 0건, 관련 legacy/public 계약 diff 없음.
#### Phase 2 후속 리뷰 보완
- [x] **Task 2.8: 실제 character endpoint 보안·오류·실패 경계 증거 보강**
**Goal 실행 `P2-R2`:** `REV-009`에서 확인한 실제 character endpoint별 인가·CORS·오류와 mutation 실패 경계를 non-vacuous 회귀 test로 고정한다.
- **추적 review ID:** `REV-009`.
- **시작 조건:** 기존 `P2-GATE` 완료 이력과 `phase2-character-review.md` 2차 리뷰 판정 존재.
- **완료 증거:** 아래 실제 endpoint test, 공통 authorization/error 회귀, `ktlintCheck`와 Progress 기록.
- **범위 밖:** 공통 Phase 1 security/error 재설계, external API 보상 endpoint·신규 DDL 추가, Phase 3 기능.
- **TDD 예외 사유:** 현재 production 실패가 아니라 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
- **대체 검증 방법:** 실제 endpoint test를 먼저 추가하고, 현재 동작이 계약을 만족하면 production code 변경 없이 통과 증거를 기록한다. 계약 불일치가 재현될 때만 해당 assertion의 RED를 확인하고 최소 수정한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **CONTRACT TEST:** 목록·상세·생성·수정 실제 경로에서 JWT role × 현재 DB role과 stale ADMIN claim을 parameterized test로 고정한다.
- [x] **CONTRACT TEST:** 실제 GET/POST/PUT의 허용·거부 Origin/preflight와 대표 binding·domain·client·server 오류의 exact status/key/KO·EN·JA envelope를 고정한다.
- [x] **FAILURE TEST:** 중복·원작·external API·S3·DB 실패에서 DB/creatorMember/originalWork/S3/external/event 결과를 직접 단언하고 기존 non-compensated external 경계를 유지한다.
- [x] **GREEN:** 새 test가 현재 계약 불일치를 재현할 때만 가장 작은 production 수정으로 통과시키고, 이미 통과하면 production code를 변경하지 않는다.
- [x] **REFACTOR:** character focused와 legacy characterization, 공통 authorization/error 및 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록: 무엇: `REV-009`의 실제 character endpoint 증거 보강. 왜: 기존 Gate 기록이 네 endpoint의 stale claim, allow/deny CORS preflight, KO/EN/JA 실패 envelope를 직접 매트릭스로 고정했다는 증거가 부족했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerTest`에 목록·상세·생성·수정 실제 경로의 stale ADMIN claim 403과 허용/거부 Origin preflight를 추가하고, `AiCharacterAdminCharacterControllerMutationTest`의 external API 실패, 생성 S3 실패, 수정 S3 실패를 KO/EN/JA envelope와 잔존 DB/S3/external 상태 단언으로 확장했다. 결과: production code 변경 없이 아래 focused 명령이 `BUILD SUCCESSFUL in 1m 47s`였다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 2 후속 리뷰 Gate
**Goal 실행 `P2-R2-GATE`:** `REV-009`의 직접 증거를 재검토하고 Phase 2 후속 리뷰를 종결한다.
- [x] **`P2-R2-GATE` 완료:** `P2-R2` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
- **시작 조건:** `P2-R2` 완료.
- **완료 증거:** `REV-009` 수정 완료, 위 두 명령 성공, `phase2-character-review.md` 최신 결론과 Progress 동기화.
- **범위 밖:** 기존 `P2-GATE` 이력 수정, Phase 3 production 변경.
- 검증 기록: 무엇: `P2-R2-GATE` 후속 리뷰 종결. 왜: `REV-009`의 직접 증거가 추가됐고 Phase 3 후속 보완으로 넘어갈 수 있는지 판정하기 위해. 어떻게: `phase2-character-review.md`에 3차 후속 검증 기록을 누적하고 위 focused 명령을 fresh 실행했다. 결과: `BUILD SUCCESSFUL in 1m 47s`였고, `REV-009`는 처리 완료로 판정했다. `ktlintCheck`는 Phase 3 후속 보완까지 완료한 뒤 공통으로 실행해 전체 후속 범위 검증 기록에 남긴다.
#### Phase 2 4차 리뷰 보완
- [x] **Task 2.9: character DB·event 실패 경계 증거 보강**
**Goal 실행 `P2-R3`:** `REV-012`에서 남은 character 생성·수정의 DB/event 실패 후 내부·외부 부작용 경계를 실제 흐름으로 고정한다.
- **추적 review ID:** `REV-012`.
- **시작 조건:** 기존 `P2-R2-GATE` 완료 이력과 `phase2-character-review.md` 4차 리뷰 판정 존재.
- **완료 증거:** 실제 mutation failure test, character/common 회귀, `ktlintCheck`와 Progress 기록.
- **범위 밖:** external character API 보상 endpoint 추가, 신규 DDL, Phase 3 이후 production 변경.
- **TDD 예외 사유:** 현재 production 실패가 아니라 `Task 2.8` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
- **대체 검증 방법:** 실제 DB flush/save 또는 event publish 실패를 먼저 재현하고, 현재 transaction·비보상 경계가 계약과 일치하면 production code 변경 없이 관찰 결과를 고정한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **FAILURE CHARACTERIZATION:** 생성·수정의 DB flush/save와 event publish 실패를 실제 transaction 경계에서 재현한다.
- [x] **CONTRACT TEST:** 각 실패 뒤 ChatCharacter·creatorMember·originalWork·event와 이미 발생한 external/S3 결과를 직접 단언한다.
- [x] **CONTRACT TEST:** 대표 실패의 exact HTTP status와 KO/EN/JA `ApiResponse.error`를 실제 mutation endpoint에서 확인한다.
- [x] **GREEN:** 현재 계약 위반이 재현될 때만 최소 production 수정으로 통과시키고, 기존 비보상 경계와 일치하면 test-only로 종료한다.
- [x] **REFACTOR:** character/common focused 회귀와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
- 검증 기록: 무엇: `REV-012`의 character 생성·수정 실패 경계 증거를 보강했다. 왜: 기존 완료 기록이 external/S3 실패는 확인했지만 event publish 실패와 KO/EN/JA 대표 실패 경계를 직접 고정하지 않았기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 external/S3 실패 locale matrix와 facade 직접 event 실패 특성화를 추가했다. 결과: production code 변경 없이 mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`였다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 2 4차 리뷰 Gate
**Goal 실행 `P2-R3-GATE`:** `REV-012`의 DB/event 실패 경계 증거를 재검토하고 Phase 2 4차 리뷰를 종결한다.
- [x] **`P2-R3-GATE` 완료:** `P2-R3` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
- **시작 조건:** `P2-R3` 완료.
- **완료 증거:** `REV-012` 처리 완료, 위 두 명령 성공, `phase2-character-review.md` 최신 결론과 Progress 동기화.
- **범위 밖:** 기존 Phase 2 완료 이력 수정, Phase 3 production 변경.
- 검증 기록: 무엇: Phase 2 4차 리뷰의 `REV-012` 처리를 종결했다. 왜: Phase 3 4차 보완으로 넘어가기 전 character failure evidence 완료 여부를 판정하기 위해. 어떻게: character mutation focused test와 최종 `ktlintCheck`를 실행하고 `phase2-character-review.md`를 처리 완료로 갱신했다. 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다.
#### Phase 2 5차 리뷰 보완
- [x] **Task 2.10: character 실제 transaction DB·event 실패 경계 완결**
**Goal 실행 `P2-R4`:** `REV-015`의 생성·수정 persistence/event 실패를 actual endpoint와 Spring transaction 경계에서 재현하고 내부 rollback·외부 비보상 결과를 고정한다.
- **추적 review ID:** `REV-015`.
- **시작 조건:** 기존 `P2-R3-GATE` 완료 이력과 `phase2-character-review.md` 5차 리뷰 판정 존재.
- **완료 증거:** actual POST/PUT failure test, transaction 종료 뒤 DB 재조회, character/common 회귀와 `ktlintCheck` 결과 및 Progress 기록.
- **범위 밖:** external character API 보상 endpoint, S3 object 정리 정책, 신규 DDL, Phase 3 이후 production 변경.
- **TDD 예외 사유:** 현재 production 결함보다 기존 `REV-012` 완료 기록의 transaction/rollback 직접 증거 누락이 확정된 회귀 검증 Task다.
- **대체 검증 방법:** test transaction 밖 actual endpoint와 repository/publisher failure injection으로 production proxy를 통과시키고, 요청 종료 뒤 내부 DB와 외부 interaction을 재조회한다. 계약 위반이 재현될 때만 최소 production 수정으로 전환한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **FAILURE CHARACTERIZATION:** `@Transactional(propagation = Propagation.NOT_SUPPORTED)` actual POST/PUT에서 `ApplicationEventPublisher` 실패를 주입하고 500 `common.error.unknown` envelope을 확인한다.
- [x] **PERSISTENCE FAILURE:** `@SpyBean ChatCharacterRepository`로 생성 `save`와 수정 `flush()` 실패를 각각 주입해 external 호출 뒤 transaction rollback 순서를 재현한다.
- [x] **CONTRACT TEST:** 생성 실패 뒤 ChatCharacter·creatorMember·originalWork 부재, 수정 실패 뒤 기존 character·creatorMember·originalWork 상태 유지, S3/event interaction과 external 호출 횟수를 직접 단언한다.
- [x] **CONTRACT TEST:** 대표 event/persistence 실패의 KO/EN/JA exact HTTP status/message를 actual mutation endpoint에서 확인한다.
- [x] **GREEN:** 현재 transaction 계약 위반이 재현될 때만 가장 작은 production 수정으로 통과시키고, 기존 rollback·비보상 경계와 일치하면 test-only로 종료한다.
- [x] **REFACTOR:** direct `createFacade` event failure test를 actual endpoint 증거로 대체하거나 역할을 명확히 축소하고 character/common 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
- 검증 기록: 무엇: `REV-015`의 생성·수정 event/save/flush 실패가 실제 MockMvc POST/PUT와 facade transaction 경계를 통과해 내부 DB rollback과 외부 비보상 호출을 보이는지 고정했다. 왜: direct facade 호출은 Spring transaction proxy 및 요청 종료 뒤 DB 상태를 증명하지 못했기 때문이다. 어떻게: `NOT_SUPPORTED` test에서 facade proxy target의 publisher mock과 `@SpyBean ChatCharacterRepository` failure를 주입하고, `TransactionTemplate` 재조회로 상태를 확인했다. 결과: 초기 RED는 `@MockBean`이 이미 생성된 facade field를 대체하지 못해 실제 event listener가 실행되고 200이 반환된 것으로 확인됐으며, 실제 proxy target에 같은 mock을 교체한 뒤 KO/EN/JA 500 envelope, 생성 내부 state 부재, 수정 기존 state 유지, 외부 호출 1회와 image 없는 S3 미호출이 통과했다. production 변경은 없었다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 8s`, 영향 범위 character/auth/error 회귀는 `BUILD SUCCESSFUL in 1m 59s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 42s`였다.
#### Phase 2 5차 리뷰 Gate
**Goal 실행 `P2-R4-GATE`:** `REV-015`의 actual transaction·rollback 증거를 재검토하고 Phase 2 5차 리뷰를 종결한다.
- [x] **`P2-R4-GATE` 완료:** `P2-R4` 완료 후 위 명령을 fresh 실행하고 리뷰 문서·Progress를 갱신한다.
- **시작 조건:** `P2-R4` 완료.
- **완료 증거:** `REV-015` 수정 완료, actual endpoint transaction evidence, focused/영향 범위 회귀와 lint 성공.
- **범위 밖:** 기존 Phase 2 완료 이력 수정, Phase 3 production 변경.
- 진행 기록: `P2-R4` 구현과 fresh 검증을 완료했고, 5차 재리뷰에서 `REV-015` 보완 완료와 Phase 2 Gate 종료를 확인했다.
#### Phase 2 6차 리뷰 보완
- [x] **Task 2.11: 캐릭터 생성 Endpoint Contract Summary 동기화**
**Goal 실행 `P2-R5`:** `REV-018`의 캐릭터 생성 예시를 `DEC-P2-T4-001` 및 production request 계약과 일치시키고
actual endpoint 회귀로 확인한다.
- **추적 review ID:** `REV-018`.
- **시작 조건:** 기존 `P2-R4-GATE` 완료 이력과 `phase2-character-review.md` 6차 리뷰 판정 존재.
- **완료 증거:** Endpoint Contract Summary 생성 JSON 정정, 정상 생성·서버 소유 field 거부 actual endpoint 회귀,
`git diff --check`와 Progress 기록.
- **범위 밖:** production DTO/facade 변경, 캐릭터 생성 behavior 변경, 외부 API 계약 변경, Phase 3 이후 production 변경.
- **TDD 예외 사유:** production과 기존 actual endpoint test는 확정 계약을 충족하고 문서 예시만 반대로 남은 문서 정합성
수정 Task다.
- **대체 검증 방법:** 생성 예시의 exact field를 문자열 검색으로 확인하고 기존 정상 생성·서버 소유 field 거부 actual endpoint
test를 재실행한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- [x] **CONTRACT DOC:** POST 생성 예시에 필수 `systemPrompt`를 추가하고 request의 `externalCharacterId`, `isActive`를
제거한다.
- [x] **CONTRACT DOC 확인:** 생성 예시와 `DEC-P2-T4-001`을 대조해 request field와 response 전용 field가 일치하는지
확인한다.
- [x] **CONTRACT TEST:** `systemPrompt`를 포함하고 서버 소유 field를 제외한 actual POST가 성공하며 response에 외부 API가
반환한 `externalCharacterId`와 서버 생성 `isActive=true`가 있는지 확인한다.
- [x] **REJECTION TEST:** `externalCharacterId` 또는 생성 `isActive`가 포함된 actual POST가 외부/S3/DB/event 부작용 전
400으로 거부되는 기존 회귀를 확인한다.
- [x] **REFACTOR:** production 변경 없이 문서 diff와 focused test 결과를 Progress에 기록한다.
- 검증 기록: 무엇: `REV-018`의 캐릭터 생성 Endpoint Contract Summary 예시를 production 생성 request 계약과 동기화했다. 왜: 예시가 필수 `systemPrompt`를 누락하고 서버 소유 `externalCharacterId`, `isActive`를 포함했기 때문이다. 어떻게: `rg -n -A 12 'POST /api/v2/admin/ai-characters' docs/20260724_AI캐릭터_관리자_API/plan-task.md`로 예시 field를 확인하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 예시는 `systemPrompt` 포함 및 서버 소유 field 제외로 확인됐고 focused test는 `BUILD SUCCESSFUL in 30s`였다.
```bash
rg -n -A 12 'POST /api/v2/admin/ai-characters' docs/20260724_AI캐릭터_관리자_API/plan-task.md
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest
git diff --check
```
#### Phase 2 6차 리뷰 Gate
**Goal 실행 `P2-R5-GATE`:** `REV-018`의 문서 계약 정합성을 재검토하고 Phase 2 6차 리뷰를 종결한다.
- [x] **`P2-R5-GATE` 완료:** `P2-R5` 완료 후 위 문서/focused 검증을 fresh 실행하고 리뷰 문서·Progress를 갱신한다.
- **시작 조건:** `P2-R5` 완료.
- **완료 증거:** `REV-018` 처리 완료, Endpoint Contract Summary·Decision Log·production 계약 일치, focused test와
diff check 성공.
- **범위 밖:** Gate에서 production code 수정, 기존 Phase 2 완료 이력 변경, Phase 3 production 변경.
- 검증 기록: 무엇: `P2-R5-GATE`에서 `REV-018` 문서 계약 정합성을 종결했다. 왜: Phase 3 6차 보완의 시작 조건이 `P2-R5-GATE` 완료이기 때문이다. 어떻게: `P2-R5` focused 검증 결과와 `phase2-character-review.md` 6차 판정을 대조했다. 결과: production 변경 없이 `REV-018` 처리 완료로 판정했다.
#### Phase 2 7차 리뷰 보완
- [x] **Task 2.12: 캐릭터 mutation의 미사용 응답 매핑 제거**
**Goal 실행 `P2-R6`:** `REV-021`의 POST/PUT 성공 응답이 `data: null`인 계약을 유지하면서 controller가 사용하지 않는
facade 응답 생성과 전체 DTO 매핑을 제거한다.
- **추적 review ID:** `REV-021`.
- **시작 조건:** `P23-CONTRACT-GATE` 완료 이력과 `phase2-character-review.md` 7차 리뷰 판정 존재.
- **완료 증거:** create/update facade 반환형을 `Unit`으로 축소하고 불필요한 mapper 호출을 제거한 diff, mutation exact
`data: null` 회귀, character/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 캐릭터 생성·수정 business pipeline, 외부 API/S3/event 순서, 공개 API schema 변경, 인접 mapper 정리.
- **TDD 예외 사유:** OpenAPI와 기존 actual endpoint test가 이미 `data: null`을 고정한 상태에서 사용되지 않는 내부 계산만
제거하는 동작 불변 리팩터링이다.
- **대체 검증 방법:** controller가 facade 반환값을 소비하지 않는지 정적 확인하고 기존 mutation exact JSON 테스트를
focused 회귀한다.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- [x] **REFACTOR:** create/update facade 반환형을 `Unit`으로 바꾸고 mutation 마지막의 미사용
`characterMapper.toResponse(...)`를 제거한다.
- [x] **CONTRACT TEST:** POST/PUT actual endpoint가 계속 200과 exact `data: null`을 반환하는지 확인한다.
- [x] **회귀 확인:** character package와 공통 authorization/error 회귀 및 `ktlintCheck`를 실행한다.
- 검증 기록: 무엇: `REV-021`의 캐릭터 mutation 미사용 response mapping을 제거했다. 왜: POST/PUT 성공 응답은
`data: null`인데 facade가 controller가 버리는 전체 상세 DTO를 생성하고 있었기 때문이다. 어떻게: `create`/`update`
반환형을 `Unit`으로 축소하고 마지막 `characterMapper.toResponse(...)` 호출만 제거했다. 결과: mutation focused 명령은
`BUILD SUCCESSFUL in 1m 54s`, character/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 57s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 33s`, `git diff --check`는 출력이 없었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 2 7차 리뷰 Gate
**Goal 실행 `P2-R6-GATE`:** `REV-021`의 불필요한 mutation 응답 매핑 제거와 계약 불변 증거를 재검토한다.
- [x] **`P2-R6-GATE` 완료:** `P2-R6` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P2-R6` 완료.
- **완료 증거:** `REV-021` 처리 완료, POST/PUT `data: null` 계약 유지, focused/영향 범위 회귀와 lint·diff 성공.
- **범위 밖:** Gate에서 production code 수정, 기존 Phase 2 완료 이력 변경.
- 검증 기록: 무엇: `P2-R6-GATE`에서 `REV-021` 처리 완료와 Phase 2 7차 리뷰 종결을 확인했다. 왜: Phase 3 7차
보완의 시작 조건이 `P2-R6-GATE` 완료이기 때문이다. 어떻게: `phase2-character-review.md` 7차 리뷰 후속 판정을
갱신하고 위 focused/영향 범위 회귀, lint, diff check 결과를 대조했다. 결과: `REV-021`은 처리 완료로 판정했고
Phase 2는 12/12 완료 상태로 동기화했다.
#### Phase 2 9차 리뷰 보완
- [x] **Task 2.13: 캐릭터 필수 image와 `isActive=true` 레거시 계약 복구**
**Goal 실행 `P2-R7`:** 캐릭터 생성의 빈 필수 `image`를 부작용 전에 거부하고, 수정의 `isActive=true` 단독 요청을
레거시와 같은 유효한 no-op mutation으로 처리한다.
- **추적 review ID:** `REV-034`, `REV-035`.
- **시작 조건:** `phase2-character-review.md` 9차 정적 리뷰 판정 존재.
- **완료 증거:** 빈 생성 image의 400/no-side-effect와 `isActive=true` 단독 수정의 200 `data: null` actual endpoint
RED/GREEN, character/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** OpenAPI schema 변경, 레거시 controller/service 변경, 캐릭터 mutation pipeline 리팩터링,
`isActive=false`의 기존 비활성화 의미 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 필수 `image`를 빈 part로 보낸 POST가 현재 200으로 처리되고 외부 생성·DB 저장이 발생하는지 actual
endpoint로 고정한다.
- [x] **RED:** `{"isActive":true}`만 보낸 PUT이 현재 400을 반환하지만 레거시 endpoint는 유효한 변경 요청으로 받아
200 `data: null`을 반환하는 차이를 고정한다.
- [x] **GREEN:** create facade 진입 직후 빈 image를 400 `common.error.invalid_request`로 거부해 외부 API, DB, S3,
event를 호출하지 않는다.
- [x] **GREEN:** `isActive`가 null이 아니면 변경 요청으로 인정하되, `false` 비활성화 분기와 `true` no-op의 레거시
service 호출·응답 의미를 그대로 유지한다.
- [x] **REFACTOR:** character package와 공통 authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해
Progress와 리뷰 문서에 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 2 9차 리뷰 Gate
**Goal 실행 `P2-R7-GATE`:** `REV-034`~`REV-035`의 multipart 필수 파일과 레거시 update parity를 재검토한다.
- [x] **`P2-R7-GATE` 완료:** `P2-R7` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P2-R7` 완료.
- **완료 증거:** 두 review ID 처리 완료, 빈 생성 image 400/no-side-effect, `isActive=true` 단독 PUT 200
`data: null`, 기존 `isActive=false`와 일반 mutation 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록(RED): 무엇: `REV-034` 빈 생성 image와 `REV-035` `isActive=true` 단독 수정. 왜: empty multipart와 optional boolean parity를 실제 endpoint에서 재현하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 신규 2건이 각각 400 기대 대비 200, 200 기대 대비 400으로 실패했다.
- 검증 기록(GREEN/GATE): 무엇: character empty image 거부와 `isActive=true` no-op parity. 왜: 외부 API·DB·S3·event 전 400과 레거시 200 `data:null` 의미를 복구하기 위해. 어떻게: 같은 focused 명령 재실행 후 `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI 23개 operation/status `jq`, controller mapping 23개, `git diff --check`를 실행했다. 결과: 모두 성공했고 `git diff --check`는 출력이 없었다.
#### Phase 2 11차 리뷰 보완
- [x] **Task 2.14: 캐릭터 관계 필수 정수의 null·누락 거부**
**Goal 실행 `P2-R8`:** 캐릭터 생성 관계의 필수 `importance`가 누락되거나 null이면 JVM 기본값 `0`으로
보정하지 않고 외부 API·DB·S3·event 전에 400으로 거부한다.
- **추적 review ID:** `REV-040`.
- **시작 조건:** `phase2-character-review.md` 11차 정적 리뷰 판정 존재.
- **완료 증거:** `importance` 누락·null의 actual endpoint RED/GREEN과 no-side-effect, 정상 정수 생성 회귀,
character/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/controller 변경, OpenAPI schema 변경, 관계 중요도 범위 정책 추가.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/dto/ChatCharacterDto.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 관계 객체에서 `importance`를 누락하거나 null로 보낸 POST가 현재 `0`으로 역직렬화되어 mutation을
진행하는지 actual endpoint로 고정하고 외부 API·DB·S3·event 결과를 단언한다.
- [x] **GREEN:** v2 캐릭터 생성 경계에서 필수 non-null primitive의 존재와 null 여부를 strict parse 결과로 검증해
`common.error.invalid_request` 400으로 변환한다.
- [x] **CONTRACT TEST:** 정상 `importance` 정수와 관계가 없는 생성은 기존 결과를 유지하고, 미지 필드 거부도
회귀하지 않는지 확인한다.
- [x] **REFACTOR:** 검증을 v2 경계의 최소 범위에 두고 character/common 영향 범위 회귀, `ktlintCheck`,
`git diff --check`를 실행해 기록한다.
- 검증 기록(RED): 무엇: 관계 `importance` 누락·null actual POST. 왜: Jackson primitive 기본값 `0` 보정으로 외부 API·DB·S3·event 부작용이 발생할 수 있는지 재현하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests "*shouldRejectMissingRelationshipImportanceBeforeSideEffects" --tests "*shouldRejectNullRelationshipImportanceBeforeSideEffects"`를 실행했다. 결과: 신규 2건이 `status().isBadRequest` 기대에서 실패해 `BUILD FAILED`였다.
- 검증 기록(GREEN): 무엇: v2 캐릭터 request reader의 primitive null/누락 거부. 왜: 전역 mapper·레거시 DTO 변경 없이 v2 생성 경계에서 OpenAPI required non-null 정수 계약을 강제하기 위해. 어떻게: 같은 명령을 재실행했다. 결과: `BUILD SUCCESSFUL in 48s`였다.
- 검증 기록(GATE): 무엇: 정상 정수 관계 생성, 미지 필드 거부, character/common 영향 범위와 lint/diff. 왜: `REV-040` 보완이 기존 캐릭터 생성·공통 오류/인가 계약을 회귀시키지 않는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*" --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: 각 Gradle 명령은 `BUILD SUCCESSFUL`이었고 `git diff --check`는 출력이 없었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 2 11차 리뷰 Gate
**Goal 실행 `P2-R8-GATE`:** `REV-040`의 캐릭터 관계 필수 정수 nullability와 부작용 경계를 재검토한다.
- [x] **`P2-R8-GATE` 완료:** `P2-R8` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P2-R8` 완료.
- **완료 증거:** review ID 처리 완료, `importance` 누락·null 400/no-side-effect, 정상 생성 회귀 성공.
- **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경.
#### Phase 2 후속 기능 보완
- [x] **Task 2.15: 캐릭터 등록용 원작 검색**
**Goal 실행 `P2-R9`:** AI 캐릭터 등록 화면에서 soft delete되지 않은 원작을 필수 `searchTerm`으로 검색하고 레거시
`OriginalWorkResponse` 전체 필드의 직접 배열로 반환한다.
- **추적 review ID:** `REV-044`.
- **시작 조건:** `P2-R8-GATE` 완료와 PRD·OpenAPI의 승인된 원작 검색 계약 존재.
- **완료 증거:** 제목·콘텐츠 타입·카테고리 부분 검색, soft delete 제외, 빈 결과, 필수 query 오류와 exact response,
Character/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 원작 등록·수정·삭제, pagination·정렬 정책 추가, 원작 schema 축약, 레거시 `/admin/chat/original` 변경.
**Interfaces:**
- `GET /api/v2/admin/ai-characters/original-works/search?searchTerm={searchTerm}`
- Produces: `ApiResponse<List<OriginalWorkResponse>>`.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminOriginalWorkSearchTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/service/AdminOriginalWorkService.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/dto/OriginalWorkDtos.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** actual GET으로 세 검색 필드, 삭제 원작 제외, 빈 결과, `searchTerm` 누락 400과
`OriginalWorkResponse`의 13개 필드를 고정한다.
- [x] **GREEN:** `AdminOriginalWorkService.searchOriginalWorksAll`과 `OriginalWorkResponse.from`을 재사용해 별도
pagination이나 축약 DTO 없이 응답한다.
- [x] **CONTRACT TEST:** 신규 route가 `/{characterId}`와 충돌하지 않고 ADMIN 이중 인가, 오류 envelope와
`Accept-Language` fallback을 유지하는지 확인한다.
- [x] **REFACTOR:** 조회 전용 facade method 외 추상화를 추가하지 않고 Character/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다.
- 검증 기록(RED): 무엇: 캐릭터 등록용 원작 검색 신규 v2 endpoint actual GET 계약. 왜: route 미구현 상태에서 검색 필드·soft delete 제외·빈 결과·필수 query 오류가 실패하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminOriginalWorkSearchTest`를 실행했다. 결과: 4개 중 3개가 신규 route 부재로 실패했다.
- 검증 기록(GREEN): 무엇: 원작 검색 endpoint 최소 구현. 왜: 레거시 `searchOriginalWorksAll`과 `OriginalWorkResponse.from` 재사용이 계약을 충족하는지 확인하기 위해. 어떻게: 같은 focused test를 재실행했다. 결과: `BUILD SUCCESSFUL`이었다.
- 검증 기록(회귀): 무엇: Character/common 영향 범위, lint, diff whitespace. 왜: 신규 target 없는 route가 기존 character route와 공통 ADMIN/error 경계를 깨지 않는지 확인하기 위해. 어떻게: 아래 영향 범위 test, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: 영향 범위 test와 ktlint는 `BUILD SUCCESSFUL`, `git diff --check`는 출력 없음이었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminOriginalWorkSearchTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 2 후속 기능 Gate
**Goal 실행 `P2-R9-GATE`:** `REV-044`의 원작 검색 범위와 레거시 response parity를 재검토한다.
- [x] **`P2-R9-GATE` 완료:** `P2-R9` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 2 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P2-R9` 완료.
- **완료 증거:** 검색 필드·soft delete 제외·직접 배열·필수 query·공통 경계 회귀 성공,
OpenAPI operation `implemented`.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 2 multipart request part 계약 후속 보완
- [x] **Task 2.16: 캐릭터 생성·수정 request part의 application/json 강제**
**Goal 실행 `P2-R10`:** 캐릭터 생성·수정 multipart의 `request` part가 OpenAPI encoding대로
`application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다.
- **추적 review ID:** `REV-055`.
- **시작 조건:** `P2-R9-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재.
- **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·mutation 의미를 유지하고,
`text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header,
외부 API·S3·DB·event no-side-effect를 반환한다.
- **범위 밖:** JSON schema·strict reader·image 계약, external/S3/DB 순서, legacy/public endpoint,
OpenAPI·신규 dependency·DDL 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** POST·PUT의 유효 JSON 문자열을 `text/plain` request part로 보내 현재 handler에 진입하는지 actual
endpoint와 no-side-effect로 고정한다.
- [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String
strict reader에 동일 payload를 전달한다.
- [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON,
필수 part 누락 400 계약을 확인한다.
- [x] **REFACTOR:** 공통 helper가 필요하면 8개 multipart mapping의 media type 확인에만 한정하고
Character/common 영향 범위 회귀, `ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
- **RED 결과 (2026-07-29):** `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`에서 새 POST·PUT, `text/plain`·content type 누락, KO/EN/JA 12개 415 기대 케이스가 실패했다.
- **GREEN/GATE 결과 (2026-07-29):** 같은 focused 명령은 `BUILD SUCCESSFUL in 31s`였고, Character/common 영향 범위 명령은 `BUILD SUCCESSFUL in 1m 2s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 18s`, `git diff --check`는 출력 없이 성공했다.
- **전체 회귀 생략:** controller part 경계와 해당 actual endpoint 테스트만 변경했으므로 focused와 Character/common 오류 계약 회귀로 검증했다. 전체 `./gradlew test`는 실행하지 않았다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 2 multipart request part 계약 후속 Gate
**Goal 실행 `P2-R10-GATE`:** `REV-055` 수정 뒤 Character POST·PUT의 part-level JSON-only·415 경계를 재검토한다.
- [x] **`P2-R10-GATE` 완료:** `P2-R10` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 2 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P2-R10` 완료.
- **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 2 multipart part 이름 계약 후속 보완
- [x] **Task 2.17: 캐릭터 생성·수정의 미정의 multipart part 거부**
**Goal 실행 `P2-R11`:** Character 생성·수정 multipart에서 OpenAPI가 정의한 `image`, `request` 외 part를
handler의 business mutation 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-060`.
- **시작 조건:** `phase2-character-review.md` 13차 정적 리뷰 판정 존재.
- **완료 증거:** POST·PUT actual endpoint에서 `unexpected` 파일/문자열 part의 400 KO/EN/JA envelope와
외부 API·S3·DB·event no-side-effect, 정상 허용 part·기존 415 경계 회귀.
- **범위 밖:** OpenAPI schema 변경, 전역 multipart resolver 변경, legacy/public endpoint, 허용 파일의 내용 검증,
공통 추상화 추가.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 생성·수정에 정상 `request`와 `unexpected` part를 함께 보내 현재 mutation이 성공하는 경로를 actual
endpoint로 고정하고 외부 API·S3·DB·event 결과를 단언한다.
- [x] **GREEN:** 기존 multipart 검사에서 실제 part 이름 집합이 POST·PUT 허용 집합 `{image, request}`의 부분집합인지
확인하고, 초과 이름이 있으면 `AiCharacterAdminApiException(HttpStatus.BAD_REQUEST,
"common.error.invalid_request")`를 던진다.
- [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, 필수 part 누락 400, request part media type
415와 no-side-effect를 함께 확인한다.
- [x] **REFACTOR:** Character controller/test만 최소 변경하고 Character/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
- 검증 기록(RED): 무엇: Character POST·PUT의 미정의 multipart part 거부. 왜: OpenAPI `additionalProperties:false`와 달리
`unexpected` part가 무시된 채 mutation이 진행될 수 있었기 때문이다. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects'`를
실행했다. 결과: 신규 6개 invocation이 400 기대 대비 200으로 실패해 RED를 확인했다.
- 검증 기록(GREEN/GATE): 무엇: Character multipart 허용 part 이름 `{image, request}` 적용과 기존 request part media type·누락 회귀.
왜: 미정의 part를 business facade 진입 전에 400으로 차단하고 기존 정상/415/400 경계를 유지하기 위해서다. 어떻게:
controller에서 `MultipartHttpServletRequest.fileMap.keys`를 검사하고 아래 focused/영향 범위 회귀와 `ktlintCheck`, `git diff --check`를
실행했다. 결과: focused undefined/non-json/missing request 명령은 `BUILD SUCCESSFUL in 1m 41s`, Character/common 영향 범위는
`BUILD SUCCESSFUL in 1m 36s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 30s`, `git diff --check`는 출력 없음이었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests '*shouldRejectNonJsonRequestPartBeforeSideEffects' --tests '*shouldKeepMissingRequestPartAsBadRequest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 2 multipart part 이름 계약 후속 Gate
**Goal 실행 `P2-R11-GATE`:** `REV-060` 수정 뒤 Character POST·PUT의 허용 part 이름과 기존 media type 경계를
재검토한다.
- [x] **`P2-R11-GATE` 완료:** `P2-R11` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 2
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P2-R11` 완료.
- **완료 증거:** 미정의 part 400/no-side-effect, 정상 허용 part, 필수 part·415 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
- 검증 기록: 무엇: `P2-R11-GATE`에서 `REV-060` 처리 완료와 Phase 2 13차 리뷰 종결을 확인했다. 왜: Phase 3
`P3-R17` 시작 조건인 `P2-R11-GATE` 완료를 판정하기 위해서다. 어떻게: focused/영향 범위 회귀, lint, diff check와
`phase2-character-review.md` 최신 결론을 대조했다. 결과: Character 미정의 multipart part는 400/no-side-effect로
처리되고 기존 정상 허용 part·필수 part 누락·request part 415 회귀가 유지됐다.
#### Phase 2 multipart 일반 form-field part 후속 보완
- [x] **Task 2.18: 캐릭터 생성·수정의 전체 multipart part 이름 검증**
**Goal 실행 `P2-R12`:** Character POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한
모든 multipart part 이름을 검사해 `{image, request}` 외 이름을 mutation 전에 400
`common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-065`.
- **시작 조건:** `phase2-character-review.md` 8차 정적 리뷰 판정 존재.
- **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 external API·S3·DB·event
no-side-effect, 기존 파일형 미정의 part·정상 허용 part·필수 part·request part 415 회귀 성공.
- **범위 밖:** OpenAPI schema, 전역 multipart resolver, legacy/public endpoint, 공통 추상화, 신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** `MockPart` 등 filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내
현재 `fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다.
- [x] **GREEN:** servlet request의 전체 part 이름 집합을 `{image, request}`와 비교해 초과 이름을 facade 진입 전에
공통 400으로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 파일형 미정의 part, 정상 생성·수정, 필수 part 누락,
request part 415와 no-side-effect를 확인한다.
- [x] **REFACTOR:** Character controller/test만 최소 변경하고 Character/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
#### Phase 2 multipart 전체 part 이름 후속 Gate
**Goal 실행 `P2-R12-GATE`:** `REV-065` 수정 뒤 Character POST·PUT의 파일·일반 form-field를 포함한 전체 part
이름과 기존 media type 경계를 재검토한다.
- [x] **`P2-R12-GATE` 완료:** `P2-R12` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 2
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P2-R12` 완료.
- **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
- 검증 기록(RED): 무엇: Character POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다.
- 검증 기록(GREEN): 무엇: Character multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 `{image, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다.
- 검증 기록(GATE): Character/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다.
---
### Phase 3: 오디오 콘텐츠·댓글 관리와 signed URL vertical slice
#### 목표
선택한 AI 캐릭터 소유 오디오 콘텐츠 목록/검색/상세/생성/수정/soft delete, 댓글 CRUD와 관리자 재생용 signed URL을
제공한다.
#### 범위와 비범위
- 포함: 콘텐츠 owner 검증, 기존 파일 처리/가격/공개/예약/번역/알림 parity, 댓글 root/reply 조회·작성·수정·soft
delete, `AudioContentCloudFront` 재사용, private path 비노출.
- 제외: 콘텐츠 구매/좋아요, 캐릭터 직접 댓글, 댓글 hard delete·cascade, content upload/processing pipeline 변경,
community audio 30분 정책 통합.
#### 선행 Phase 및 의존성
- Phase 1 target resolver.
- 콘텐츠 생성/수정/delete 기존 동작 특성화 테스트.
#### API endpoint와 request/response contract
- `GET /api/v2/admin/ai-characters/audio-content-themes`
- Request: query/body 없음.
- Response: `List<GetAudioContentThemeResponse(id, theme, image)>`.
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search_word=&page=&size=` ->
`GetCreatorAdminContentListResponse` 전체 필드.
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}?timezone=Asia/Seoul` ->
`GetAudioContentDetailResponse` 전체 nested 필드.
- `POST /api/v2/admin/ai-characters/{characterId}/audio-contents`
- multipart 필수 `contentFile`, `coverImage`, `request: CreateAudioContentRequest`.
- Response: `CreateAudioContentResponse(contentId)`.
- `PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
- multipart optional `coverImage`, 필수 `request: UpdateCreatorAdminContentRequest`에서 `id` 제외.
- Response: `data: null`.
- 수정 `audioFile` 교체는 레거시 creator admin 수정 pipeline에 없어 제공하지 않는다.
- 댓글은 `GET|POST .../{contentId}/comments`, `PUT|DELETE .../{contentId}/comments/{commentId}`,
`GET .../{commentId}/replies`의 5개 operation을 사용한다.
- 정식 전체 schema와 optional/nullable은 `api-contract.openapi.json`의 AudioContent operation을 따른다. 현재 구현의
`description`, `releaseDateUtc`, `audioSignedUrl`, `status`, `seriesIds`, `themeName`, `imageUrl` 별칭은
`P23-CONTRACT-3`에서 레거시 계약으로 정합화한다.
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: `AudioContent` owner-scoped query adapter 추가 가능.
- Service: 신규 content admin application service에서 기존 creator/admin content service를 테스트로 고정 후 선택 위임 또는 최소 추출한다.
- CloudFront: 기존 `AudioContentCloudFront` 그대로 주입해 사용한다.
#### DB migration
- 없음.
#### transaction과 concurrency 고려사항
- write transaction 시작 직후 target과 `content.member.id == creatorMember.id`를 검증한다.
- S3 업로드, 이벤트 발행 순서는 기존 동작 parity를 따른다.
- soft delete는 기존 콘텐츠 삭제 동작처럼 `isActive=false`, 필요한 경우 `releaseDate=null`을 유지한다.
#### 보안 및 개인정보 위험
- private S3 object path, signed key material을 응답하지 않는다.
- 다른 캐릭터 content ID 접근은 4xx와 no side effect다.
#### acceptance criteria
- target 캐릭터 소유 콘텐츠만 조회/변경된다.
- signed URL 만료 계산이 기존 creator admin policy와 동일하다는 테스트가 있다.
- signed URL 만료 계산·path 처리에서 실제 기존 구현에서 관찰되는 edge case가 특성화 테스트로 고정된다.
- invalid target/ownership 실패 시 DB/S3/event side effect가 없다.
#### targeted test
- Characterization: `LegacyCreatorAdminAudioContentCharacterizationTest`, `AudioContentCloudFrontCharacterizationTest`.
- V2 RED/GREEN: `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`.
- Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`
#### 전체 회귀 테스트 영향
- 기존 `CreatorAdminContentController`, `AdminContentController`, public content 조회 테스트가 통과해야 한다.
#### rollback 전략
- 신규 content v2 admin route/facade를 제거한다.
- 신규 DDL이 없으므로 schema rollback은 없다.
#### 권장 commit 경계
- `feat: add ai character admin content slice`
- [x] **Task 3.1: 기존 콘텐츠와 signed URL 특성화 baseline 고정**
- **Goal 이력 `P3-H1`:** 기존 creator/admin 콘텐츠와 signed URL 동작을 신규 v2 비교 기준으로 고정했다.
- **완료 증거:** 아래 특성화 테스트와 `ktlintCheck` 실행 기록.
- **범위 밖:** 신규 v2 content endpoint 구현.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AudioContentCloudFrontCharacterizationTest.kt`
- CHARACTERIZE: 기존 creator/admin content 구현을 대상으로 검증, 파일 처리, 가격, 공개/예약, 번역/알림, soft delete, upload/processing pipeline 결과를 고정한다.
- CHARACTERIZE: 기존 creator admin signed URL 만료 계산식과 만료 계산·path 처리에서 실제로 관찰되는 edge case, private path 비노출 계약을 고정한다.
- BASELINE: 신규 v2 production code 변경 전에 두 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- REFACTOR: 테스트 fixture만 정리하고 content/signed URL parity baseline은 변경하지 않는다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*CharacterizationTest'`
- 검증 기록 (2026-07-26): 위 특성화 테스트 6건과 `./gradlew ktlintCheck`가 모두 통과했다.
- [x] **Task 3.2: content controller/facade/DTO 구현**
- **Goal 이력 `P3-H2`:** 콘텐츠 조회·생성·수정의 최초 v2 API 구현을 제공했다.
- **완료 증거:** 아래 RED/GREEN 및 후속 보완 기록. 이 이력만으로 Phase 3 Gate 통과를 의미하지 않는다.
- **범위 밖:** 후속 심층 리뷰에서 확정되는 누락·회귀 보완.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*`
- RED: Task 3.1 baseline에서 옮긴 content parity, cross-character content 접근, signed URL/private path 계약 테스트와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
- GREEN: content endpoint와 owner-scoped query/write를 구현하고 목록/검색 `page/size` 기본값·최소·최대 보정을 적용한다.
- REFACTOR: signed URL/private path mapping과 기존 pipeline 재사용 경계를 정리하고 회귀 테스트를 재실행한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`
- 구현 기록(2026-07-26): `GET` 목록/검색·상세와 `PUT` 수정/soft delete를 target resolver와 owner-scoped query로 구현했다. 응답은 v2 DTO만 사용하며 private content path 필드를 반환하지 않는다.
- 구현 보완(2026-07-27): `POST` 생성을 기존 `AudioContentService.createAudioContent` 재사용 방식으로 제공한다. v2 adapter에서 `themeId`, ISO-8601 `releaseDateUtc`, owner-scoped `seriesIds` 연결을 처리하고, 업로드 완료 전 `isActive` 직접 활성화는 기존 processing pipeline parity를 위해 수행하지 않는다.
- 검증 기록(RED, 2026-07-26): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest'`를 실행했고, 신규 route 부재로 목록은 404, 상세와 PUT은 404/405여서 의도대로 실패함을 확인했다.
- 검증 기록(GREEN, 2026-07-26): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`와 `./gradlew ktlintCheck`를 실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
- 검토 보완(2026-07-26): 상세 응답에 target owner 범위의 활성 series ID 목록을 추가하고, 다른 캐릭터 콘텐츠 PUT이 DB를 변경하지 않는 통합 테스트를 추가했다.
- 재검증(2026-07-26): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`를 실행해 `BUILD SUCCESSFUL`을 확인했다.
- 검토 보완(2026-07-27): 생성/수정의 `seriesIds` owner-scoped 연결, 생성의 ISO-8601 `releaseDateUtc` 변환, POST 제공 문서 불일치를 보완했다. 수정 `audioFile` 교체는 기존 creator/admin 수정 pipeline에 없어 Phase 3에서 미지원으로 명시했다.
- [x] **Task 3.3: Phase 3 요구사항·계약·코드 리뷰**
**Goal 실행 `P3-R1`:** PRD Feature C와 Endpoint Contract Summary를 콘텐츠 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다.
- **추적 review ID:** `REV-004`, `REV-005`, `REV-006`, `REV-007`, `REV-008`.
- **시작 조건:** `P2-GATE`, `P3-H1`, `P3-H2` 완료 증거 존재.
- **완료 증거:** `docs/sample/sample-review.md` 형식의 리뷰 문서, endpoint·pipeline·side-effect 추적표, 모든 후보 판정과 Progress 기록.
- **범위 밖:** 리뷰 도중 production code 수정, series/community 기능 검토.
- **TDD 예외 사유:** 구현이 아닌 read-only 리뷰 Task다.
- **대체 검증 방법:** PRD·계약·production·test를 대조하고 focused test를 실행해 실제 결과를 리뷰 문서에 기록한다.
**Files:**
- Create: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- [x] 테마·목록·검색·상세·생성·수정·soft delete endpoint와 DTO 필드를 PRD/계약에 추적한다.
- [x] signed URL TTL/path, private path 비노출, viewer 상태 기본값을 production·test에 추적한다.
- [x] 생성/update pipeline, 파일, 가격, 공개·예약, 번역·알림, `seriesIds`, 날짜 변환과 실패 순서를 확인한다.
- [x] owner 검증, no-side-effect, ADMIN 인가, 오류 i18n, pagination/multipart 계약을 확인한다.
- [x] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` 결과와 후보 판정을 리뷰 문서에 기록한다.
- 검증 기록: 무엇: Phase 3 오디오 콘텐츠 slice의 read-only 요구사항·계약·코드 리뷰. 왜: `P3-H1`, `P3-H2` 완료 이력 이후 `REV-004`~`REV-008`의 실제 확정 여부와 후속 소유 Goal을 고정하기 위해. 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`에 PRD Feature C, Endpoint Contract Summary, production/test 대조표와 발견 사항을 기록하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 2s`였고, `REV-004`~`REV-008`은 각각 `P3-T3`~`P3-T7`의 기존 소유 Goal로 연결했다. 리뷰 Task이므로 production code는 수정하지 않았다.
- [x] **Task 3.4: 활성 콘텐츠 테마 API 보완**
**Goal 실행 `P3-T3`:** 활성 콘텐츠 테마를 전용 `themeId/themeName/imageUrl` DTO로 반환하는 관리자 API를 완결한다.
- **추적 review ID:** `REV-007`, `REV-008` 중 v2 DTO 경계와 endpoint 인가.
- **시작 조건:** `P3-R1` 완료와 테마 endpoint review 판정.
- **완료 증거:** request body 없는 GET, 활성 필터, DTO field contract RED/GREEN과 Progress 기록.
- **범위 밖:** 테마 CRUD, legacy DTO 외부 노출.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentThemeControllerTest.kt`
- [x] **RED:** 테마 endpoint 부재 또는 계약 불일치와 legacy field 노출을 재현하는 가장 작은 실패 test를 작성한다.
- [x] **RED 확인:** focused test를 실행해 의도한 route·field assertion 실패를 확인한다.
- [x] **GREEN:** 활성 테마만 `themeId`, `themeName`, `imageUrl`로 반환하는 최소 구현을 작성한다.
- [x] **GREEN 확인:** request body 없음, exact field set과 ADMIN 이중 인가를 포함한 focused test 성공을 확인한다.
- [x] **REFACTOR:** v2 DTO 경계만 정리하고 테마 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록: 무엇: `P3-T3`의 활성 콘텐츠 테마 endpoint 계약 증거를 전용 focused test로 분리했다. 왜: production 동작은 이미 `AiCharacterAdminAudioContentController`/`Facade`/DTO에서 충족하고 있었지만, `REV-007`, `REV-008` 기준 Gate 증거가 단일 대형 controller test에 섞여 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentThemeControllerTest`를 추가해 활성 필터, orders 정렬, `themeId/themeName/imageUrl` exact field, legacy `id/theme/image` 비노출과 anonymous 401을 검증하고 기존 controller test의 중복 테마 케이스를 제거했다. 결과: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest`는 `BUILD SUCCESSFUL in 35s`, 테마+기존 controller focused 회귀는 `BUILD SUCCESSFUL in 48s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 11s`였다. production code는 추가하지 않았다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest`
- [x] **Task 3.5: 콘텐츠 목록·상세·signed URL 보완**
**Goal 실행 `P3-T4`:** owner-scoped 콘텐츠 조회와 signed URL·상세 DTO 계약을 독립적으로 완결한다.
- **추적 review ID:** `REV-005`, `REV-007` 중 상세 parity와 v2 전용 DTO 경계.
- **시작 조건:** `P3-T3` 완료와 조회/signed URL 관련 review 판정.
- **완료 증거:** 검색·status·pagination·상세·TTL/path/private 정보 RED/GREEN과 Progress 기록.
- **범위 밖:** 콘텐츠 생성·수정, community audio 30분 정책.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AudioContentCloudFrontCharacterizationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
- [x] **RED:** `purchaseOption=RENT_ONLY`, entity `isOnlyRental=true`, 미래·과거 `releaseDate` 조합에서 legacy 상세와 다른 `isOnlyRental`, `purchaseOption`, `releaseDate`를 재현한다.
- [x] **RED:** 응답의 creator·buyer·other content·comment·translation 중첩 타입이 legacy/public DTO package에 직접 의존하는 현재 경계를 검출하고 exact JSON key를 고정한다.
- [x] **RED 확인:** query/legacy baseline test를 실행해 세 compatibility field와 금지 DTO 의존이 의도대로 실패하는지 확인한다.
- [x] **GREEN:** legacy 파생 규칙과 현지화된 `releaseDate` 의미를 유지하고 UTC 원본은 `releaseDateUtc`에만 반환하며, 동일 JSON을 v2 전용 중첩 DTO로 최소 매핑한다.
- [x] **GREEN 확인:** 같은 query/legacy baseline test를 다시 실행해 legacy compatibility field, `releaseDateUtc`와 exact JSON schema가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** owner·검색·status·pagination, 활성 owner-scoped `seriesIds`, viewer 기본값, signed URL TTL/path와 private 정보 비노출을 함께 회귀한다.
- [x] 조회/signed URL focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 상세 응답의 legacy `releaseDate` 의미, RENT_ONLY 파생값과 v2 전용 중첩 DTO 경계. 왜: `REV-005`, `REV-007`에서 상세 DTO가 legacy/public 중첩 DTO에 직접 의존하고, 과거 공개일을 legacy `releaseDate`에도 노출하고 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentQueryTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest`를 실행했다. 결과: 2건이 의도대로 실패했다. 하나는 `releaseDate`가 존재해서 실패했고, 하나는 legacy nested DTO package 누출 assertion으로 실패했다.
- 검증 기록(GREEN/REFACTOR): 무엇: 상세 응답의 `releaseDateUtc` 전용 노출, RENT_ONLY 파생 규칙, v2 전용 중첩 DTO. 왜: 관리자 상세는 UTC 원본을 `releaseDateUtc`에만 고정하고, response DTO는 legacy/public DTO 타입을 외부 계약으로 재노출하지 않아야 하기 때문이다. 어떻게: `AiCharacterAdminAudioContentDto`에 v2 중첩 response DTO를 추가하고, `AiCharacterAdminAudioContentMapper`의 `releaseDate`, `isOnlyRental`, `purchaseOption`, `creator` mapping만 최소 수정했다. 결과: `AiCharacterAdminAudioContentQueryTest`는 `BUILD SUCCESSFUL in 29s`, 계획서 Verify 묶음은 `BUILD SUCCESSFUL in 41s`, `./gradlew ktlintCheck`는 최초 unused import 2건으로 실패 후 정리 재실행에서 `BUILD SUCCESSFUL in 10s`였다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AudioContentCloudFrontCharacterizationTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest`
- [x] **Task 3.6: 콘텐츠 생성 pipeline 보완**
**Goal 실행 `P3-T5`:** 콘텐츠 생성의 multipart 입력, legacy field 변환, 파일·processing·series 연결과 side effect parity를 완결한다.
- **추적 review ID:** `REV-006`, `REV-007`, `REV-008` 중 생성 binding·field·pipeline 특성화.
- **시작 조건:** `P3-T4` 완료와 생성 pipeline 관련 review 판정.
- **완료 증거:** 전체 생성 field·파일·날짜·series RED/GREEN, 실패 순서와 focused/legacy test 기록.
- **범위 밖:** upload/processing pipeline 정책 변경, 오디오 파일 교체.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] `tags` 필수 여부와 생성 `isActive=false`의 canonical 계약을 legacy pipeline·Endpoint Contract Summary로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다.
- [x] **RED:** `coverImage`, `audioFile`, `request` 각 part 누락에서 Kotlin nullable 때문에 `MissingServletRequestPartException`이 발생하지 않는 현재 binding과 KO/EN/JA envelope 차이, facade·DB·S3·event 호출 0건 기대를 재현한다.
- [x] **RED:** 생성 request 전체 field, `description/releaseDateUtc` 변환, `tags` 누락, `isActive=false`, target·theme·`seriesIds` 오류와 S3/processing/event 실패 순서를 각각 고정한다.
- [x] **RED 확인:** create/error/legacy characterization test를 실행해 part별 exception·field 계약·failure order가 의도대로 실패하는지 확인한다.
- [x] **GREEN:** 필수 file part를 non-null binding으로 만들고 확정된 field 계약, 외부 부작용 전 참조 검증과 legacy upload/processing parity를 최소 구현한다.
- [x] **GREEN 확인:** 같은 test를 다시 실행해 part별 400/i18n, 정상 생성과 실패 후 DB/S3/event 결과가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** cover/audio upload, 가격·공개·예약·번역·알림 및 실패 후 DB/S3/event 결과를 characterization/focused test로 회귀하고 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 콘텐츠 생성의 필수 multipart part와 legacy 기본 계약. 왜: `REV-006`에서 생성 binding·field·failure-order 증거가 분리되지 않았고, `tags` 누락과 `isActive=true` 요청의 canonical 동작을 확정해야 했기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`를 실행했다. 결과: 정상 생성 케이스가 200 기대 대비 500으로 실패했고, 원인은 production 계약이 아니라 test fixture의 `AmazonS3Client.getUrl(String, String)` 미설정으로 `S3Uploader.putS3`에서 null URL이 발생한 것이었다.
- 검증 기록(GREEN/REFACTOR): 무엇: 생성 필수 part 400, 업로드 전 S3 0회, `tags` 누락 허용, `isActive=true` 요청의 legacy processing 기본값. 왜: 신규 v2 생성은 기존 upload/processing pipeline을 바꾸지 않고 adapter 계약만 고정해야 하기 때문이다. 어떻게: test fixture에 `amazonS3Client.getUrl(...)` mock만 추가하고 production code는 변경하지 않았다. 결과: `AiCharacterAdminAudioContentCreateTest`는 `BUILD SUCCESSFUL`, create+legacy+error contract 회귀는 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 10s`였다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`
- [x] **Task 3.7: 콘텐츠 수정·soft delete 보완**
**Goal 실행 `P3-T6`:** 콘텐츠 수정, cover 유지/교체, series 재연결과 soft delete를 owner-safe하게 완결한다.
- **추적 review ID:** `REV-004`.
- **시작 조건:** `P3-T5` 완료와 수정·삭제 관련 review 판정.
- **완료 증거:** 수정·soft delete·audioFile 미지원·cross-owner RED/GREEN과 Progress 기록.
- **범위 밖:** audio file 교체, hard delete.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- [x] **RED:** 기존 `SeriesContent.orders`, row ID, `createdAt`이 있는 콘텐츠에 동일 `seriesIds`를 PUT했을 때 전부 삭제·재생성되는 현재 동작을 실패 test로 고정한다.
- [x] **RED 확인:** 동일 ID, 추가 ID, 제거 ID를 각각 요청해 교집합 metadata 보존과 차집합만 insert/delete한다는 기대가 현재 실패하는지 확인한다.
- [x] **GREEN:** 기존 연결과 요청 ID의 차집합만 변경하고 교집합 row의 ID·`orders`·`createdAt`을 보존하는 최소 구현을 작성한다.
- [x] **GREEN 확인:** 같은 update test를 다시 실행해 동일 집합 no-op, 교집합 metadata 보존과 차집합 변경만 발생하는지 확인한다.
- [x] **REFACTOR:** cover 유지/교체, 날짜, `audioFile` 미지원, `isActive=false`와 cross-owner/invalid series의 DB/S3/event no-side-effect를 회귀한다.
- [x] 수정 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 동일 `seriesIds` 수정 시 기존 `SeriesContent` metadata 보존. 왜: `REV-004`에서 기존 구현이 연결을 모두 삭제·재생성해 row ID, `orders`, `createdAt`을 소실했기 때문이다. 어떻게: `AiCharacterAdminAudioContentUpdateTest`를 추가하고 focused 실행했다. 결과: 최초 focused test는 200 기대 대비 500으로 실패했고, 원인은 응답 매핑의 CloudFront private key fixture 문제임을 로그로 확인한 뒤 test fixture에 `AudioContentCloudFront` mock을 추가했다.
- 검증 기록(GREEN/REFACTOR): 무엇: `replaceSeriesIds`가 요청 ID와 기존 연결의 차집합만 변경하고 교집합 row를 보존하도록 수정했다. 왜: 동일 series 연결의 metadata를 유지하면서 제거·추가만 반영해야 하기 때문이다. 어떻게: `requestedIds`, 기존 연결 ID set을 비교해 삭제 대상만 remove하고 신규 ID만 persist했다. 결과: `AiCharacterAdminAudioContentUpdateTest`는 `BUILD SUCCESSFUL in 46s`, update+controller+legacy+error contract 회귀는 `BUILD SUCCESSFUL in 1m 21s`였다. 기존 controller 회귀 2건은 앞선 `P3-T4` 확정 계약(`releaseDate` 미노출, `RENT_ONLY` 파생값)에 맞춰 기대값만 갱신했다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest`
- [x] **Task 3.8: 콘텐츠 ownership·오류·legacy 회귀 보완**
**Goal 실행 `P3-T7`:** Phase 3 모든 endpoint의 ownership·ADMIN·오류 계약과 legacy/public 회귀를 고정한다.
- **추적 review ID:** `REV-006`, `REV-007`, `REV-008`.
- **시작 조건:** `P3-T6` 완료 또는 앞선 Goal의 근거 있는 `해당 없음` 판정.
- **완료 증거:** 권한 매트릭스, 정확한 domain/client 오류 status/key/KO·EN·JA, no-side-effect와 legacy 회귀 기록.
- **범위 밖:** Phase 1 공통 handler 재설계, Phase 4 series 기능.
**Files:**
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **RED:** 테마·목록·상세·생성·수정 endpoint 각각의 JWT role × DB role, stale ADMIN claim과 허용·거부 Origin을 parameterized test로 고정한다.
- [x] **RED:** target/content/theme/series/date와 세 multipart part 누락의 exact status, exception type, message key와 KO/EN/JA envelope를 실제 endpoint에서 고정한다.
- [x] **RED 확인:** 실제 endpoint matrix와 legacy characterization을 실행해 누락된 인가·i18n·failure-order assertion이 의도대로 실패하는지 확인한다.
- [x] **GREEN:** 확정된 domain/client/server 오류만 최소 매핑하고 ownership 실패 시 DB insert/update/delete, S3, event 0건을 보장한다.
- [x] **GREEN 확인:** 같은 endpoint/error/ownership test를 다시 실행해 status/header/envelope, KO/EN/JA와 no-side-effect가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** legacy characterization에 validation·파일·가격·공개/예약·번역/알림·failure order를 보강하고 signed URL edge case와 함께 실행한다.
- [x] 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest` 참조와 과거 test 수는 삭제하지 않고 정정 기록을 누적한다.
- [x] `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest`, `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentOwnershipTest`의 파일 존재와 각 소유 계약 통과를 확인한다.
- [x] creator/admin/public content 회귀, 신규 DTO 의존 방향과 focused test·`ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): malformed `characterId`/`contentId` 6건이 미매핑 404 EN envelope을 기대한 새 ownership focused test에서 GET 400, write 415를 반환해 18건 중 6건이 실패했다.
- 검증 기록(GREEN/REFACTOR): 모든 resource path를 `[0-9]+`로 제한한 뒤 malformed path 404, 실제 5개 endpoint의 non-ADMIN/stale claim 403, 테마 CORS allow/deny, unknown target 생성 S3 0회를 고정했다. common authorization/error test의 KO·EN·JA matrix와 기존 content/legacy characterization을 재사용했다. focused는 `BUILD SUCCESSFUL in 1m 5s`, content+authorization+error 회귀는 `BUILD SUCCESSFUL in 2m 8s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`였다. `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`는 현재 존재하지 않는 과거 계획 참조이며 이 기록으로 정정한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`
#### Phase 3 Gate
**Goal 실행 `P3-GATE`:** Phase 3 콘텐츠 관리와 signed URL의 PRD 추적성, pipeline 안전성과 legacy 회귀를 최종 판정한다.
- [x] **`P3-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P3-R1`, `P3-T3`~`P3-T7` 완료 또는 근거 있는 `해당 없음` 판정.
- **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록.
- **범위 밖:** Gate 실패와 무관한 Phase 4 기능 구현.
- [x] `REV-004`~`REV-008`의 response parity matrix, multipart exception, series metadata, DTO 경계와 test 증거가 각 소유 Goal의 Progress에 연결됐다.
- [x] 동일 `seriesIds`의 row metadata 보존, legacy `releaseDate`·rental 파생값, 세 필수 part와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다.
- [x] 완료 이력의 누락 test 파일·test 수·characterization 범위는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다.
- 검증 기록: 무엇: `P3-GATE` Phase 3 최종 판정. 왜: `P3-R1`, `P3-T3`~`P3-T7`의 확정 finding 처리와 Gate 명령 성공을 확인하기 위해. 어떻게: 아래 세 Gate 명령을 fresh 실행했다. 결과: content focused 명령은 `BUILD SUCCESSFUL in 2m 15s`, authorization/error 명령은 `BUILD SUCCESSFUL in 1m 29s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. `git diff --check`는 `P3-T7` 완료 전 실행에서 출력 없음이었다. `REV-004`~`REV-008`의 Phase 3 소유 항목은 처리 완료로 판정했다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
**Expected:** 모든 명령 exit code 0, Phase 3 review의 후보·확정 미처리 항목 0건, Phase 4가 소비할 owner query 계약 확정.
#### Phase 3 후속 리뷰 보완
- [x] **Task 3.9: 콘텐츠 생성 pipeline·multipart 오류 증거 보강**
**Goal 실행 `P3-R2`:** `REV-010`의 생성 field adapter, 선검증, multipart 오류와 S3/processing/event 실패 순서를 실제 endpoint에서 고정한다.
- **추적 review ID:** `REV-010`.
- **시작 조건:** `P2-R2-GATE` 완료와 기존 `P3-GATE` 완료 이력 존재.
- **완료 증거:** 생성 actual endpoint/legacy characterization test, 실패 지점별 관찰 결과, `ktlintCheck`와 Progress 기록.
- **범위 밖:** legacy upload/processing 정책 변경, 추정에 의한 S3 보상 추가, audio file 교체.
- **TDD 예외 사유:** 현재 production 실패가 아니라 `P3-T5` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
- **대체 검증 방법:** legacy와 actual endpoint를 characterization하고 불일치가 재현될 때만 RED/GREEN으로 최소 수정한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **CONTRACT TEST:** `coverImage`, `audioFile`, `request` 누락의 실제 endpoint KO/EN/JA status/key/envelope와 facade·DB·S3·event 0건을 고정한다.
- [x] **CONTRACT TEST:** 생성 request 전체 field의 legacy adapter 결과와 target/theme/series 선검증을 확인한다.
- [x] **FAILURE CHARACTERIZATION:** cover upload, audio upload와 event 실패 지점별 DB/S3/event 결과를 legacy parity와 대조하고 비트랜잭션 S3 결과를 명시한다.
- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, 현재 동작이 계약을 만족하면 production code를 변경하지 않는다.
- [x] **REFACTOR:** create/legacy/error focused test와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
- 검증 기록: 무엇: `REV-010`의 생성 multipart·theme·cover 실패 증거를 실제 endpoint test로 보강했다. 왜: 기존 완료 기록이 part별 KO/EN/JA message, invalid theme 선검증, cover upload 실패 후 DB/event 상태를 직접 고정하지 않았기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 세 필수 part KO/EN/JA envelope, invalid theme 선검증, cover upload 실패 rollback/event 0회 단언을 추가했다. 결과: production code 변경 없이 create 단독 명령은 `BUILD SUCCESSFUL in 1m 4s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
- [x] **Task 3.10: 콘텐츠 수정 차집합·cover·ownership 증거 보강**
**Goal 실행 `P3-R3`:** `REV-011`의 `seriesIds` 교집합/차집합, cover 변경과 실제 endpoint ownership·오류 no-side-effect를 고정한다.
- **추적 review ID:** `REV-011`.
- **시작 조건:** `P3-R2` 완료.
- **완료 증거:** update/ownership actual endpoint test, content/authorization/error 회귀, `ktlintCheck`와 Progress 기록.
- **범위 밖:** audio file 교체, hard delete, Phase 4 series API 구현.
- **TDD 예외 사유:** 현재 production 실패가 아니라 `P3-T6`~`P3-T7` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
- **대체 검증 방법:** 교집합+추가+제거와 cover/ownership 계약을 non-vacuous test로 작성하고 실패가 재현될 때만 최소 수정한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- [x] **CONTRACT TEST:** 기존 교집합, 신규 추가, 기존 제거를 한 요청에 포함해 교집합 row ID·`orders`·`createdAt` 보존과 차집합만 insert/delete됨을 확인한다.
- [x] **CONTRACT TEST:** cover 미지정 유지, 성공 교체와 업로드 실패, 날짜 변경, soft delete, cross-owner/invalid series의 DB/S3/event 결과를 고정한다.
- [x] **CONTRACT TEST:** 실제 목록·상세·생성·수정의 ownership/domain 오류를 KO/EN/JA envelope과 DB insert/update/delete·S3·event count로 확인한다.
- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, 현재 동작이 계약을 만족하면 production code를 변경하지 않는다.
- [x] **REFACTOR:** content package와 공통 authorization/error, legacy characterization, `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
- 검증 기록: 무엇: `REV-011`의 수정 차집합·cover·ownership 증거를 보강했다. 왜: 기존 완료 기록보다 실제 endpoint의 교집합 보존, cover 유지/교체/실패, ownership/domain no-side-effect 증거가 좁았기 때문이다. 어떻게: `AiCharacterAdminAudioContentUpdateTest`와 `AiCharacterAdminAudioContentOwnershipTest`에 관련 회귀를 추가하고 content/common 회귀로 재확인했다. 결과: production code 변경 없이 content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 3 후속 리뷰 Gate
**Goal 실행 `P3-R2-GATE`:** `REV-010`~`REV-011`의 직접 증거를 재검토하고 Phase 3 후속 리뷰를 종결한다.
- [x] **`P3-R2-GATE` 완료:** `P3-R2`, `P3-R3` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
- **시작 조건:** `P3-R2`, `P3-R3` 완료.
- **완료 증거:** 두 review ID 수정 완료, content/common 회귀와 `ktlintCheck` 성공, `phase3-audio-content-review.md` 최신 결론과 Progress 동기화.
- **범위 밖:** 기존 `P3-GATE` 이력 수정, Phase 4 기능 구현.
- 검증 기록: 무엇: `P3-R2-GATE` 후속 리뷰 종결. 왜: `REV-010`~`REV-011`의 직접 증거가 추가됐고 Phase 4 전 Phase 3 후속 보완 종료 여부를 판정하기 위해. 어떻게: content/common 회귀와 `ktlintCheck`를 fresh 실행하고 `phase3-audio-content-review.md`에 3차 후속 검증 기록을 누적했다. 결과: content/common 회귀는 `BUILD SUCCESSFUL in 2m 41s`였고, `ktlintCheck` 결과는 아래 Progress 검증 기록에 남긴다.
#### Phase 3 4차 리뷰 보완
- [x] **Task 3.11: 생성 후반 실패·ownership no-side-effect 증거 보강**
**Goal 실행 `P3-R4`:** `REV-013`~`REV-014`에서 남은 audio upload/event 실패와 실제 endpoint ownership/domain 오류의 부작용 경계를 고정한다.
- **추적 review ID:** `REV-013`, `REV-014`.
- **시작 조건:** `P2-R3-GATE`와 기존 `P3-R2-GATE` 완료.
- **완료 증거:** create/update/ownership actual endpoint test, legacy characterization, content/common 회귀, `ktlintCheck`와 Progress 기록.
- **범위 밖:** S3 보상 정책 신설, audio file 수정 지원, Phase 4 기능 구현.
- **TDD 예외 사유:** 현재 production 실패가 아니라 `Task 3.9`~`Task 3.10` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
- **대체 검증 방법:** cover 이후 audio upload와 event 실패, ownership/domain 거부를 실제 endpoint에서 먼저 characterization하고 계약 불일치가 재현될 때만 최소 수정한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
- [x] **FAILURE CHARACTERIZATION:** 두 번째 S3 audio upload 실패와 event publish 실패에서 DB rollback, S3 호출·잔존 결과와 event 결과를 각각 고정한다.
- [x] **CONTRACT TEST:** 목록·상세·생성·수정의 target/ownership/domain 거부를 KO/EN/JA exact envelope로 확인한다.
- [x] **CONTRACT TEST:** 각 거부 뒤 AudioContent·SeriesContent·S3·event의 insert/update/delete count가 변하지 않음을 직접 단언한다.
- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, legacy 비트랜잭션 S3 경계와 일치하면 production code를 변경하지 않는다.
- [x] **REFACTOR:** content/common/legacy 회귀와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
- 검증 기록: 무엇: `REV-013`~`REV-014`의 content 생성 후반 실패와 ownership/domain no-side-effect 증거를 보강했다. 왜: 기존 완료 기록이 cover upload 실패와 일부 ownership 경로에 치우쳐 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 audio upload 실패와 event publish 실패를 추가하고, `AiCharacterAdminAudioContentOwnershipTest`에 목록·상세·생성·수정 unknown target KO/EN/JA 및 AudioContent·SeriesContent·S3·event 무변경 단언을 추가했다. 결과: create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`였다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 3 4차 리뷰 Gate
**Goal 실행 `P3-R3-GATE`:** `REV-013`~`REV-014`의 직접 증거를 재검토하고 Phase 3 4차 리뷰를 종결한다.
- [x] **`P3-R3-GATE` 완료:** `P3-R4` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
- **시작 조건:** `P3-R4` 완료.
- **완료 증거:** 두 review ID 처리 완료, 위 두 명령 성공, `phase3-audio-content-review.md` 최신 결론과 Progress 동기화.
- **범위 밖:** 기존 Phase 3 완료 이력 수정, Phase 4 기능 구현.
- 검증 기록: 무엇: Phase 3 4차 리뷰의 `REV-013`~`REV-014` 처리를 종결했다. 왜: 사용자 지시에 따라 Phase 3 후속 보완까지만 완료하고 Phase 4로 넘어가지 않기 위해. 어떻게: content/common 회귀와 최종 `ktlintCheck`를 fresh 실행하고 `phase3-audio-content-review.md`를 처리 완료로 갱신했다. 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다.
#### Phase 3 5차 리뷰 보완
- [x] **Task 3.12: 생성 필수 multipart part의 exact binding 계약 복구**
**Goal 실행 `P3-R5`:** `REV-016`의 생성 필수 파일 part를 MVC non-null binding으로 고정하고 세 part 누락의 exact exception·KO/EN/JA 계약을 복구한다.
- **추적 review ID:** `REV-016`.
- **시작 조건:** `P2-R4-GATE` 완료와 PRD API Expectations 179~180의 missing-part 계약.
- **완료 증거:** 세 part별 RED/GREEN, exact `MissingServletRequestPartException`, facade/DB/S3/event 0회, content/common 회귀와 Progress 기록.
- **범위 밖:** legacy `AudioContentService.createAudioContent` signature 변경, upload/processing pipeline 정책 변경, 오디오 파일 교체.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **RED:** `coverImage`, `audioFile`, `request` 각각을 누락한 KO/EN/JA actual endpoint test에서 `MvcResult.resolvedException`이 정확히 `MissingServletRequestPartException`이고 message가 `common.error.invalid_request`인지 단언한다.
- [x] **RED 확인:** create focused test를 실행해 nullable `coverImage`·`audioFile`이 legacy `SodaException`까지 전달되어 exact exception/message assertion이 실패하는지 확인한다.
- [x] **GREEN:** 생성 controller와 facade의 `coverImage`, `audioFile`을 non-null `MultipartFile`로 바꾸고 legacy service에는 검증된 non-null 값을 그대로 전달한다.
- [x] **GREEN 확인:** 같은 focused test를 재실행해 세 part 누락 9건의 exact exception·KO/EN/JA 400 envelope과 facade/DB/S3/event 0회를 확인한다.
- [x] **REFACTOR:** 중복된 missing-part request/assertion만 parameterized helper로 정리하고 create/error/legacy 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
- [x] **Task 3.13: ownership/domain KO·EN·JA no-side-effect matrix 완결**
**Goal 실행 `P3-R6`:** `REV-017`의 cross-owner와 domain validation 경로를 actual endpoint KO/EN/JA 및 DB/S3/event 무변경 증거로 완결한다.
- **추적 review ID:** `REV-017`.
- **시작 조건:** `P3-R5` 완료와 기존 `P3-R4` unknown target matrix.
- **완료 증거:** cross-owner detail/update, create/update other-owner series, invalid date의 exact envelope·side-effect assertions, content/common 회귀와 Progress 기록.
- **범위 밖:** 새로운 ownership 정책, 오류 key/status 변경, Phase 4 series API 구현.
- **TDD 예외 사유:** 현재 production 위반보다 `REV-014` 완료 기록 대비 대표 ownership/domain 직접 증거 누락이 확정된 test 보강 Task다.
- **대체 검증 방법:** 기존 실제 endpoint test를 KO/EN/JA parameterized matrix로 확장하고 요청 전후 entity field·연결 row와 S3/event interaction을 비교한다. 실패가 재현될 때만 validation 순서를 최소 수정한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **OWNERSHIP TEST:** 다른 캐릭터 소유 콘텐츠의 detail/update를 KO/EN/JA actual endpoint matrix로 만들고 content field·S3·event 무변경을 단언한다.
- [x] **DOMAIN TEST:** create/update의 다른 owner `seriesIds`와 invalid `releaseDateUtc`를 KO/EN/JA matrix로 만들고 AudioContent·SeriesContent insert/update/delete, S3, event 0회를 단언한다.
- [x] **NON-VACUOUS 확인:** owner 또는 validation guard를 제거하면 각 matrix가 status/message 또는 side-effect assertion으로 실패하는지 확인한다.
- [x] **GREEN:** 현재 계약 위반이 재현될 때만 target/ownership/domain 선검증 순서를 최소 수정하고, 이미 충족하면 test-only로 종료한다.
- [x] **REFACTOR:** unknown target과 cross-owner/domain fixture의 공통 assertion만 정리하고 content/common 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 3 5차 리뷰 Gate
**Goal 실행 `P3-R4-GATE`:** `REV-016`~`REV-017`의 exact multipart와 ownership/domain 증거를 재검토하고 Phase 3 5차 리뷰를 종결한다.
- [x] **`P3-R4-GATE` 완료:** `P3-R5`, `P3-R6` 완료 후 위 content/common 회귀와 lint를 fresh 실행하고 리뷰 문서·Progress를 갱신한다.
- **시작 조건:** `P3-R5`, `P3-R6` 완료.
- **완료 증거:** `REV-016`, `REV-017` 수정 완료, 세 필수 part와 ownership/domain matrix 직접 증거, focused/영향 범위 회귀와 lint 성공.
- **범위 밖:** 기존 Phase 3 완료 이력 수정, Phase 4 기능 구현.
#### Phase 3 6차 리뷰 보완
- [x] **Task 3.14: 빈 multipart 파일 계약 고정**
**Goal 실행 `P3-R7`:** `REV-019`의 생성·수정 empty-file 경계를 v2 facade에서 고정해 0-byte upload와 수정
`audioFile` 계약 우회를 차단한다.
- **추적 review ID:** `REV-019`.
- **시작 조건:** `P2-R5-GATE`와 기존 `P3-R4-GATE` 완료.
- **완료 증거:** 생성 empty cover/audio 거부, 수정 empty cover 유지, empty/non-empty audio 거부 actual endpoint
RED/GREEN과 DB/S3/event assertion, content/common 회귀 및 Progress 기록.
- **범위 밖:** legacy `AudioContentService`·`CreatorAdminContentService` 공용 계약 변경, 파일 content-type/확장자 정책 추가,
오디오 파일 교체, Phase 4 기능 구현.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- [x] **RED:** 생성의 빈 `coverImage`와 빈 `audioFile` actual endpoint를 KO/EN/JA로 요청해 400
`common.error.invalid_request`, AudioContent/SeriesContent·S3·event 0회를 기대하고 현재 200/업로드 경로로 실패함을
확인한다.
- [x] **RED:** 수정의 빈 `coverImage`가 생략과 동일하게 기존 cover path를 유지하고 S3를 호출하지 않는 기대, 빈
`audioFile` part가 non-empty와 동일하게 400으로 거부되는 기대가 현재 실패함을 확인한다.
- [x] **GREEN:** create 시작 시 `coverImage.isEmpty || audioFile.isEmpty`를 `invalidRequest()`로 거부한다.
- [x] **GREEN:** update는 `audioFile != null`이면 크기와 관계없이 `invalidRequest()`로 거부하고,
`coverImage?.takeUnless { it.isEmpty }`만 legacy update service에 전달한다.
- [x] **GREEN 확인:** 같은 focused test를 재실행해 create empty-file의 부작용 0회, update empty cover의 DB/S3 유지와
empty/non-empty audio 거부가 모두 통과하는지 확인한다.
- [x] **REFACTOR:** empty-file fixture만 공통화하고 legacy service를 수정하지 않은 채 content/common 회귀와
`ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 생성 empty `coverImage`/`audioFile` KO/EN/JA와 수정 empty cover/audio actual endpoint 계약을 추가했다. 왜: 빈 multipart 파일이 null/non-empty 검사 사이를 통과하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest`를 실행했다. 결과: 26건 중 신규 8건이 line 119, 204, 236에서 실패해 RED를 확인했다.
- 검증 기록(GREEN): 무엇: v2 facade empty-file 경계와 실제 service publisher no-interaction 증거. 왜: legacy 공용 service 변경 없이 신규 관리자 API 계약만 고정하고 event 부작용 assertion이 detached mock을 보지 않게 하기 위해. 어떻게: 같은 focused 명령을 재실행했다. 결과: reviewer gate 보완 후 최종 empty create/update focused는 `BUILD SUCCESSFUL in 43s`, non-empty audio update 보완 focused는 `BUILD SUCCESSFUL in 44s`였다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
- [x] **Task 3.15: ownership/domain event 0회 실제 publisher 증거 복구**
**Goal 실행 `P3-R8`:** `REV-020`의 ownership/domain no-side-effect test가 실제 `AudioContentService`와
`CreatorAdminContentService`의 publisher를 관찰하도록 연결해 NON-VACUOUS 완료 증거를 복구한다.
- **추적 review ID:** `REV-020`.
- **시작 조건:** `P3-R7` 완료.
- **완료 증거:** 실제 두 service proxy target의 publisher 교체·복원, mock identity 확인, ownership/domain matrix의
event 0회와 content/common 회귀 및 Progress 기록.
- **범위 밖:** production event 발행 순서·payload 변경, application context event infrastructure 변경, 신규 test 전용
production seam 추가, Phase 4 기능 구현.
- **TDD 예외 사유:** 현재 production의 잘못된 event 발행이 아니라 detached mock으로 인한 완료 증거 공백이 확정된
test-only Task다.
- **대체 검증 방법:** Phase 2와 기존 content create event failure test의 `AopTestUtils`·`ReflectionTestUtils` 방식을
재사용해 실제 proxy target field와 mock identity를 확인한다.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentService.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- [x] **EVIDENCE RED:** 현재 `@MockBean ApplicationEventPublisher`와 실제 두 service proxy target의 publisher가 같은
instance인지 단언해 detached 상태에서 실패함을 확인한다.
- [x] **EVIDENCE GREEN:** 각 test에서 `AudioContentService`, `CreatorAdminContentService` proxy target의 기존 publisher를
보관하고 같은 mock으로 교체하며 `finally`/teardown에서 원래 publisher를 복원한다.
- [x] **NON-VACUOUS 확인:** 교체 직후 실제 두 target field가 mock과 같은 instance인지 단언하고, cross-owner
detail/update·other-owner series·invalid date 및 unknown target matrix가 실제 publisher no-interaction을 통과하는지
확인한다.
- [x] **회귀 확인:** 기존 create event failure helper와 충돌하지 않고 정상 content 생성·수정의 event 회귀가 유지되는지
content/common 명령으로 확인한다.
- [x] **REFACTOR:** Phase 2 및 create failure test의 기존 helper 패턴 범위에서만 중복을 정리하고 production seam이나
공용 test abstraction은 추가하지 않는다.
- 검증 기록: 무엇: `REV-020`의 실제 publisher no-interaction 증거를 복구했다. 왜: detached `@MockBean ApplicationEventPublisher`만 검증하면 실제 service field 호출 여부를 증명할 수 없기 때문이다. 어떻게: `AudioContentService`, `CreatorAdminContentService` proxy target의 `applicationEventPublisher`를 테스트 mock으로 교체·복원하고 field identity를 단언한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest`를 실행했다. 결과: `BUILD SUCCESSFUL in 39s`였다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 3 6차 리뷰 Gate
**Goal 실행 `P3-R5-GATE`:** `REV-019`~`REV-020`의 empty-file 계약과 actual publisher 증거를 재검토하고 Phase 3
6차 리뷰를 종결한다.
- [x] **`P3-R5-GATE` 완료:** `P3-R7`, `P3-R8` 완료 후 위 content/common 회귀와 lint를 fresh 실행하고
리뷰 문서·Progress를 갱신한다.
- **시작 조건:** `P3-R7`, `P3-R8` 완료.
- **완료 증거:** `REV-019`, `REV-020` 처리 완료, 생성·수정 empty-file actual endpoint 계약, 실제 두 service publisher
no-interaction 증거, focused/영향 범위 회귀와 lint·diff check 성공.
- **범위 밖:** Gate에서 production code 수정, 기존 Phase 3 완료 이력 변경, Phase 4 기능 구현.
#### Phase 3 7차 리뷰 보완
- [x] **Task 3.19: 관리자 오디오 repository의 미사용 확장 제거**
**Goal 실행 `P3-R9`:** `REV-022`의 실제 호출되지 않는 조회·시리즈 교체 helper와 그 전용 status enum을 제거해
repository를 현재 owner-scoped 상세 조회 책임으로 축소한다.
- **추적 review ID:** `REV-022`.
- **시작 조건:** `P2-R6-GATE` 완료와 `phase3-audio-content-review.md` 7차 리뷰 판정 존재.
- **완료 증거:** 호출 검색 결과와 일치하는 미사용 method/enum/import 제거 diff, owner-scoped 상세 조회 회귀,
content/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 실제 사용 중인 `findByIdAndCreatorMemberId`, legacy repository/service, 콘텐츠·시리즈 동작 변경,
인접 repository 리팩터링.
- **TDD 예외 사유:** 호출자가 없는 내부 코드 제거이며 외부 동작이나 계약을 추가하지 않는 동작 불변 리팩터링이다.
- **대체 검증 방법:** production/test 전체 호출 검색으로 제거 대상을 확정하고 상세·ownership 테스트를 focused 회귀한다.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- [x] **REFACTOR:** `findPage`, `findSeriesIds`, `replaceSeriesIds`, 그 private helper와
`AiCharacterAdminAudioContentStatus`를 제거하고 발생한 unused import만 정리한다.
- [x] **STATIC 확인:** `findByIdAndCreatorMemberId` 외 제거 대상의 호출이 0건인지 production/test 전체에서 확인한다.
- [x] **회귀 확인:** 콘텐츠 상세·ownership focused test, content/common 영향 범위 회귀와 `ktlintCheck`를 실행한다.
- 검증 기록: 무엇: `REV-022`의 관리자 오디오 repository 미사용 확장과 전용 status enum을 제거했다. 왜: 현재
facade가 사용하는 repository 경계는 owner-scoped 상세 조회 `findByIdAndCreatorMemberId` 하나뿐이기 때문이다. 어떻게:
`findPage`, `findSeriesIds`, `replaceSeriesIds`, `hasActiveSeriesIds`, 관련 private helper와
`AiCharacterAdminAudioContentStatus`를 제거하고 package-scoped 호출 검색을 실행했다. 결과: 대상 package 호출 검색은
출력이 없었고, 상세·ownership focused 명령은 `BUILD SUCCESSFUL in 3m 39s`, content/common 영향 범위 회귀는
`BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`, `git diff --check`는 출력이 없었다.
```bash
rg -n 'findPage|findSeriesIds|replaceSeriesIds|hasActiveSeriesIds|AiCharacterAdminAudioContentStatus' src/main src/test
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 7차 리뷰 Gate
**Goal 실행 `P3-R9-GATE`:** `REV-022`의 미사용 코드 제거와 콘텐츠 동작 불변 증거를 재검토한다.
- [x] **`P3-R9-GATE` 완료:** `P3-R9` 완료 후 위 static/focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P3-R9` 완료.
- **완료 증거:** `REV-022` 처리 완료, owner-scoped 상세 동작 유지, focused/영향 범위 회귀와 lint·diff 성공.
- **범위 밖:** Gate에서 production code 수정, legacy 콘텐츠·시리즈 동작 변경.
- 검증 기록: 무엇: `P3-R9-GATE`에서 `REV-022` 처리 완료와 Phase 3 7차 리뷰 종결을 확인했다. 왜: Phase 4 7차
보완의 시작 조건이 `P3-R9-GATE` 완료이기 때문이다. 어떻게: `phase3-audio-content-review.md` 7차 리뷰 후속 판정을
갱신하고 위 static/focused/영향 범위 회귀, lint, diff check 결과를 대조했다. 결과: `REV-022`는 처리 완료로
판정했고 Phase 3은 19/19 완료 상태로 동기화했다.
#### Phase 3 8차 리뷰 보완
- [x] **Task 3.20: 오디오 생성 날짜·시간대 의미 검증 복구**
**Goal 실행 `P3-R10`:** `REV-030`의 잘못된 `releaseDate` 형식과 `timezone` 값을 legacy service 호출 전에
400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-030`.
- **시작 조건:** `P3-R9-GATE` 완료와 `phase3-audio-content-review.md` 8차 리뷰 판정 존재.
- **완료 증거:** 잘못된 날짜 형식·시간대의 actual endpoint RED, KO/EN/JA 400 envelope과 DB/S3/event 0건,
정상 생성 및 content/common 영향 범위 회귀, lint·diff와 Progress 기록.
- **범위 밖:** OpenAPI field/schema 변경, legacy `AudioContentService` 전역 동작 변경, 공통 예외 handler에
`DateTimeException`을 일괄 client 오류로 추가, upload/processing pipeline 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 그 밖의 필드는 유효한 생성 request에서 `releaseDate="not-a-date"`와
`timezone="Invalid/Zone"`을 각각 보내 현재 500 `common.error.unknown`이 반환되는지 확인한다.
- [x] **RED:** 두 입력을 KO/EN/JA actual endpoint matrix로 고정하고 AudioContent·S3·event가 요청 전후
변하지 않음을 단언한다.
- [x] **GREEN:** strict JSON parse 결과를 재사용해 `releaseDate`의 `yyyy-MM-dd HH:mm` 형식과 `timezone`의
`ZoneId`를 legacy service 호출 전에 검증하고 `DateTimeException`을 해당 입력 경계에서만 400으로 변환한다.
- [x] **REFACTOR:** 정상 예약/즉시 생성과 기존 preview/theme 오류 key를 유지하고 content/common 영향 범위 회귀,
`ktlintCheck`, `git diff --check`를 실행해 Progress에 기록한다.
- 검증 기록(RED): 무엇: 오디오 생성의 잘못된 `releaseDate` 형식과 `timezone` 의미 오류를 KO/EN/JA actual endpoint로 고정했다. 왜: strict JSON parse는 통과하지만 legacy `AudioContentService`의 Java time 변환 예외가 500으로 분류됐기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 6개 matrix를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`를 실행했다. 결과: 신규 6건이 400 기대 assertion에서 실패해 `BUILD FAILED in 1m 4s`를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: v2 facade 생성 경계에서 `releaseDate`를 `yyyy-MM-dd HH:mm`으로, `timezone`을 `ZoneId`로 legacy 호출 전에 검증했다. 왜: 전역 handler나 legacy service 영향 없이 신규 관리자 생성 API의 client 오류만 400으로 분류하기 위해. 어떻게: strict parse 결과를 재사용해 `DateTimeException`을 `common.error.invalid_request`로 변환하고 focused/영향 범위 회귀를 실행했다. 결과: 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`였다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 8차 리뷰 Gate
**Goal 실행 `P3-R10-GATE`:** `REV-030`의 의미 검증과 오류·no-side-effect 계약을 재검토한다.
- [x] **`P3-R10-GATE` 완료:** `P3-R10` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P3-R10` 완료.
- **완료 증거:** `REV-030` 처리 완료, 잘못된 날짜·시간대 400/no-side-effect, 정상 생성 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록: 무엇: `P3-R10-GATE`에서 `REV-030` 처리 완료와 Phase 3 8차 리뷰 종결을 확인했다. 왜: Phase 4 2차 보완으로 넘어가기 전에 오디오 생성 의미 검증과 영향 범위 회귀가 완료됐는지 판정하기 위해. 어떻게: `phase3-audio-content-review.md`에 처리 결과를 누적하고 위 focused/영향 범위 회귀와 lint 결과를 대조했다. 결과: 잘못된 날짜·시간대는 400/no-side-effect로 고정됐고 Phase 3은 20/20 완료 상태로 동기화했다.
#### Phase 3 9차 리뷰 보완
- [x] **Task 3.21: 오디오 상세의 예약 공개일 레거시 표시 복구**
**Goal 실행 `P3-R11`:** 미래 예약 콘텐츠 상세의 `releaseDate`를 KO/EN/JA 레거시 형식으로 반환하고, 공개 시각이 지난
콘텐츠는 기존처럼 null을 반환한다.
- **추적 review ID:** `REV-036`.
- **시작 조건:** `P2-R7-GATE` 완료와 `phase3-audio-content-review.md` 9차 정적 리뷰 판정 존재.
- **완료 증거:** 미래·과거 예약일과 KO/EN/JA locale actual endpoint RED/GREEN, signed URL·전체 상세 DTO 불변,
content/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** OpenAPI schema 변경, 레거시 `AudioContentService` 변경, 예약 공개·signed URL 정책 재설계,
목록 response mapping 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Confirm: `src/main/resources/messages*.properties`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 미래 UTC `releaseDate`를 가진 owner 콘텐츠 상세가 현재 모든 locale에서 null을 반환하는 계약 차이를
actual endpoint로 고정한다.
- [x] **GREEN:** 기존 `SodaMessageSource`와 `LangContext`를 사용해 legacy의 미래 여부, UTC→Asia/Seoul 변환,
`content.release_date.format` 포맷을 동일하게 적용한다.
- [x] **CONTRACT TEST:** 미래 예약일은 KO/EN/JA 형식 문자열, 현재 또는 과거 예약일은 null이며 나머지 상세 필드와
signed URL 결과가 변하지 않는지 확인한다.
- [x] **REFACTOR:** 단일 mapper 안에서 legacy 규칙만 최소 이관하고 content package와 공통 authorization/error 회귀,
`ktlintCheck`, `git diff --check`를 실행해 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 9차 리뷰 Gate
**Goal 실행 `P3-R11-GATE`:** `REV-036`의 미래·과거 예약일과 locale별 상세 응답 parity를 재검토한다.
- [x] **`P3-R11-GATE` 완료:** `P3-R11` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P3-R11` 완료.
- **완료 증거:** review ID 처리 완료, 미래 KO/EN/JA `releaseDate`, 과거 null, signed URL·상세 DTO 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록(RED): 무엇: 미래 예약 오디오 상세 `releaseDate` KO/EN/JA. 왜: mapper가 모든 상세 `releaseDate`를 null로 고정하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest`를 실행했다. 결과: 신규 미래 locale 3건이 기대 문자열 대비 null로 실패했다.
- 검증 기록(GREEN/GATE): 무엇: 미래 예약일의 레거시 locale 표시와 과거 null. 왜: 기존 `content.release_date.format`과 UTC→Asia/Seoul 변환을 신규 상세 endpoint에 맞추기 위해. 어떻게: 같은 focused 명령 재실행 후 targeted/전체/lint/OpenAPI/mapping/diff 검증을 실행했다. 결과: focused query test와 전체 검증이 모두 성공했다.
#### Phase 3 11차 리뷰 보완
- [x] **Task 3.22: 오디오 생성 primitive 필드의 null·누락 계약 강제**
**Goal 실행 `P3-R12`:** 오디오 생성의 필수 `price` 누락·null과 non-null primitive의 명시적 null을 JVM 기본값으로
보정하지 않고 파일 업로드·DB·event 전에 400으로 거부하며, optional 필드 생략 시 기존 기본값은 유지한다.
- **추적 review ID:** `REV-041`.
- **시작 조건:** `P2-R8-GATE` 완료와 `phase3-audio-content-review.md` 11차 정적 리뷰 판정 존재.
- **완료 증거:** 필수 `price` 누락·null과 optional primitive null의 actual endpoint RED/GREEN/no-side-effect,
optional 생략·정상 생성 회귀, content/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/service 변경, OpenAPI schema·기본값 변경, 별도 입력 범위 정책 추가.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** `price` 누락·null과 `themeId`, `isAdult`, `isGeneratePreview`, `isOnlyRental`,
`isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`의 명시적 null이 현재 기본값으로 처리되는지 actual
endpoint matrix로 고정하고 AudioContent·S3·event 무변경을 단언한다.
- [x] **GREEN:** v2 생성 경계에서 OpenAPI required/non-null primitive의 존재와 null 여부를 검증하고
`common.error.invalid_request` 400으로 변환한다.
- [x] **CONTRACT TEST:** optional primitive를 생략하면 OpenAPI·레거시 기본값을 유지하고 정상 예약·즉시 생성,
기존 미지 필드·날짜·시간대 검증이 변하지 않는지 확인한다.
- [x] **REFACTOR:** 기존 strict parse 결과를 재사용하는 최소 검증으로 제한하고 content/common 영향 범위 회귀,
`ktlintCheck`, `git diff --check`를 실행해 기록한다.
- 검증 기록(RED): 무엇: 오디오 생성 `price` 누락·null과 primitive field explicit null actual POST. 왜: Jackson primitive 기본값 `0`/`false` 보정으로 파일 업로드·DB·event 부작용이 발생할 수 있는지 재현하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests "*shouldRejectMissingOrNullPriceBeforeUpload" --tests "*shouldRejectNullPrimitiveFieldsBeforeUpload"`를 실행했다. 결과: 8개 invocation이 `status().isBadRequest` 기대에서 실패해 `BUILD FAILED`였다. `themeId:null`은 기존 missing-theme 검증으로 이미 400이었다.
- 검증 기록(GREEN): 무엇: v2 오디오 생성 request reader의 primitive null/누락 거부. 왜: 전역 mapper·레거시 DTO/service 변경 없이 v2 생성 경계에서 OpenAPI required/non-null primitive 계약을 강제하기 위해. 어떻게: 같은 명령을 재실행했다. 결과: `BUILD SUCCESSFUL in 42s`였다.
- 검증 기록(GATE): 무엇: optional 생략 기본값, 정상 생성, 날짜/시간대·미지 필드 검증, content/common 영향 범위와 lint/diff. 왜: `REV-041` 보완이 기존 오디오 생성·공통 오류/인가 계약을 회귀시키지 않는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*" --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: 각 Gradle 명령은 `BUILD SUCCESSFUL`이었고 `git diff --check`는 출력이 없었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 11차 리뷰 Gate
**Goal 실행 `P3-R12-GATE`:** `REV-041`의 오디오 생성 primitive nullability와 기본값·부작용 경계를 재검토한다.
- [x] **`P3-R12-GATE` 완료:** `P3-R12` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P3-R12` 완료.
- **완료 증거:** review ID 처리 완료, invalid primitive 400/no-side-effect, 생략 기본값과 정상 생성 회귀 성공.
- **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경.
#### Phase 3 후속 기능 보완
- [x] **Task 3.23: 오디오 콘텐츠 댓글 CRUD**
**Goal 실행 `P3-R13`:** target AI 캐릭터 소유 활성 오디오 콘텐츠의 원댓글·답글을 조회하고 target AI 명의로
작성·수정하며, 해당 콘텐츠에 달린 댓글·답글은 작성자와 관계없이 row 단위로 soft delete한다.
- **추적 review ID:** `REV-045`.
- **시작 조건:** `P2-R9-GATE` 완료와 PRD·OpenAPI의 승인된 댓글 행위자·소유권 계약 존재.
- **완료 증거:** 5개 actual endpoint, root/reply 조회, target AI 작성, 작성자 제한 수정, owner 범위 삭제,
cross-resource/parent/character 격리, idempotent delete와 exact response 회귀.
- **범위 밖:** 캐릭터 직접 댓글 삭제, 댓글 hard delete·cascade, 새 pagination wrapper, 레거시/public endpoint 변경.
**Interfaces:**
- `GET|POST /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments`
- `PUT|DELETE /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}`
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}/replies`
- 조회 query: 필수 `timezone`, `page`, `size`; response `GetAudioContentCommentListResponse(totalCount, items)`.
- 작성 body: 필수 `comment`, optional/nullable `parentId`, optional `isSecret=false`, optional/nullable `languageCode`.
- 수정 body: 필수 `comment`; mutation 성공 `data: null`.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCommentTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** root/reply 목록의 `timezone/page/size`, `totalCount/items`, target 소유 활성 콘텐츠 경계를 actual
GET으로 고정한다.
- [x] **RED:** root와 reply 작성 시 저장된 `member`가 target `creatorMember`이고, `parentId`가 같은 콘텐츠의 활성
root가 아니면 400/no insert/no event인지 고정한다.
- [x] **RED:** target AI가 작성한 활성 댓글/답글만 수정되고 팬 작성, 다른 콘텐츠·캐릭터 댓글 수정은
400/no mutation인지 고정한다.
- [x] **RED:** target 소유 콘텐츠의 팬/AI 댓글·답글 삭제는 해당 row만 비활성화하고 하위 답글은 유지하며, 이미
비활성인 row는 200 no-op인지 고정한다.
- [x] **GREEN:** 기존 `AudioContentCommentService`의 조회·작성·수정 의미를 재사용하되 facade에서 target,
active owner, 동일 리소스 root parent, actor 권한을 먼저 검증한다.
- [x] **CONTRACT TEST:** 미지 필드, 잘못된 page/size/timezone, cross-resource ID의 400
envelope와 모든 mutation의 `data: null`을 확인한다.
- [x] **REFACTOR:** 댓글 전용 추상화나 cascade 로직을 추가하지 않고 content/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCommentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 후속 기능 Gate
**Goal 실행 `P3-R13-GATE`:** `REV-045`의 댓글 actor·owner·parent·soft delete 경계를 재검토한다.
- [x] **`P3-R13-GATE` 완료:** `P3-R13` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 3 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P3-R13` 완료.
- **완료 증거:** 5개 operation, 레거시 목록 parity, AI 작성·수정 제한, owner 범위 row soft delete,
cross-resource no-side-effect 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 3 UTC 날짜 계약 보완
- [x] **Task 3.24: 오디오 생성·상세·댓글의 timezone 제거와 UTC 계약 정합화**
**Goal 실행 `P3-R14`:** 신규 관리자 오디오 생성에서 `timezone`을 제거하고 nullable `releaseDate`를 UTC
`date-time`으로 받으며, 상세·댓글·답글 조회도 `timezone` 없이 기존 날짜 필드를 ISO-8601 UTC(`Z`)로 반환한다.
- **추적 review ID:** `REV-050`.
- **시작 조건:** `P3-R13-GATE` 완료와 `DEC-UTC-DATE-001` 및 OpenAPI 2.2.0 계약 존재.
- **완료 증거:** 오디오 생성·상세·댓글·답글 4개 actual operation의 query/body·UTC exact JSON RED/GREEN,
기존 상세 `releaseDate` null/노출 조건·댓글 pagination/ownership 보존, legacy/public 회귀와 Progress 기록.
- **범위 밖:** 오디오 목록의 날짜 필드 변경, legacy/public request/response 변경, 로컬 시각+timezone 병행 지원,
신규 dependency·DDL, 댓글 mutation 의미 변경.
**Interfaces:**
- `POST /api/v2/admin/ai-characters/{characterId}/audio-contents`: multipart `request`에 `timezone`이 없고,
optional/nullable `releaseDate`는 ISO-8601 UTC(`Z`)다.
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`: query 없음. nullable
`releaseDate`는 기존 미래 예약일 노출·현재/과거 null 조건을 유지하고 값이 있으면 ISO-8601 UTC(`Z`)다.
- `GET .../audio-contents/{contentId}/comments`와 `GET .../comments/{commentId}/replies`: query는 `page`,
`size`만 사용하고 `totalCount`, `items`와 각 item의 기존 `date` 필드명을 유지한다. `date` 값은 ISO-8601 UTC(`Z`)다.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCommentTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** `timezone` 없는 상세·댓글·답글 GET이 현재 400이고, 현재 상세 `releaseDate`와 댓글 `date`가
locale/legacy 문자열인 계약 차이를 actual endpoint로 고정한다.
- [x] **RED:** `timezone` 없는 생성 request의 UTC `releaseDate`가 현재 legacy 형식 검증에서 거부되는 것과,
로컬 문자열·non-UTC offset·`timezone` 미지 필드가 400/no upload/no DB/no event인지 고정한다.
- [x] **GREEN:** v2 전용 생성 DTO에서 `timezone`을 제거하고 UTC instant를 한 번 파싱한다. 초 단위를 버리는 문자열
재포맷을 하지 않고 UTC `LocalDateTime`을 내부 생성 경계에 전달하되, 기존 legacy 생성 진입점의 외부 계약은 유지한다.
- [x] **GREEN:** controller/facade의 세 GET signature와 timezone 검증을 제거하고, 상세 mapper와 v2 owner-scoped
댓글 query/mapping에서 기존 `toUtcIso()`를 재사용해 `releaseDate`/`date`만 UTC로 직렬화한다.
- [x] **CONTRACT TEST:** 생성 `releaseDate` 생략·null·정상 UTC, 상세 미래 UTC·현재/과거 null, root/reply UTC
`date`, `totalCount/items`, page/size와 target/owner/block/secret 필터가 기존 의미를 유지하는지 확인한다.
- [x] **REFACTOR:** legacy/public controller·DTO·timezone 동작을 변경하지 않고 v2 경계의 최소 분기만 남긴다.
content/common 및 직접 영향 legacy 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다.
```bash
./gradlew test \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCommentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest \
--tests kr.co.vividnext.sodalive.content.AudioContentServiceTest
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 36
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 34
and ([$operations[] | select(.["x-implementation-status"] == "alignment-required")] | length) == 2
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 3 UTC 날짜 계약 Gate
**Goal 실행 `P3-R14-GATE`:** `REV-050`의 오디오 생성·상세·댓글 UTC 계약과 legacy/public 격리를 재검토한다.
- [x] **`P3-R14-GATE` 완료:** `P3-R14` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 3 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P3-R14` 완료.
- **완료 증거:** 오디오 4개 operation의 timezone 제거·UTC `releaseDate`/`date`, 기존 null/노출·pagination·ownership
및 legacy/public 계약 회귀 성공, OpenAPI 해당 4개 operation `implemented`.
- **범위 밖:** Gate에서 production code 또는 public/legacy API schema 변경.
- **`P3-R14` / `P3-R14-GATE` 검증(2026-07-29):** RED는 create/query/comment focused 명령에서 48개 중 9개가
기존 `timezone` 필수·legacy 날짜 포맷 차이로 실패해 `BUILD FAILED in 1m 1s`였다. v2 전용 생성 DTO와 UTC 내부 생성
경계, 세 GET query 제거·거부, `toUtcIso()` 응답 mapping 후 같은 focused 명령은 `BUILD SUCCESSFUL in 2m 13s`였다.
content/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 35s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 23s`였다.
OpenAPI 36개 operation 상태는 `implemented` 34개와 `alignment-required` 2개를 확인했고, `git diff --check`는 출력이
없었다. 전체 `./gradlew test`는 v2 audio/content-common 및 legacy service 회귀가 직접 영향 범위를 포함하므로 생략했다.
#### Phase 3 pagination 계약 후속 보완
- [x] **Task 3.25: 오디오 댓글·답글 목록의 optional page/size 기본값 복구**
**Goal 실행 `P3-R15`:** OpenAPI 공통 `Page`, `Size` 계약대로 오디오 댓글·답글 목록에서 `page`, `size` 생략과
부분 생략을 허용하고 각각 `0`, `20`을 적용한다.
- **추적 review ID:** `REV-052`.
- **시작 조건:** `P2-R10-GATE`, `P3-R14-GATE` 완료와 OpenAPI의 optional `Page`/`Size` 계약 존재.
- **완료 증거:** 두 actual GET에서 query 전체 생략·`page`만 지정·`size`만 지정 시 200과 기본값이 적용되고,
음수 page·1 미만 size·미지 query는 400이며 기존 pagination·UTC date·ownership/filter 결과가 유지된다.
- **범위 밖:** OpenAPI pagination schema 변경, FanTalk 보정 정책 적용, 댓글 mutation·legacy/public API 변경,
신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCommentTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 댓글·답글 GET에서 query 전체 생략과 한쪽만 지정한 요청이 현재 400인 것을 actual endpoint로 고정한다.
- [x] **GREEN:** controller의 `page`, `size`에 OpenAPI 기본값을 적용하고 facade의 query 이름 검증은 미지
parameter만 거부하도록 최소 수정한다.
- [x] **CONTRACT TEST:** 전체·부분 생략, 유효 page/size, 음수 page, 1 미만 size, `timezone` 등 미지 query와
기존 UTC exact JSON을 확인한다.
- [x] **REFACTOR:** 다른 목록 API와 legacy/public pagination은 변경하지 않고 content/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCommentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 pagination 계약 후속 Gate
**Goal 실행 `P3-R15-GATE`:** `REV-052` 수정 뒤 두 GET의 optional pagination과 미지 query 거부 경계를 재검토한다.
- [x] **`P3-R15-GATE` 완료:** `P3-R15` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 3 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P3-R15` 완료.
- **완료 증거:** 두 operation의 기본값·부분 생략·범위 오류·미지 query 및 기존 UTC/ownership 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
- **`P3-R15` / `P3-R15-GATE` 검증(2026-07-29):** RED는 `AiCharacterAdminAudioContentCommentTest` 9건 중
댓글·답글의 전체 생략 `isOk` 기대 2건이 각각 실패해 `BUILD FAILED in 45s`였다. controller의 두 목록 query에
`page=0`, `size=20` 기본값을 적용하고 facade가 known query 이름의 부분집합을 허용하도록 수정한 뒤 같은 focused
명령은 `BUILD SUCCESSFUL in 40s`였다. content package와 `AiCharacterAdminErrorContractTest` 영향 범위 회귀는
`BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 37s`였다. 전체 `./gradlew test`는
controller/facade와 해당 actual endpoint test만 변경했고 직접 영향 범위 회귀가 이를 포함하므로 실행하지 않았다.
#### Phase 3 multipart request part 계약 후속 보완
- [x] **Task 3.26: 오디오 생성·수정 request part의 application/json 강제**
**Goal 실행 `P3-R16`:** 오디오 생성·수정 multipart의 `request` part가 OpenAPI encoding대로
`application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다.
- **추적 review ID:** `REV-056`.
- **시작 조건:** `P3-R15-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재.
- **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·file/series/UTC 의미를 유지하고,
`text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header,
S3·DB·processing/event no-side-effect를 반환한다.
- **범위 밖:** JSON schema·strict reader·file empty 정책, series/UTC 의미, legacy/public endpoint,
OpenAPI·신규 dependency·DDL 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 현재 200을 기대하는 `text/plain` request part 수정 테스트를 OpenAPI의 415/no-side-effect 계약으로
교정하고 생성·수정 actual endpoint에서 재현한다.
- [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String
strict reader에 동일 payload를 전달한다.
- [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON,
필수 part 누락 400 및 기존 UTC/file/series 회귀를 확인한다.
- [x] **REFACTOR:** content facade/domain 로직을 변경하지 않고 content/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
```bash
./gradlew test \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 3 multipart request part 계약 후속 Gate
**Goal 실행 `P3-R16-GATE`:** `REV-056` 수정 뒤 AudioContent POST·PUT의 part-level JSON-only·415 경계를 재검토한다.
- [x] **`P3-R16-GATE` 완료:** `P3-R16` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 3 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P3-R16` 완료.
- **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 및 UTC/file/series 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
- **`P3-R16` / `P3-R16-GATE` 검증(2026-07-29):** RED는 새 POST·PUT의 `text/plain`·content type 누락
KO/EN/JA 415 기대와 기존 controller의 text/plain 성공 기대를 포함해 focused 83건 중 13건이 200 응답으로 실패해
`BUILD FAILED in 1m 15s`였다. controller의 request multipart header만 `application/json` 호환 여부를 확인하도록
하고 기존 strict String reader와 facade를 그대로 둔 뒤 focused는 `BUILD SUCCESSFUL in 56s`, content package와
`AiCharacterAdminErrorContractTest` 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 16s`였다. OpenAPI 두 AudioContent multipart encoding은 `application/json`으로 정적 대조했고,
`git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 v2 AudioContent controller와 실제 endpoint test에
변경을 한정했고 content/common 영향 범위 회귀가 이를 포함하므로 실행하지 않았다.
#### Phase 3 multipart part 이름 계약 후속 보완
- [x] **Task 3.27: 오디오 생성·수정의 미정의 multipart part 거부**
**Goal 실행 `P3-R17`:** AudioContent 생성은 `contentFile`, `coverImage`, `request`, 수정은
`coverImage`, `request` 외 multipart part를 business mutation 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-061`.
- **시작 조건:** `P2-R11-GATE` 완료와 `phase3-audio-content-review.md` 13차 정적 리뷰 판정 존재.
- **완료 증거:** POST·PUT의 미정의 part 400 KO/EN/JA envelope와 S3·DB·processing/event no-side-effect,
수정의 기존 `audioFile`·`contentFile` 거부 및 정상/필수 part/415 회귀.
- **범위 밖:** OpenAPI schema, file empty·UTC·series 의미, 전역 multipart resolver, legacy/public endpoint,
신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 생성·수정에 `unexpected` part를 추가해 현재 정상 mutation으로 진행되는 경로와 side effect를 actual
endpoint로 고정한다.
- [x] **GREEN:** 실제 part 이름 집합을 생성 `{contentFile, coverImage, request}`, 수정
`{coverImage, request}`와 비교해 초과 이름을 공통 400으로 거부한다. 수정 controller의 기존
`audioFile`·`contentFile` 인자는 제거하고 같은 미정의 part 검증으로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, 필수 part 누락, 빈 파일, request part 415,
수정 파일 교체 미지원과 no-side-effect를 확인한다.
- [x] **REFACTOR:** content controller/test만 최소 변경하고 content/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
**처리 기록 (2026-07-29 / P3-R17):**
- RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects'` → 새 테스트 6개가 400 기대 대비 기존 정상 mutation 경로로 실패.
- GREEN/focused: 동일 focused 명령 재실행 → `BUILD SUCCESSFUL in 1m 33s`.
- 파일 교체 회귀: `audioFile`, `contentFile` 수정 part를 `shouldRejectFileReplacementPartBeforeSideEffects`로 통합 확인, focused 재실행 → `BUILD SUCCESSFUL in 1m 59s`.
- 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 2m 27s`.
- 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 31s`; `git diff --check` → 출력 없음.
- OpenAPI 대조: `api-contract.openapi.json`의 AudioContent 생성 schema는 required `{contentFile, coverImage, request}`, 수정 schema는 `{coverImage, request}` 및 `additionalProperties: false` 유지 확인.
#### Phase 3 multipart part 이름 계약 후속 Gate
**Goal 실행 `P3-R17-GATE`:** `REV-061` 수정 뒤 AudioContent POST·PUT의 exact part 이름과 기존 파일·media type
경계를 재검토한다.
- [x] **`P3-R17-GATE` 완료:** `P3-R17` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P3-R17` 완료.
- **완료 증거:** 미정의 part 400/no-side-effect, 정상·필수·빈 파일·415·수정 교체 미지원 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
**Gate 기록 (2026-07-29 / P3-R17-GATE):**
- Focused: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests '*shouldRejectUndefinedMultipartPartBeforeSideEffects' --tests '*shouldRejectFileReplacementPartBeforeSideEffects'` → `BUILD SUCCESSFUL in 1m 30s`.
- 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 2m 14s`.
- 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 1s`; `git diff --check` → 출력 없음.
- 판정: AudioContent POST·PUT exact multipart part 이름과 기존 필수/빈 파일/request 415/파일 교체 미지원 회귀가 모두 통과해 Phase 3 완료.
#### Phase 3 multipart 일반 form-field part 후속 보완
- [x] **Task 3.28: 오디오 콘텐츠 생성·수정의 전체 multipart part 이름 검증**
**Goal 실행 `P3-R18`:** AudioContent POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한
모든 multipart part 이름을 검사해 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` 외 이름을
mutation 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-066`.
- **시작 조건:** `P2-R12-GATE` 완료와 `phase3-audio-content-review.md` 8차 정적 리뷰 판정 존재.
- **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 S3·DB·processing·event
no-side-effect, 기존 파일형 미정의 part·수정 파일 교체 거부·필수/빈 파일·request part 415 회귀 성공.
- **범위 밖:** OpenAPI schema, 파일 교체 지원, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재
`fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다.
- [x] **GREEN:** servlet request의 전체 part 이름 집합을 operation별 허용 집합과 비교해 초과 이름을 facade 진입
전에 공통 400으로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 기존 파일형 미정의 part, 수정 파일 교체 거부, 정상·필수·빈 파일,
request part 415와 no-side-effect를 확인한다.
- [x] **REFACTOR:** AudioContent controller/test만 최소 변경하고 content/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
#### Phase 3 multipart 전체 part 이름 후속 Gate
**Goal 실행 `P3-R18-GATE`:** `REV-066` 수정 뒤 AudioContent POST·PUT의 파일·일반 form-field를 포함한 전체 part
이름과 기존 파일·media type 경계를 재검토한다.
- [x] **`P3-R18-GATE` 완료:** `P3-R18` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 3
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P3-R18` 완료.
- **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
- 검증 기록(RED): 무엇: AudioContent POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다.
- 검증 기록(GREEN): 무엇: AudioContent multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 생성 `{contentFile, coverImage, request}`, 수정 `{coverImage, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다.
- 검증 기록(GATE): content/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다.
#### Phase 3 15차 리뷰 보완
- [x] **Task 3.29: v2 오디오 생성 preview 시간 검증 복구**
**Goal 실행 `P3-R19`:** `REV-072`에 따라 v2 오디오 생성도 기존 creator 생성과 동일하게
`previewStartTime`·`previewEndTime`의 쌍, 형식, 최소 15초 규칙을 파일 업로드와 DB·event 부작용 전에 검증한다.
- **추적 review ID:** `REV-072`.
- **시작 조건:** Phase 1~7 9차 정적 리뷰 판정과 `phase3-audio-content-review.md`의 `REV-072` 근거 존재.
- **완료 증거:** v2 actual endpoint의 한쪽만 입력, 잘못된 형식, 15초 미만 RED와
`content.error.preview_time_both_required`·`content.error.preview_time_format
`content.error.preview_time_minimum` 오류 계약, DB/S3/event 0건, 정상 preview 및 legacy/public 회귀,
`ktlintCheck`, `git diff --check`, Progress 기록.
- **범위 밖:** preview 규칙·오류 key 변경, OpenAPI field/schema 변경, upload/processing pipeline 변경,
관련 없는 `AudioContentService` refactor.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 나머지 필드는 유효한 actual endpoint 생성 request에 preview 시작만 입력, 형식 오류,
15초 미만 구간을 각각 보내 현재 200과 DB/S3/event 부작용이 발생하는 경로를 고정한다.
- [x] **GREEN:** 문자열 request overload에만 있던 `validatePreviewTime` 호출을 두 생성 경로가 공유하는
parsed request overload로 이동해 legacy와 v2가 검증을 정확히 한 번 수행하도록 한다.
- [x] **CONTRACT TEST:** KO/EN/JA의 세 기존 오류 key와 no-side-effect, 정상 15초 이상 preview의 metadata를
actual endpoint 및 service 단위에서 확인한다.
- [x] **REFACTOR:** 공유 검증 호출 위치만 최소 변경하고 v2 content 및 legacy creator content 영향 범위 회귀,
`ktlintCheck`, `git diff --check` 결과를 Progress에 기록한다.
- 검증 기록: 무엇: v2 오디오 생성 preview 시간 검증을 기존 creator 생성과 동일한 공유 parsed request overload로 복구했다. 왜:
`REV-072`처럼 v2 경로가 문자열 request overload의 검증을 우회해 잘못된 preview 입력이 DB/S3/event 경계로 진행될 수 있었기
때문이다. 어떻게: invalid preview actual endpoint 9건은 production 변경 전 400 기대 대비 200/부작용 경로로 실패했고, 테스트 JSON
조립 오류 수정 후 `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)` 시작부로 `validatePreviewTime`을 이동했다.
결과: `./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 preview 시간 검증 후속 Gate
**Goal 실행 `P3-R19-GATE`:** `REV-072` 수정 뒤 v2·legacy 생성의 preview 검증과 부작용 순서를 재판정한다.
- [x] **`P3-R19-GATE` 완료:** `P3-R19` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 3 리뷰·finding 상태·Progress를 갱신한다.
- **시작 조건:** `P3-R19` 완료.
- **완료 증거:** 세 preview 오류 계약과 DB/S3/event no-side-effect, 정상 preview 및 legacy/public 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest \
--tests kr.co.vividnext.sodalive.content.AudioContentServiceTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test'
./gradlew ktlintCheck
git diff --check
```
- Gate 검증 기록: 무엇: `REV-072` 수정 뒤 Phase 3 content와 legacy AudioContent 영향 범위를 재검증했다. 왜: 공유 service
overload 변경이 v2 actual endpoint와 legacy creator 생성 경로를 동시에 통과해야 하기 때문이다. 어떻게:
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test'`,
`./gradlew ktlintCheck`, `git diff --check`를 fresh 실행했다. 결과: 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 22s`,
`ktlintCheck`는 `BUILD SUCCESSFUL in 32s`, `git diff --check`는 출력 없이 통과했다.
---
### Phase 2·3 후속: 레거시 JSON 계약 정합화
기존 Phase 2·3 완료 이력은 보존한다. 2026-07-28 확정된 레거시 필드명·전체 payload 이관 정책에 따라 문서 계약을 먼저
고정하고, 현재 구현된 캐릭터·오디오 콘텐츠 v2 DTO와 endpoint를 별도 후속 Goal에서 정합화한다.
- [x] **Task 3.16: 전체 API OpenAPI 계약 고정**
**Goal 실행 `P23-CONTRACT-1`:** 23개 endpoint의 request/response를 레거시 DTO 전체 필드와 직접 대조해 OpenAPI 3.1
JSON과 설명 문서로 고정한다.
- **시작 조건:** 사용자 확정 정책과 시리즈 미연결 콘텐츠 검색 endpoint 분리 결정.
- **완료 증거:** JSON 문법·OpenAPI lint/validate·TypeScript client 생성 및 `tsc --noEmit` 성공, 23개 operation과 누락 `$ref` 0건,
`./gradlew tasks --all` 성공 및 검증 기록.
- **범위 밖:** production DTO/controller/test 수정, legacy/public endpoint 변경.
- **TDD 예외 사유:** 실행 코드를 변경하지 않는 계약 문서 작성 Task다.
- **대체 검증 방법:** 레거시 Kotlin DTO의 생성자 필드와 OpenAPI schema를 대조하고 두 validator와 client generator로
기계 검증한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Create: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Create: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- [x] 캐릭터, 테마·오디오 콘텐츠, 시리즈·연결 콘텐츠 검색, 커뮤니티, FanTalk의 레거시 DTO 전체 필드를 schema로
옮긴다.
- [x] path로 이동한 ID만 body에서 제거하고 나머지 query/body/response 필드명은 레거시와 동일하게 유지한다.
- [x] 레거시 mutation의 `data: null`, 오디오 생성의 `data.contentId`, FanTalk 답변 축약 응답 예외를 operation별로
고정한다.
- [x] FanTalk 관리자 목록과 시리즈 미연결 콘텐츠 검색을 별도 operation으로 포함해 총 23개 endpoint를 검증한다.
- [x] OpenAPI lint/validate, TypeScript Fetch client 생성과 `tsc --noEmit`을 실행하고 결과를 기록한다.
- [x] **Task 3.17: Phase 2 캐릭터 runtime 계약 정합화**
**Goal 실행 `P23-CONTRACT-2`:** 현재 구현된 캐릭터 4개 endpoint를
`api-contract.openapi.json`의 레거시 필드명·전체 request/response·mutation 응답에 맞춘다.
- **시작 조건:** `P23-CONTRACT-1` 완료.
- **완료 증거:** 4개 actual endpoint의 exact JSON field/required/nullable/multipart/`data: null` RED/GREEN,
Phase 2 focused·legacy 회귀와 Progress 기록.
- **범위 안:** `isActive=false`와 다른 optional JSON field의 동시 입력 허용 및 나머지 JSON field 미반영이라는 레거시
request 의미 복구.
- **범위 밖:** 외부 API·ownership·soft delete persistence 결과 변경, 신규 character business behavior.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
- [x] **RED:** 목록 `content`, 상세 전체 nested 필드, 생성 전체 request와 필수 `image`, 수정 optional 필드,
`isActive=false`와 다른 optional JSON field의 동시 입력·미반영, 생성·수정 `data: null` exact JSON 테스트를 작성해
현재 v2 축약/변환 DTO와의 불일치를 확인한다.
- [x] **GREEN:** business pipeline을 바꾸지 않는 최소 DTO/controller/facade/mapper 변경으로 OpenAPI 계약을 통과시킨다.
- [x] **REFACTOR:** Phase 2 actual endpoint와 legacy characterization, 공통 오류 계약을 회귀하고 결과를 기록한다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
- 검증 기록(RED): 무엇: 캐릭터 4개 actual endpoint의 레거시 list/detail/multipart/mutation/soft-delete 계약. 왜: 현재 v2 DTO와
mutation 응답 및 단독 soft-delete 제한이 OpenAPI 원본과 다른지 실제 실패로 고정하기 위해. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를
실행했다. 결과: test compile은 성공했고 59건 중 계약 불일치 9건이 의도한 assertion에서 실패해 `BUILD FAILED`를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: 레거시 DTO 재사용, 필수 생성 image, exact `data: null`, mixed JSON soft-delete와 character/common
회귀. 왜: business pipeline을 유지하면서 runtime 경계만 계약 원본에 맞추고 legacy·인가·오류 회귀를 방지하기 위해. 어떻게:
위 focused 명령을 먼저 실행한 뒤 Task의 character/authorization/error 회귀 명령과 `./gradlew ktlintCheck`를 fresh 실행했다.
결과: focused 59건은 `BUILD SUCCESSFUL in 2m 5s`, 지정 회귀 6개 suite 170건은 failure/error/skipped 0으로
`BUILD SUCCESSFUL in 2m 36s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 48s`였다.
- 검증 기록(독립 리뷰 보완): 무엇: OpenAPI `size` minimum 1과 mixed soft-delete의 미존재 `originalWorkId` 무시 계약. 왜: 독립
코드 리뷰에서 기존 20..50 보정이 Character list 계약과 다르고 soft-delete 검증 경계를 더 직접 고정할 필요가 확인됐기 때문이다.
어떻게: `size=1`에서 두 row 중 `content` 한 건만 반환하는 RED와 `isActive=false` + `originalWorkId=999999` 성공 assertion을
추가했다. 결과: focused 60건 중 pagination 1건이 RED로 실패했고 soft-delete 강화분은 통과했다. size 하한만 1로 바꾼 뒤
focused 60건은 `BUILD SUCCESSFUL in 2m 20s`, 지정 회귀 6개 suite 171건은 failure/error/skipped 0으로
`BUILD SUCCESSFUL in 2m 21s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`였다.
- [x] **Task 3.18: Phase 3 오디오 콘텐츠 runtime 계약 정합화**
**Goal 실행 `P23-CONTRACT-3`:** 현재 구현된 테마·오디오 콘텐츠 5개 endpoint를
`api-contract.openapi.json`의 레거시 필드명·전체 request/response·성공 응답에 맞춘다.
- **시작 조건:** `P23-CONTRACT-2` 완료.
- **완료 증거:** 5개 actual endpoint의 exact query/multipart/JSON schema RED/GREEN, Phase 3 focused·legacy 회귀와
Progress 기록.
- **범위 밖:** upload/processing pipeline, signed URL 정책, series 연결 behavior 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentThemeControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
- [x] **RED:** 테마 `id/theme/image`, 목록 전체 item, 상세 전체 nested DTO, 생성 `contentFile`과
`CreateAudioContentRequest`, 수정 `UpdateCreatorAdminContentRequest` 및 각 성공 `data` 형태를 exact JSON으로 고정한다.
- [x] **GREEN:** 기존 business pipeline과 signed URL 계산을 유지하는 최소 DTO/controller/facade/mapper adapter를 구현한다.
- [x] **REFACTOR:** Phase 3 actual endpoint·legacy characterization·ownership/error 계약을 회귀하고 결과를 기록한다.
- 검증 기록(RED): 무엇: 테마 `id/theme/image`, 목록 `search_word`와 전체 legacy item, 상세 필수 `timezone`과 전체 nested
DTO, 생성 `contentFile`·`CreateAudioContentRequest`·`data.contentId`, 수정 `UpdateCreatorAdminContentRequest`·`data: null`.
왜: 현재 v2 alias와 mutation 상세 응답이 확정 OpenAPI 계약과 다른 상태를 실제 실패로 고정하기 위해. 어떻게: 지정된 theme,
query, controller, create, update 5개 test class를 production 변경 전에 실행했다. 결과: test compile은 성공했고 61건 중
계약 불일치 21건이 의도한 assertion에서 실패해 `BUILD FAILED in 2m 17s`를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: legacy DTO·목록 service 재사용, owner-scoped 상세 mapping, 기존 create/update service
위임, series 연결 보존, ownership·legacy·authorization·error 회귀. 왜: upload/processing·signed URL·ownership 의미는
유지하면서 HTTP 경계만 확정 계약에 맞추기 위해. 어떻게: focused 5개 class를 먼저 실행한 뒤 Task에 명시된 content 전체와
authorization/error 명령 및 `./gradlew ktlintCheck`를 fresh 실행했다. 결과: focused 61건은 `BUILD SUCCESSFUL in 3m 27s`,
최종 10개 suite 206건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 1m 43s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 16s`였다.
- 검증 기록(Important review 보완): 무엇: PUT에 계약 밖 `audioFile` 또는 `contentFile` part가 있으면 400으로 거부하고
AudioContent·SeriesContent, S3, event를 변경하지 않는 계약. 왜: `contentFile`은 controller에 bind되지 않아 create-style part를
보낸 PUT이 파일을 무시한 채 200 `data: null`로 처리됐기 때문이다. 어떻게: 기존 `audioFile` no-side-effect test를 두 part
parameterized test로 확장하고 controller/facade에 optional `contentFile` binding과 공동 guard만 추가했다. 결과: RED는 2건 중
`contentFile` 1건만 실패해 `BUILD FAILED in 30s`, GREEN은 2건 모두 `BUILD SUCCESSFUL in 33s`였다. content·authorization·error
10개 suite 207건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 1m 46s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 32s`였다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 2·3 계약 정합화 Gate
**Goal 실행 `P23-CONTRACT-GATE`:** 문서 계약과 구현된 9개 endpoint의 runtime 응답이 일치하고 Phase 4가 같은 계약을
소비할 수 있는지 판정한다.
- [x] **`P23-CONTRACT-GATE` 완료:** `P23-CONTRACT-1`~`P23-CONTRACT-3` 완료 후 character/content focused·legacy
회귀, OpenAPI validate/client 생성과 `ktlintCheck`를 fresh 실행한다.
- **범위 밖:** Gate에서 직접 production code 수정, Phase 4 이후 기능 구현.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
npx --yes @openapitools/openapi-generator-cli validate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @openapitools/openapi-generator-cli generate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json \
-g typescript-fetch \
-o /tmp/ai-character-admin-typescript-client
```
- 검증 기록: 무엇: `P23-CONTRACT-GATE` runtime/API 계약 Gate. 왜: `P23-CONTRACT-1`~`P23-CONTRACT-3` 완료 후 Phase 4가
소비할 Character·AudioContent 9개 endpoint의 runtime 응답과 OpenAPI 계약이 함께 유효한지 확인하기 위해. 어떻게: 위 네
Gate 명령을 fresh 실행했다. 결과: character/content/common 회귀는 `BUILD SUCCESSFUL in 4m 59s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 45s`, OpenAPI validate는 `No validation issues detected.`, TypeScript Fetch client 생성은
`/tmp/ai-character-admin-typescript-client`에 성공했다.
---
### Phase 4: 시리즈 관리 vertical slice
#### 목표
선택한 AI 캐릭터 소유 시리즈 CRUD, soft delete, 콘텐츠 연결/해제/검색/순서 관리를 owner-safe v2 경로로 제공한다.
#### 범위와 비범위
- 포함: 시리즈 목록/상세/생성/수정/soft delete, 콘텐츠 연결/해제, 시리즈 콘텐츠 조회/검색, owner-scoped 순서 변경, 기존 creator series behavior parity 특성화.
- 제외: 기존 `CreatorAdminContentSeriesController.updateSeriesOrders(ids)` 계약 변경.
#### 선행 Phase 및 의존성
- Phase 1 target resolver.
- Phase 3 content owner query를 재사용할 수 있다.
- 기존 creator series 생성/수정/soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior를 통과하는 특성화 테스트로 먼저 고정해야 한다.
#### API endpoint와 request/response contract
- 정식 전체 schema는 `api-contract.openapi.json`의 Series operation 10개를 따른다.
- `GET /api/v2/admin/ai-characters/series-genres`는 활성 장르의 `id`, `genre`, `isAdult` 직접 배열을 반환한다.
- `GET /series/{seriesId}`의 `data`는 목록 `items`의 단일 객체와 동일한 11개 필드·타입을 반환한다.
- `GET/POST/PUT /api/v2/admin/ai-characters/{characterId}/series...`
- `GET /series/{seriesId}/contents` query: `page`, `size`; response:
`GetCreatorAdminContentSeriesContentResponse(totalCount, items)`
- `GET /series/{seriesId}/contents/search` query: 필수 `search_word`; response:
`List<SearchContentNotInSeriesResponse>`
- `POST /series/{seriesId}/contents` request: `AddingContentToTheSeriesRequest(contentIdList: List<Long>)`
- `DELETE /series/{seriesId}/contents/{contentId}` request body 없음
- `PUT /series/orders` request: `UpdateOrdersRequest(ids: List<Long>)`
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: `Series`와 `SeriesContent` owner-scoped query/update adapter 추가.
- Service: 신규 series application service에서 기존 CRUD 핵심을 테스트 후 재사용하되, 모든 write 전에 series/content owner를 검증한다.
#### DB migration
- 없음.
#### transaction과 concurrency 고려사항
- 순서 변경은 동일 owner의 모든 series ID를 한 transaction에서 검증 후 갱신한다.
- 콘텐츠 연결/해제는 series와 content owner를 모두 검증한 뒤 수행한다.
- 동시에 순서 변경 요청이 들어오면 마지막 transaction 결과가 반영되는 기존 단순 정책을 유지하되 cross-owner 갱신은 절대 허용하지 않는다.
#### 보안 및 개인정보 위험
- ID-only order update로 다른 creator series를 변경하지 못해야 한다.
- 연결 가능한 content 검색은 target owner 범위로 제한한다.
#### acceptance criteria
- 기존 creator series의 CRUD, soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior parity가 특성화 테스트로 고정된다.
- 모든 시리즈/콘텐츠 ID는 target creatorMember 소유일 때만 변경된다.
- soft delete는 `isActive=false`이며 활성 조회에서 제외된다.
- 기존 owner-less order update 취약 경로가 신규 v2 API에는 없다.
#### targeted test
- Characterization: `LegacyCreatorAdminSeriesCharacterizationTest`.
- V2 RED/GREEN: `AiCharacterAdminSeriesServiceTest`.
- Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`
#### 전체 회귀 테스트 영향
- 기존 creator-admin series endpoint 계약은 유지한다.
- 기존 series query/order 테스트가 있으면 함께 실행한다.
#### rollback 전략
- 신규 series v2 admin route/facade를 제거한다.
- 기존 data model 변경이 없으므로 schema rollback은 없다.
#### 권장 commit 경계
- `feat: add ai character admin series slice`
- [x] **Task 4.1: 기존 series parity 특성화 baseline 고정**
**Goal 실행 `P4-T1`:** 기존 creator series의 CRUD·연결·검색·순서 동작을 신규 v2 구현의 비교 기준으로 고정한다.
- **시작 조건:** Phase 2·3 runtime 계약 정합화의 `P23-CONTRACT-GATE` 완료와 사용자 진행 지시.
- **완료 증거:** production 변경 전 특성화 테스트 통과, 관찰된 오류·side-effect 정책과 Progress 기록.
- **범위 밖:** 신규 v2 series production code 구현.
**Files:**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/LegacyCreatorAdminSeriesCharacterizationTest.kt`
- [x] 목록·상세·생성·수정·soft delete와 inactive 조회 baseline test를 작성한다.
- [x] 콘텐츠 연결·해제·검색과 순서 변경의 결과·검증·side effect를 고정한다.
- [x] Phase 4 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
- [x] production code 변경 없이 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- [x] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- **관찰된 legacy baseline:** creator 목록은 owner의 활성 시리즈만 `orders` 오름차순으로 반환하지만 owner 상세는
`isActive=false`도 반환하고, 전역 admin의 `findByIdAndActiveTrue`는 inactive를 제외한다. 생성은 DB save → S3 upload →
language detect event 순이며 키워드를 `#` prefix 기준으로 중복 제거한다. 수정은 일반 field와 `isActive=false`를 한 요청에서
모두 반영하고 title/introduction 변경 시 translation event를 발행한다.
- **관찰된 연결·검색·순서 baseline:** 콘텐츠 연결은 owned ID만 부분 반영하고 foreign/missing ID를 건너뛰며 전부 무효일 때만
`creator.admin.series.no_content_added`를 던진다. 해제할 link가 없으면 no-op이다. 연결 목록 `totalCount`는 target series가 아닌
owner의 활성 시리즈 전체 link 수이고, missing series 조회도 이 count와 빈 items를 반환한다. 미연결 검색은 processed 또는
reserved owner content만 반환하지만 series 존재·owner를 검증하지 않는다. 순서 변경은 owner·active를 검증하지 않고 존재하는
ID만 요청 index + 1로 갱신하므로 foreign/inactive도 변경되고 missing ID 자리는 순번 gap으로 남는다.
- **legacy 오류 표면:** creator-admin security 실패는 401/403 `sendError`이고, controller 이후 `SodaException`은 HTTP 200
`ApiResponse.error`로 노출된다. 아래 Phase 4 v2 결정은 이를 복제하지 않고 신규 prefix의 비2xx envelope 정책을 따른다.
- **Phase 4 v2 domain/client 오류 결정:** target/series/content 미존재·inactive·cross-owner, 존재하지 않는 양수 genre,
잘못된 pagination, malformed request, 중복·누락·foreign/inactive order ID, 이미 연결된 content와 없는 link 해제는 mutation 전
400 `common.error.invalid_request`로 실패하고 DB/S3/event side effect는 0건이어야 한다. 빈 `contentIdList` 또는 legacy 규칙상
추가 가능한 ID가 0개인 비소유권 입력은 400 `creator.admin.series.no_content_added`를 유지한다. create/update의 legacy 입력
validation key도 아래 표처럼 400으로 유지한다. 예상하지 못한 DB/S3/event 오류는 500 `common.error.unknown`을 사용하며,
transaction DB 변경과 미발행 event는 rollback하지만 이미 성공한 S3 upload는 legacy에 삭제 계약이 없어 보상하지 않는다.
| Phase 4 domain/client 경우 | status | message key | KO | EN | JA |
|---|---:|---|---|---|---|
| missing/inactive/cross-owner resource, pagination·binding·order/link 검증 실패 | 400 | `common.error.invalid_request` | 잘못된 요청입니다. | Invalid request. | 無効なリクエストです。 |
| 생성 title 공백 | 400 | `creator.admin.series.title_required` | 시리즈 제목을 입력하세요 | Please enter a series title. | シリーズのタイトルを入力してください。 |
| 생성 introduction 공백 | 400 | `creator.admin.series.introduction_required` | 시리즈 소개를 입력하세요 | Please enter a series introduction. | シリーズ紹介を入力してください。 |
| 생성 keyword 공백 | 400 | `creator.admin.series.keyword_required` | 시리즈를 설명할 수 있는 키워드를 입력하세요 | Please enter keywords that describe the series. | シリーズを説明できるキーワードを入力してください。 |
| 생성 genre ID 0 이하 | 400 | `creator.admin.series.genre_required` | 올바른 장르를 선택하세요 | Please select a valid genre. | 正しいジャンルを選択してください。 |
| 생성 published days 비어 있음 | 400 | `creator.admin.series.published_days_required` | 시리즈 연재요일을 선택하세요 | Please select publishing days. | シリーズの連載曜日を選択してください。 |
| `RANDOM`과 특정 요일 혼합 | 400 | `creator.admin.series.published_days_random_exclusive` | 랜덤과 연재요일 동시에 선택할 수 없습니다. | You cannot select random and specific days at the same time. | ランダムと連載曜日を同時に選択することはできません。 |
| 생성 cover image 누락 | 400 | `creator.admin.series.cover_image_required` | 커버이미지를 선택해 주세요. | Please select a cover image. | カバー画像を選択してください。 |
| 수정 field와 image 모두 없음 | 400 | `creator.admin.series.no_changes` | 변경사항이 없습니다. | No changes to update. | 変更データがありません。 |
| 추가 가능한 content ID 0개 | 400 | `creator.admin.series.no_content_added` | 추가된 콘텐츠가 없습니다. | No content was added. | 追加されたコンテンツがありません。 |
| 예상하지 못한 server/infrastructure 오류 | 500 | `common.error.unknown` | 알 수 없는 오류가 발생했습니다. 다시 시도해 주세요. | An unknown error occurred. try again. | 不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。 |
- 검증 기록: 무엇: creator-admin series CRUD/list/inactive, content link/unlink/count/search, owner-less order와 legacy 오류 key
characterization. 왜: 신규 v2가 legacy JSON·domain 의미를 재사용하되 legacy의 owner-less·부분 성공·HTTP 200 오류 표면은
안전한 비2xx owner-first 계약으로 분리하기 위해. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.LegacyCreatorAdminSeriesCharacterizationTest`와
`./gradlew ktlintCheck`를 production 변경 없이 실행했다. 결과: focused test는 첫 실행부터 `BUILD SUCCESSFUL in 47s`,
`ktlintCheck`는 `BUILD SUCCESSFUL in 19s`였다. 전체 `./gradlew test`는 production 변경이 없고 지정 focused test가 실제
service/repository/S3/event 경계를 포함하므로 실행하지 않았다.
- [x] **Task 4.2: 시리즈 목록·상세 조회 구현**
**Goal 실행 `P4-T2`:** target owner의 활성 시리즈 목록·상세를 레거시 response 계약으로 제공한다.
- **시작 조건:** `P4-T1` 완료와 Phase 4 오류 계약의 계획 반영.
- **완료 증거:** 목록·상세 전체 필드, inactive·cross-owner·pagination RED/GREEN과 Progress 기록.
- **범위 밖:** 시리즈 mutation, 콘텐츠 연결, 순서 변경.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesQueryTest.kt`
- [x] 미구현 목록·상세, inactive 제외, cross-owner 거부와 pagination 경계 실패 test를 작성한다.
- [x] owner-scoped 목록·상세 최소 구현으로 focused test를 통과시킨다.
- [x] 목록 `totalCount/items`와 item 전체 필드, 상세의 문자열 `publishedDaysOfWeek/genre/keywords/state`를
`api-contract.openapi.json`과 exact JSON으로 검증한다.
- [x] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: v2 series 목록·상세 전체 legacy 필드, 활성 owner 범위, missing/inactive/cross-owner 상세와
`page=1&size=1`, 음수 page·0 size 경계. 왜: 신규 route 미구현과 Phase 4 오류 결정을 실제 HTTP 계약 실패로 고정하기 위해.
어떻게: production 파일 생성 전에
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesQueryTest`를 실행했다.
결과: test compile은 성공했고 5건 모두 기대 status 200/400 대신 미구현 404로 실패해 `BUILD FAILED in 44s`를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: target resolver 선행, 활성 owner 목록·상세, exact legacy DTO, pagination과 legacy
characterization 회귀. 왜: 기존 creator series 동작과 응답 형태는 재사용하면서 inactive·cross-owner 상세만 신규 400 계약으로
제한하기 위해. 어떻게: 같은 focused 명령, series package 회귀와 `./gradlew ktlintCheck`를 실행했다. 결과: focused 5건은
failure/error/skipped 0으로 `BUILD SUCCESSFUL in 34s`, Task 4.1 포함 series 12건은 failure/error/skipped 0으로
`BUILD SUCCESSFUL in 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였다.
- 검증 기록(독립 리뷰 보완): 무엇: inactive `ChatCharacter` target의 목록·상세 400 계약. 왜: resolver는 role/memberKind만
검증하므로 Phase 4의 target inactive 결정이 series facade에서 누락됐기 때문이다. 어떻게: inactive target 목록·상세 테스트를
추가해 focused 명령을 실행한 뒤 facade의 공통 active target guard를 적용하고 focused/series 회귀와 `ktlintCheck`를 재실행했다.
결과: 보완 RED는 7건 중 기존 5건은 통과하고 신규 2건만 400 기대 대비 200으로 실패해 `BUILD FAILED in 35s`였다. 보완 후
focused 7건은 `BUILD SUCCESSFUL in 44s`, Task 4.1 포함 series 14건은 `BUILD SUCCESSFUL in 49s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 23s`였다.
- [x] **Task 4.3: 시리즈 생성·수정·soft delete 구현**
**Goal 실행 `P4-T3`:** target owner의 시리즈 생성·수정·soft delete를 기존 creator parity로 제공한다.
- **시작 조건:** `P4-T1`, `P4-T2` 완료.
- **완료 증거:** CRUD RED/GREEN, ownership·inactive·no-side-effect와 legacy 회귀 기록.
- **범위 밖:** 콘텐츠 연결·해제, 순서 변경, hard delete.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- [x] 생성·수정·soft delete와 cross-owner mutation 실패 test를 작성한다.
- [x] owner 검증 후 최소 CRUD 구현으로 test를 통과시킨다.
- [x] `isActive=false`와 활성 조회 제외, invalid target의 DB/event no-side-effect를 검증한다.
- [x] focused/legacy test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: v2 series 생성·수정·DELETE soft delete, cross-owner·missing·inactive·no-change와
DB/S3/event 부작용 0건. 왜: mutation route 미구현과 owner-first 계약을 실제 HTTP 경계로 고정하기 위해. 어떻게: production
변경 전에 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest`를
실행했다. 결과: 12건 모두 기대 200/400 대신 미구현 method의 405로 실패해 `BUILD FAILED in 51s`를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: legacy 생성·수정 위임, path `seriesId` 조립, 활성 target/owned series 선검증과 null 성공
envelope. 왜: keyword/S3/genre/event/entity 갱신을 복제하지 않고 creator parity를 유지하기 위해. 어떻게: focused test,
`./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`, `./gradlew ktlintCheck`와
`git diff --check`를 실행했다. 결과: focused 12건과 series 회귀 26건은 failure/error/skipped 0, fresh 회귀는
`BUILD SUCCESSFUL in 4m 36s`, ktlint는 import 정렬 1건 수정 후 `BUILD SUCCESSFUL in 21s`, diff check는 오류가 없었다.
- 검증 기록(독립 리뷰 보완): 무엇: 존재하지 않는 양수 `genreId` 생성·이미지 포함 수정 요청을 legacy 호출 전에 400으로 차단하고
DB/S3/event 부작용 0건을 보장했다. 왜: legacy 수정 path는 이미지 업로드 후 genre를 조회하므로 Phase 4의 mutation 전 검증
결정을 위반할 수 있었기 때문이다. 어떻게: 누락 genre 생성·수정 테스트 2건을 추가해 focused 14건 중 신규 2건만 RED로 실패함을
확인한 뒤 active genre 사전 guard를 추가했다. 결과: focused 14건은 `BUILD SUCCESSFUL in 35s`, series 회귀 28건은
`BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 49s`, `git diff --check`는 오류가 없었다.
- [x] **Task 4.4: 시리즈 콘텐츠 조회·검색·연결·해제 구현**
**Goal 실행 `P4-T4`:** 동일 owner의 시리즈와 콘텐츠만 검색·연결·해제할 수 있도록 한다.
- **시작 조건:** `P4-T2`, `P4-T3`와 Phase 3 owner query 계약 완료.
- **완료 증거:** 연결 목록·미연결 검색의 분리된 응답, pagination·전체 ID 사전 검증·원자적 연결/해제 RED/GREEN과
Progress 기록.
- **범위 밖:** 콘텐츠 자체 수정, 시리즈 순서 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt`
- [x] `GET .../contents`와 `GET .../contents/search?search_word=...`의 별도 응답, 연결·해제와 cross-owner ID 실패
test를 작성한다.
- [x] 모든 series/content ID를 mutation 전에 검증하는 최소 구현을 통과시킨다.
- [x] `contentIdList` 필드와 mutation `data: null`, 일부 연결 성공이 남지 않는 원자성과 pagination 경계를 검증한다.
- [x] focused/Phase 3 owner query 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 연결 목록·미연결 검색·pagination·원자적 연결/해제와 invalid series/content 경계. 왜: 신규 v2 route와
legacy 부분 성공/no-op을 owner-first 400 계약으로 바꾸기 위해. 어떻게: production 변경 전에
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContentTest`를 실행했다.
결과: 7건 모두 미구현 route의 404/405로 기대한 200/400을 충족하지 못해 `BUILD FAILED in 54s`를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: legacy 목록/검색 DTO 재사용, `contentIdList` 전체 선검증, 연결/해제 `data: null`과 invalid
요청의 무변경 경계. 왜: legacy의 owner 전체 link count와 응답 형태는 유지하면서 foreign/missing/inactive/already-linked ID와
없는 link 해제의 부분 성공을 막기 위해. 어떻게: focused, Phase 4 series 회귀, Phase 3 content owner-query 회귀와
`ktlintCheck`를 실행했다. 결과: focused 7건은 `BUILD SUCCESSFUL in 1m 23s`, series 회귀는
`BUILD SUCCESSFUL in 1m 55s`, content 회귀는 `BUILD SUCCESSFUL in 1m 24s`, ktlint는 `BUILD SUCCESSFUL in 36s`였다.
- [x] **Task 4.5: owner-scoped 시리즈 순서 변경 구현**
**Goal 실행 `P4-T5`:** 요청된 모든 series ID의 owner를 먼저 검증한 뒤 한 transaction에서 순서를 변경한다.
- **시작 조건:** `P4-T3` 완료.
- **완료 증거:** 정상·중복/누락·cross-owner·동시 요청 RED/GREEN과 owner-less 경로 비사용 증거.
- **범위 밖:** 기존 `CreatorAdminContentSeriesController.updateSeriesOrders(ids)` 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesOrderTest.kt`
- [x] 정상 순서와 cross-owner ID-only 취약 경로를 재현하는 실패 test를 작성한다.
- [x] 동일 owner 전체 검증 후 한 transaction에서 갱신하는 최소 구현을 통과시킨다.
- [x] 검증 실패 시 update 0건과 동시 요청의 기존 last-transaction 정책을 확인한다.
- [x] focused/legacy order test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(GREEN/REFACTOR): 무엇: v2 series order endpoint와 owner active series 선검증, duplicate/missing/cross-owner/inactive/empty
ID 거부, 동시 순서 변경용 ID 오름차순 pessimistic lock, last-request 결과를 고정했다. 왜: legacy `updateSeriesOrders(ids)`는
owner-less로 요청 순서대로 row를 수정하므로 신규 v2 경계에서 owner 검증과 lock 순서를 먼저 보장해야 하기 때문이다. 어떻게:
`AiCharacterAdminSeriesOrderTest` RED 후 `PUT /api/v2/admin/ai-characters/{characterId}/series/orders`를 추가하고,
`CreatorAdminContentSeriesRepository.findActiveByCreatorIdAndIdInForUpdate`로 같은 transaction 안에서 대상 row를 선잠금한 뒤
legacy update를 재사용했다. 결과: focused order test는 `BUILD SUCCESSFUL in 34s`, Phase 4 series 회귀는
`BUILD SUCCESSFUL in 55s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. 리뷰 재확인에서
blocking/important/minor finding 0건을 확인했다.
- [x] **Task 4.6: Phase 4 보안·오류·회귀 검증**
**Goal 실행 `P4-T6`:** 모든 series endpoint의 ADMIN·오류·ownership 계약과 legacy 회귀를 고정한다.
- **시작 조건:** `P4-T2`~`P4-T5` 완료.
- **완료 증거:** endpoint 권한 매트릭스, domain 오류 status/key/KO·EN·JA, legacy 회귀와 Progress 기록.
- **범위 밖:** Phase 5 community 기능.
**Files:**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContractTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- [x] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다.
- [x] target/series/content/pagination 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
- [x] invalid ownership의 DB/event side effect 0건과 legacy creator series 계약을 검증한다.
- [x] Phase 4 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
#### Phase 4 Gate
**Goal 실행 `P4-GATE`:** Phase 4 series 사용자 흐름과 ownership·회귀 품질을 최종 판정한다.
- [x] **`P4-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P4-T1`~`P4-T6` 완료.
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`와
`./gradlew ktlintCheck` 성공, Progress 기록.
- **범위 밖:** 실패와 무관한 Phase 5 구현.
#### Phase 4 후속 리뷰 보완
- [x] **Task 4.7: 시리즈 HTTP 경계를 OpenAPI 9개 operation과 정합화**
**Goal 실행 `P4-R1`:** `REV-023`의 계약 밖 시리즈 DELETE endpoint를 제거하고 `REV-024`의 JSON body 두 곳에서
`additionalProperties: false`를 실제로 강제한다.
- **추적 review ID:** `REV-023`, `REV-024`.
- **시작 조건:** `P3-R9-GATE` 완료와 `phase4-series-review.md` 판정 존재.
- **완료 증거:** `DELETE /series/{seriesId}`가 405이고 `PUT /series/{seriesId}`의 `isActive=false`가 soft delete를
담당하는 actual endpoint 테스트, 콘텐츠 추가·순서 변경 request의 미지 필드 400/no-side-effect 테스트,
Series 9개 operation mapping 정적 대조와 Progress 기록.
- **범위 밖:** OpenAPI operation 추가, legacy controller 변경, 시리즈 CRUD/ownership/lock 정책 변경, 다른 Phase JSON 경계.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesOrderTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** `DELETE /series/{seriesId}` 405와 `PUT` `isActive=false` soft delete 성공을 actual endpoint로 고정한다.
- [x] **RED:** 콘텐츠 추가와 순서 변경 JSON에 계약 밖 필드를 추가하면 400
`common.error.invalid_request`이고 DB/event 부작용이 0회임을 확인한다.
- [x] **GREEN:** 계약 밖 DELETE controller/facade 경로를 제거하고 두 JSON body만 strict reader로 역직렬화한다.
- [x] **REFACTOR:** OpenAPI Series operation 9개와 controller mapping을 대조하고 series/common 영향 범위 회귀,
`ktlintCheck`, diff check를 실행한다.
- 검증 기록(RED): 무엇: 계약 밖 `DELETE /series/{seriesId}` 제거 기대와 `PUT isActive=false` soft delete, 콘텐츠 추가·순서 변경 미지 필드 거부를 actual endpoint로 고정했다. 왜: `REV-023`~`REV-024`가 실제 실패를 내는지 확인하기 위해. 어떻게: 아래 focused series mutation/content/order 명령을 production 변경 전 실행했다. 결과: 28개 중 6개가 기존 DELETE 200 또는 unknown-field 성공 때문에 실패해 RED를 확인했다.
- 검증 기록(GREEN): 무엇: `DELETE /series/{seriesId}` controller/facade 경로를 제거하고 content add/order JSON body를 facade strict reader로 파싱했다. 왜: OpenAPI 9개 operation과 `additionalProperties: false` 계약을 runtime에 맞추기 위해. 어떻게: 같은 focused 명령을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 25s`였다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContentTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesOrderTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 후속 리뷰 Gate
**Goal 실행 `P4-R1-GATE`:** `REV-023`~`REV-024` 수정 뒤 Series controller가 OpenAPI 9개 operation과 일치하는지
재검토한다.
- [x] **`P4-R1-GATE` 완료:** `P4-R1` 완료 후 mapping 정적 대조와 series/common 회귀, lint·diff를 fresh 실행하고
리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P4-R1` 완료.
- **완료 증거:** 계약 밖 DELETE 제거, 두 JSON body 미지 필드 거부, 9개 operation 일치, 영향 범위 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
- 검증 기록: 무엇: `P4-R1-GATE`에서 Series controller mapping과 회귀 품질을 재판정했다. 왜: `REV-023`~`REV-024` 처리 후 OpenAPI 9개 operation과 runtime JSON 경계가 일치하는지 확인하기 위해. 어떻게: `rg -n "@(Get|Post|Put|Delete)Mapping|fun delete\(|facade\.delete\(|@RequestBody request:" "src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series"`로 9개 mapping과 두 `@RequestBody String`, 삭제 facade 부재를 확인했고, series/common 회귀와 `ktlintCheck`, `git diff --check`를 fresh 실행했다. 결과: 정적 대조는 9개 mapping만 출력했고 `DELETE /series/{seriesId}`와 `facade.delete`는 출력되지 않았다. 회귀 명령은 `BUILD SUCCESSFUL in 2m 19s`, 최초 `ktlintCheck`는 blank line 1건으로 실패했으나 포맷 수정 후 재실행은 `BUILD SUCCESSFUL in 33s`, `git diff --check`는 출력이 없었다.
#### Phase 4 2차 후속 리뷰 보완
- [x] **Task 4.8: 시리즈 필수 이미지와 연결 해제 경계 복구**
**Goal 실행 `P4-R2`:** `REV-031`의 soft-delete 콘텐츠 연결 해제를 복구하고, `REV-032`의 생성 필수 `image`
part를 공통 multipart binding 계약에 맞춘다.
- **추적 review ID:** `REV-031`, `REV-032`.
- **시작 조건:** `P4-R1-GATE` 완료와 `phase4-series-review.md` 2차 리뷰 판정 존재.
- **완료 증거:** 연결 후 soft delete된 owner 콘텐츠 해제 성공, cross-owner/missing link 무변경,
생성 `image` 누락의 exact `MissingServletRequestPartException`·KO/EN/JA 400/no-side-effect RED/GREEN,
series/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 콘텐츠 연결 추가·검색의 active/duration 적격성 변경, 빈 `image` 파일 정책 신설,
legacy/public series controller 변경, OpenAPI schema 변경.
- **계약 판정:** PRD API Expectations와 OpenAPI `SeriesCreateMultipart.required`를 우선한다. 기존 Phase 4 오류 표의
`creator.admin.series.cover_image_required`는 nullable legacy 전달을 기록한 과거 결정이며,
신규 관리자 endpoint의 누락 part는 `common.error.invalid_request`로 정정한다.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** owner 콘텐츠를 시리즈에 연결한 뒤 `isActive=false`, `releaseDate=null`로 soft delete하고
`DELETE .../contents/{contentId}`가 현재 400으로 실패하며 link가 남는지 확인한다.
- [x] **RED:** 생성 `image` part 누락을 KO/EN/JA actual endpoint로 보내 exact
`MissingServletRequestPartException`, 400 `common.error.invalid_request`, facade/DB/S3/event 0회를 단언한다.
- [x] **GREEN:** 해제는 실제 series link와 그 콘텐츠 owner만 검증하고, 연결 추가에만 필요한
active/release/duration 적격성 검사를 해제 경로에서 제거한다.
- [x] **GREEN:** 생성 controller/facade의 `image`를 non-null `MultipartFile`로 바꾸고 검증된 파일을 legacy service에
그대로 전달한다. update의 optional `image`는 유지한다.
- [x] **REFACTOR:** 정상 연결/해제, missing/cross-owner link, 생성 validation key와 series/common 영향 범위 회귀,
`ktlintCheck`, `git diff --check`를 실행해 Progress에 기록한다.
- 검증 기록(RED): 무엇: soft-delete된 owner content의 기존 series link 해제와 생성 `image` 누락의 공통 binding 오류를 actual endpoint로 고정했다. 왜: `REV-031`~`REV-032`가 실제 runtime에서 실패하는지 확인하기 위해. 어떻게: 아래 focused mutation/content 명령을 production 변경 전 실행했다. 결과: soft-delete unlink 1건은 400, missing image KO/EN/JA 3건은 legacy message 기대 차이로 실패해 RED를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: unlink 경로에서 추가 적격성 guard를 제거하고 실제 owner link만 검증했으며, create `image` part를 non-null binding으로 변경했다. 왜: 연결 추가 조건과 기존 link 해제 조건을 분리하고 OpenAPI required part 계약을 MVC binding 단계에서 강제하기 위해. 어떻게: focused 명령, series/common 영향 범위 회귀, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: focused는 `BUILD SUCCESSFUL`, 영향 범위 회귀는 `BUILD SUCCESSFUL`, `ktlintCheck`는 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 2차 후속 리뷰 Gate
**Goal 실행 `P4-R2-GATE`:** `REV-031`~`REV-032`의 unlink 상태 전이와 multipart 필수 part 계약을 재검토한다.
- [x] **`P4-R2-GATE` 완료:** `P4-R2` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P4-R2` 완료.
- **완료 증거:** 두 review ID 처리 완료, soft-delete 콘텐츠 unlink 성공, 필수 image 누락 exact 400,
기존 연결 추가·update optional image 계약 유지.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록: 무엇: `P4-R2-GATE`에서 `REV-031`~`REV-032` 처리 결과를 재판정했다. 왜: soft-delete linked content 해제, 필수 create image 누락, 기존 link 오류와 optional update image 계약이 동시에 유지되는지 확인하기 위해. 어떻게: focused mutation/content, series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다. 결과: 모든 Gradle 명령이 `BUILD SUCCESSFUL`이고 diff check 출력이 없어 Phase 4 후속 리뷰를 완료로 판정했다.
#### Phase 4 3차 리뷰 보완
- [x] **Task 4.9: 시리즈 빈 image의 0-byte 업로드 차단**
**Goal 실행 `P4-R3`:** 시리즈 생성의 빈 필수 `image`를 부작용 전에 거부하고, 수정의 빈 optional `image`는 생략으로
정규화해 기존 커버를 유지한다.
- **추적 review ID:** `REV-037`.
- **시작 조건:** `P3-R11-GATE` 완료와 `phase4-series-review.md` 3차 정적 리뷰 판정 존재.
- **완료 증거:** 생성 빈 image 400/no-side-effect, 수정 JSON+빈 image의 기존 커버 유지/S3 0회 actual endpoint
RED/GREEN, series/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** OpenAPI schema 변경, 레거시 series service 변경, 정상 image 업로드 경로·파일 정책 확장.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 빈 필수 image로 시리즈를 생성하면 현재 0-byte S3 upload와 DB/event 부작용이 발생하는지 actual
endpoint로 고정한다.
- [x] **RED:** 유효한 수정 JSON과 빈 optional image를 함께 보내면 현재 0-byte cover로 교체되는지 고정한다.
- [x] **GREEN:** create facade에서 `image.isEmpty`를 legacy 호출 전에 400 `common.error.invalid_request`로 거부하고,
update의 빈 image는 null로 정규화해 legacy service에 전달한다.
- [x] **CONTRACT TEST:** 빈 image만 있고 JSON 변경 필드가 없는 update는 기존 `no_changes` 400을 유지하며, 정상
image 생성·교체와 image 생략 수정은 그대로 동작하는지 확인한다.
- [x] **REFACTOR:** series package와 공통 authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해
Progress와 리뷰 문서에 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 3차 리뷰 Gate
**Goal 실행 `P4-R3-GATE`:** `REV-037`의 생성·수정 empty-file 정책과 기존 정상 upload 계약을 재검토한다.
- [x] **`P4-R3-GATE` 완료:** `P4-R3` 완료 후 위 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P4-R3` 완료.
- **완료 증거:** review ID 처리 완료, 생성 빈 image 400/no-side-effect, 수정 빈 image 생략, 정상 upload 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록(RED): 무엇: 시리즈 생성·수정 empty image. 왜: empty multipart가 legacy service로 전달되어 0-byte S3/cover 변경을 유발하는지 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest`를 실행했다. 결과: 신규 3건이 실패해 생성 400 미충족, 수정 cover 유지 미충족, empty-only no_changes 미충족을 확인했다.
- 검증 기록(GREEN/GATE): 무엇: 생성 empty image 400/no-side-effect, 수정 empty image 생략, empty-only no_changes 유지. 왜: legacy service 공용 동작 변경 없이 v2 facade 경계만 보정하기 위해. 어떻게: 같은 focused 명령 재실행 후 targeted/전체/lint/OpenAPI/mapping/diff 검증을 실행했다. 결과: focused series mutation과 전체 검증이 모두 성공했다.
#### Phase 4 5차 리뷰 보완
- [x] **Task 4.10: 시리즈 생성 primitive 필드의 명시적 null 거부**
**Goal 실행 `P4-R4`:** 시리즈 생성의 non-null primitive `genreId`, `isAdult`에 명시적 null이 들어오면 JVM 기본값으로
보정하지 않고 S3·DB·event 전에 400으로 거부하며, 필드 생략 시 기존 기본값은 유지한다.
- **추적 review ID:** `REV-042`.
- **시작 조건:** `P3-R12-GATE` 완료와 `phase4-series-review.md` 5차 정적 리뷰 판정 존재.
- **완료 증거:** 두 필드의 explicit null actual endpoint RED/GREEN/no-side-effect, 생략 기본값·정상 생성 회귀,
series/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/service 변경, OpenAPI schema·기본값 변경, genre 유효성 정책 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** `genreId: null`, `isAdult: null`이 현재 각각 primitive 기본값으로 역직렬화되는 경로를 actual endpoint로
고정하고 S3·DB·event 결과를 단언한다.
- [x] **GREEN:** v2 생성 경계에서 두 non-null primitive의 명시적 null을 `common.error.invalid_request` 400으로
변환한다.
- [x] **CONTRACT TEST:** 두 필드 생략 시 `genreId=0`, `isAdult=false` 기본값과 정상 image 생성, 기존 미지 필드·빈 image
검증을 유지한다.
- [x] **REFACTOR:** strict parse 결과를 활용한 v2 전용 최소 검증으로 제한하고 series/common 영향 범위 회귀,
`ktlintCheck`, `git diff --check`를 실행해 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 5차 리뷰 Gate
**Goal 실행 `P4-R4-GATE`:** `REV-042`의 시리즈 생성 primitive nullability와 기본값·부작용 경계를 재검토한다.
- [x] **`P4-R4-GATE` 완료:** `P4-R4` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P4-R4` 완료.
- **완료 증거:** review ID 처리 완료, explicit null 400/no-side-effect, 생략 기본값과 정상 생성 회귀 성공.
- **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경.
- 검증 기록: 무엇: `REV-042`의 시리즈 생성 primitive explicit null 경계를 처리했다. 왜: OpenAPI non-null primitive가
Jackson 기본값으로 보정되어 S3·DB·event mutation으로 이어질 수 있기 때문이다. 어떻게: actual multipart POST RED/GREEN,
series/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행하고 리뷰 문서를 갱신했다. 결과: `genreId:null`,
`isAdult:null`은 400/no-side-effect로 고정됐고 생략 기본값·정상 생성 회귀는 유지됐다.
#### Phase 4 후속 기능 보완
- [x] **Task 4.11: 시리즈 등록용 장르 목록**
**Goal 실행 `P4-R5`:** AI 캐릭터 시리즈 등록 화면에서 활성 장르를 `orders` 오름차순으로 조회하고
`id`, `genre`, `isAdult`의 직접 배열로 반환한다.
- **추적 review ID:** `REV-046`.
- **시작 조건:** `P3-R13-GATE` 완료와 PRD·OpenAPI의 승인된 장르 목록 계약 존재.
- **완료 증거:** 활성 장르만 정렬된 exact response, 빈 목록, ADMIN 공통 경계와 Series/common 회귀,
OpenAPI `implemented`, Progress 기록.
- **범위 밖:** 장르 CRUD·순서 수정, character별 장르 제한, pagination, 레거시 장르 endpoint 변경.
**Interfaces:**
- `GET /api/v2/admin/ai-characters/series-genres`
- Produces: `ApiResponse<List<GetSeriesGenreListResponse>>`.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesReferenceController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesGenreTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreService.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreRepository.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** active/inactive와 서로 다른 `orders`를 가진 장르로 exact array·정렬·빈 목록을 actual GET에 고정한다.
- [x] **GREEN:** `AdminContentSeriesGenreService.getSeriesGenreList`를 그대로 재사용하고 별도 query·DTO·pagination을
추가하지 않는다.
- [x] **CONTRACT TEST:** 정적 `/series-genres`가 character/series 동적 route와 충돌하지 않고 ADMIN 이중 인가와
오류 envelope를 유지하는지 확인한다.
- [x] **REFACTOR:** 조회 controller와 facade method만 추가하고 Series/common 영향 범위 회귀, `ktlintCheck`,
OpenAPI 상태, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesGenreTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 장르 목록 Gate
**Goal 실행 `P4-R5-GATE`:** `REV-046`의 활성 장르·정렬·직접 배열 계약을 재검토한다.
- [x] **`P4-R5-GATE` 완료:** `P4-R5` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 4 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P4-R5` 완료.
- **완료 증거:** 활성 장르 `orders` 정렬, exact direct array, route·공통 경계 회귀 성공,
OpenAPI operation `implemented`.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
- [x] **Task 4.12: 시리즈 상세 data를 목록 item과 정합화**
**Goal 실행 `P4-R6`:** 시리즈 상세 `data`를 별도 레거시 상세 DTO가 아니라 시리즈 목록 `items` 하나와 동일한
11개 필드·타입으로 반환한다.
- **추적 review ID:** `REV-047`.
- **시작 조건:** `P4-R5-GATE` 완료와 PRD·OpenAPI의 승인된 시리즈 상세 계약 존재.
- **완료 증거:** 목록과 상세의 동일 series exact JSON 대조, owner/active 격리, enum·nullable·cover URL parity,
기존 상세 전용 `genre`, `keywords` 부재와 Progress 기록.
- **범위 밖:** 목록 wrapper 변경, 시리즈 entity/legacy detail DTO 변경, 새 필드 추가, public/legacy endpoint 변경.
**Interfaces:**
- `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`
- Produces `data`:
`seriesId`, `title`, `introduction`, `coverImageUrl`, `publishedDaysOfWeek`, `genreId`, `isAdult`, `state`,
`isActive`, `writer`, `studio`.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesQueryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContractTest.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 동일 series의 목록 item과 상세 `data`가 현재 필드·타입이 다른 것을 exact JSON 비교로 고정한다.
- [x] **GREEN:** v2 상세 response type을 `AiCharacterAdminSeriesListItem`으로 통일하고 owner 범위에서 조회한 entity를
동일 필드로 매핑한다.
- [x] **CONTRACT TEST:** `publishedDaysOfWeek`와 `state` enum, `genreId`, `isActive`, nullable `writer/studio`,
cover URL이 목록과 같고 `genre`, `keywords`가 없는지 확인한다.
- [x] **REFACTOR:** 레거시 `GetCreatorAdminContentSeriesDetailResponse`와 entity mapper는 변경하지 않고 v2
series 경계만 수정해 Series/common 회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesQueryTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContractTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 시리즈 상세 정합화 Gate
**Goal 실행 `P4-R6-GATE`:** `REV-047`의 상세 단일 목록-item schema와 runtime parity를 재검토한다.
- [x] **`P4-R6-GATE` 완료:** `P4-R6` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 4 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P4-R6` 완료.
- **완료 증거:** 목록 item/상세 data exact parity, owner/active 경계, 구 상세 필드 제거 회귀 성공,
OpenAPI operation `implemented`.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 4 multipart request part 계약 후속 보완
- [x] **Task 4.13: 시리즈 생성·수정 request part의 application/json 강제**
**Goal 실행 `P4-R7`:** 시리즈 생성·수정 multipart의 `request` part가 OpenAPI encoding대로
`application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다.
- **추적 review ID:** `REV-057`.
- **시작 조건:** `P3-R16-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재.
- **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·image/genre/owner 의미를 유지하고,
`text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header,
S3·DB·event no-side-effect를 반환한다.
- **범위 밖:** JSON schema·strict reader·image/genre/state 의미, legacy/public endpoint,
OpenAPI·신규 dependency·DDL 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** POST·PUT의 유효 JSON 문자열을 `text/plain` request part로 보내 현재 handler에 진입하는지 actual
endpoint와 no-side-effect로 고정한다.
- [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String
strict reader에 동일 payload를 전달한다.
- [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON,
필수 part 누락 400 및 기존 image/genre/owner 회귀를 확인한다.
- [x] **REFACTOR:** series facade/domain 로직을 변경하지 않고 series/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 4 multipart request part 계약 후속 Gate
**Goal 실행 `P4-R7-GATE`:** `REV-057` 수정 뒤 Series POST·PUT의 part-level JSON-only·415 경계를 재검토한다.
- [x] **`P4-R7-GATE` 완료:** `P4-R7` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 4 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P4-R7` 완료.
- **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 및 image/genre/owner 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 4 multipart part 이름 계약 후속 보완
- [x] **Task 4.14: 시리즈 생성·수정의 미정의 multipart part 거부**
**Goal 실행 `P4-R8`:** Series 생성·수정 multipart에서 OpenAPI가 정의한 `image`, `request` 외 part를
business mutation 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-062`.
- **시작 조건:** `P3-R17-GATE` 완료와 `phase4-series-review.md` 7차 정적 리뷰 판정 존재.
- **완료 증거:** POST·PUT 미정의 part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect,
정상 image/genre/owner·필수 part·415 회귀.
- **범위 밖:** OpenAPI schema, image empty·genre/state 의미, 전역 multipart resolver, legacy/public endpoint,
신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 생성·수정에 정상 part와 `unexpected` part를 함께 보내 현재 mutation이 성공하는 경로를 actual
endpoint와 side effect로 고정한다.
- [x] **GREEN:** 실제 part 이름 집합이 POST·PUT 허용 집합 `{image, request}`의 부분집합인지 검사해 초과 이름을
`AiCharacterAdminApiException(HttpStatus.BAD_REQUEST, "common.error.invalid_request")`로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, 필수/빈 image, request part 415,
genre/owner 경계와 no-side-effect를 확인한다.
- [x] **REFACTOR:** Series controller/test만 최소 변경하고 Series/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
**처리 기록 (2026-07-29 / P4-R8):**
- RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectUndefinedCreateMultipartPartBeforeSideEffects' --tests '*shouldRejectUndefinedUpdateMultipartPartBeforeSideEffects'` → 새 테스트 6개가 400 기대 대비 기존 mutation 경로로 실패.
- GREEN/focused: 동일 focused 명령 재실행 → `BUILD SUCCESSFUL in 3m 5s`.
- 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 2m 9s`.
- 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 42s`; `git diff --check` → 출력 없음.
- OpenAPI 대조: `api-contract.openapi.json`의 `SeriesCreateMultipart`, `SeriesUpdateMultipart`는 `additionalProperties: false`이고 허용 property가 `image`, `request`임을 확인했다.
#### Phase 4 multipart part 이름 계약 후속 Gate
**Goal 실행 `P4-R8-GATE`:** `REV-062` 수정 뒤 Series POST·PUT의 허용 part 이름과 image·media type 경계를
재검토한다.
- [x] **`P4-R8-GATE` 완료:** `P4-R8` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P4-R8` 완료.
- **완료 증거:** 미정의 part 400/no-side-effect, 정상·필수/빈 image·415·genre/owner 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
**Gate 기록 (2026-07-29 / P4-R8-GATE):**
- Focused: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectUndefinedCreateMultipartPartBeforeSideEffects' --tests '*shouldRejectUndefinedUpdateMultipartPartBeforeSideEffects'` → `BUILD SUCCESSFUL in 57s`.
- 영향 범위: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` → `BUILD SUCCESSFUL in 1m 52s`.
- 정적 검증: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 16s`; `git diff --check` → 출력 없음.
- 판정: Series POST·PUT 미정의 part 400/no-side-effect, 정상·필수/빈 image·request 415·genre/owner 회귀가 모두 통과해 Phase 4 완료.
#### Phase 4 multipart 일반 form-field part 후속 보완
- [x] **Task 4.15: 시리즈 생성·수정의 전체 multipart part 이름 검증**
**Goal 실행 `P4-R9`:** Series POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를 포함한 모든
multipart part 이름을 검사해 `{image, request}` 외 이름을 mutation 전에 400
`common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-067`.
- **시작 조건:** `P3-R18-GATE` 완료와 `phase4-series-review.md` 8차 정적 리뷰 판정 존재.
- **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect,
기존 파일형 미정의 part·정상·필수/빈 image·request part 415 회귀 성공.
- **범위 밖:** OpenAPI schema, 전역 multipart resolver, legacy/public endpoint, 신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재
`fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다.
- [x] **GREEN:** servlet request의 전체 part 이름 집합을 `{image, request}`와 비교해 초과 이름을 facade 진입 전에
공통 400으로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 기존 파일형 미정의 part, 정상·필수/빈 image,
request part 415와 no-side-effect를 확인한다.
- [x] **REFACTOR:** Series controller/test만 최소 변경하고 Series/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
#### Phase 4 multipart 전체 part 이름 후속 Gate
**Goal 실행 `P4-R9-GATE`:** `REV-067` 수정 뒤 Series POST·PUT의 파일·일반 form-field를 포함한 전체 part 이름과
기존 image·media type 경계를 재검토한다.
- [x] **`P4-R9-GATE` 완료:** `P4-R9` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P4-R9` 완료.
- **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectFilenameLessUndefined*MultipartPartBeforeSideEffects*'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
- 검증 기록(RED): 무엇: Series POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다.
- 검증 기록(GREEN): 무엇: Series multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 `{image, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다.
- 검증 기록(GATE): Series/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다.
#### Phase 4 장르 ID domain validation 후속 보완
- [x] **Task 4.16: 시리즈 생성·수정의 0 이하 장르 ID 사전 거부**
**Goal 실행 `P4-R10`:** Series POST·PUT의 non-null `genreId`가 0 이하이거나 활성 장르가 아니면 legacy service
호출 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-068`.
- **시작 조건:** `P4-R9-GATE` 완료.
- **완료 증거:** 생성·수정의 `genreId=0`, 음수, 미존재 양수는 모두 KO/EN/JA 400이고 S3·DB·event
no-side-effect이며, 활성 장르와 수정 `genreId=null` 회귀 성공.
- **범위 밖:** 장르 조회 정책, OpenAPI schema, legacy repository 반환형, DB constraint, legacy/public endpoint.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 생성·수정 actual endpoint에 `genreId=0`과 음수를 보내 현재 active-genre 검사를 우회하고 legacy
repository의 non-null 경계에서 예외가 발생하는 경로를 400 기대와 no-side-effect로 고정한다.
- [x] **GREEN:** `rejectMissingActiveGenre`에서 `genreId <= 0 || !repository.existsActiveGenre(genreId)`를
공통 invalid request로 변환한다.
- [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 미존재 양수, 활성 장르, 수정 `genreId=null`,
owner/image/media type 경계와 no-side-effect를 확인한다.
- [x] **REFACTOR:** Series facade/test만 최소 변경하고 Series/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
#### Phase 4 장르 ID domain validation 후속 Gate
**Goal 실행 `P4-R10-GATE`:** `REV-068` 수정 뒤 Series 생성·수정의 장르 ID domain validation과 기존
genre/owner/multipart 경계를 재검토한다.
- [x] **`P4-R10-GATE` 완료:** `P4-R10` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 4
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P4-R10` 완료.
- **완료 증거:** 0 이하·미존재 장르 400/no-side-effect와 활성·nullable 수정 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests '*shouldRejectNonPositiveGenreBeforeSideEffects*'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
- 검증 기록(RED): 무엇: Series POST·PUT `genreId=0/-1`. 왜: 0 이하 장르 ID가 active genre 검사를 우회해 legacy service/repository까지 도달하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다.
- 검증 기록(GREEN): 무엇: 0 이하·미존재 장르 ID 사전 거부. 왜: 생성·수정의 non-null `genreId`가 유효한 활성 장르가 아니면 legacy 호출 전에 400이어야 하기 때문이다. 어떻게: `rejectMissingActiveGenre`가 `genreId <= 0 || !existsActiveGenre`를 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다.
- 검증 기록(GATE): Series/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다.
---
### Phase 5: 커뮤니티 게시글·댓글 관리 vertical slice
#### 목표
선택한 AI 캐릭터 소유 커뮤니티 게시글 등록, 수정, 고정/해제, soft delete, 관리자 조회와 댓글 CRUD를 제공한다.
#### 범위와 비범위
- 포함: owner-scoped community query/write, 최대 고정 3개, soft delete 시 fixed 상태 제거, 댓글 root/reply
조회·작성·수정·soft delete, 이미지/오디오/유료 게시글 검증, 기존 알림/최근 소식 side effect parity.
- 제외: 구매/좋아요, 캐릭터 직접 댓글, 댓글 hard delete·cascade, public community 조회 정책 변경.
#### 선행 Phase 및 의존성
- Phase 1 target resolver.
- 기존 community write behavior 특성화 테스트.
#### API endpoint와 request/response contract
- 정식 전체 schema는 `api-contract.openapi.json`의 Community operation 8개를 따른다.
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=&size=` ->
`AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`.
- `totalCount`는 target creatorMember 소유 active 게시글 전체 개수이고, `items`는 기존
`GetCommunityPostListResponse` item 필드와 고정 우선 정렬을 유지한다.
- `POST /api/v2/admin/ai-characters/{characterId}/community-posts`는 optional `audioFile`, optional `postImage`, 필수
`request: CreateCommunityPostRequest`를 받고 `data: null`을 반환한다.
- `PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`는 optional `postImage`, 필수 request를 받는다.
- update request는 두 레거시 update DTO에서 ID를 제외한 `content`, `isCommentAvailable`, `isAdult`, `isActive`,
`isFixed`만 포함하고 `data: null`을 반환한다. 수정 `audioFile`, `price`는 레거시 계약에 없어 포함하지 않는다.
- 댓글은 `GET|POST .../{postId}/comments`, `PUT|DELETE .../{postId}/comments/{commentId}`,
`GET .../{commentId}/replies`의 5개 operation을 사용한다.
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: creatorMember owner-scoped community post query adapter 추가 가능.
- Service: 신규 community application service에서 target owner 검증 후 기존 핵심 로직을 선택 재사용한다.
#### DB migration
- 없음.
#### transaction과 concurrency 고려사항
- 고정 게시글 수 검증과 고정 처리는 같은 transaction에서 수행한다.
- soft delete는 같은 transaction에서 `isActive=false`, `isFixed=false`, `fixedAt=null`을 함께 적용한다.
- 동시 고정 요청은 기존 최대 3개 정책이 깨지지 않도록 repository count와 update 순서를 테스트한다.
#### 보안 및 개인정보 위험
- 다른 character/HUMAN creator 게시글 수정, 고정, soft delete를 차단한다.
- 유료 게시글의 접근 정책과 파일 경로 노출 정책을 기존 동작과 맞춘다.
#### acceptance criteria
- target creatorMember 소유 게시글만 조회/변경된다.
- 최대 고정 수 3개 정책이 유지된다.
- soft delete된 게시글은 fixed 상태와 fixedAt이 제거된다.
- invalid target/ownership 실패 시 DB/S3/event side effect가 없다.
#### targeted test
- Characterization: `LegacyCommunityPostCharacterizationTest`.
- V2 RED/GREEN: `AiCharacterAdminCommunityPostServiceTest`.
- Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`
#### 전체 회귀 테스트 영향
- 기존 v2 community 조회와 legacy community write 테스트가 통과해야 한다.
#### rollback 전략
- 신규 community v2 admin route/facade를 제거한다.
- 신규 DDL이 없으므로 schema rollback은 없다.
#### 권장 commit 경계
- `feat: add ai character admin community slice`
- [x] **Task 5.1: 기존 community behavior 특성화 baseline 고정**
**Goal 실행 `P5-T1`:** 기존 community의 media·유료·고정·soft delete·알림 동작을 신규 v2 비교 기준으로 고정한다.
- **시작 조건:** `P4-GATE` 완료.
- **완료 증거:** production 변경 전 특성화 테스트 통과, 오류·side-effect·동시성 관찰 결과와 Progress 기록.
- **범위 밖:** 신규 v2 community production code 구현.
**Files:**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/LegacyCommunityPostCharacterizationTest.kt`
- [x] image/audio/paid post validation과 notification/recent-news side effect baseline을 작성한다.
- [x] 최대 고정 3개와 fixed post soft delete clearing baseline을 작성한다.
- [x] Phase 5 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
- [x] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
- [x] fixture/event spy만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- **관찰된 legacy baseline:** 유료 게시글은 `postImage`가 필수이고 오디오 게시글도 `postImage` 없이는 실패한다. 무료 게시글 생성은 FCM
`CHANGE_NOTICE`와 home following recent-news를 발행하지만 유료 게시글은 recent-news를 발행하지 않는다. 이미 고정된 게시글의
재고정은 최대 3개 count를 다시 적용하지 않고, 다른 미고정 게시글을 4번째로 고정하려 하면 `creator.community.max_fixed_post_count`로
실패한다. `isActive=false` 수정은 같은 transaction에서 `isFixed=false`, `fixedAt=null`로 정리한다.
- **Phase 5 v2 domain/client 오류 결정:** target/post missing·inactive·cross-owner, pagination·binding 실패, 최대 고정 3개 초과,
유료/오디오 게시글 이미지 누락, 이미지 validation 실패는 신규 prefix에서 400으로 반환한다. ownership/validation 실패는 DB/S3/FCM/recent-news
side effect 전에 발생해야 한다. 예상하지 못한 S3/event/server 오류는 500 `common.error.unknown`을 사용하며, legacy처럼 recent-news
publish 실패는 게시글 생성을 실패시키지 않는다.
| Phase 5 domain/client 경우 | status | message key | KO | EN | JA |
|---|---:|---|---|---|---|
| missing/inactive/cross-owner resource, pagination·binding 검증 실패 | 400 | `common.error.invalid_request` | 잘못된 요청입니다. | Invalid request. | 無効なリクエストです。 |
| 유료 게시글 이미지 누락 | 400 | `creator.community.paid_post_image_required` | 유료 게시글은 이미지를 등록해 주세요. | Please add an image for paid posts. | 有料投稿には画像を登録してください。 |
| 오디오 게시글 이미지 누락 | 400 | `creator.community.audio_post_image_required` | 오디오 게시글은 이미지를 등록해 주세요. | Please add an image for audio posts. | オーディオ投稿には画像を登録してください。 |
| 고정 게시글 3개 초과 | 400 | `creator.community.max_fixed_post_count` | 고정 게시글은 최대 3개까지 가능합니다. | You can pin up to 3 posts. | 固定投稿は最大3件まで可能です。 |
| 이미지가 아님 | 400 | `image.error.only_image_allowed` | 이미지만 업로드할 수 있습니다. | Only images can be uploaded. | 画像のみアップロードできます。 |
| 유료가 아닌 게시글 GIF 이미지 | 400 | `image.error.gif_paid_only` | GIF 이미지는 유료 게시글에만 등록할 수 있습니다. | GIF images can only be used for paid posts. | GIF画像は有料投稿にのみ登録できます。 |
| 예상하지 못한 server/infrastructure 오류 | 500 | `common.error.unknown` | 알 수 없는 오류가 발생했습니다. 다시 시도해 주세요. | An unknown error occurred. try again. | 不明なエラーが発生しました。恐れ入りますが、もう一度お試しください。 |
- 검증 기록: 무엇: legacy community media/paid validation, FCM/recent-news side effect, fixed limit와 soft-delete fixed clearing baseline.
왜: Phase 5 v2 구현 전에 재사용할 legacy 동작과 신규 prefix에서 보강할 owner-first 오류 경계를 분리하기 위해. 어떻게:
`LegacyCommunityPostCharacterizationTest`를 추가하고 production 변경 없이 focused/community package test, `ktlintCheck`, `git diff --check`를
실행했다. 결과: focused characterization은 `BUILD SUCCESSFUL in 2m 22s`, community package targeted test는
`BUILD SUCCESSFUL in 2m 22s`, `ktlintCheck`는 unused import 2건 정리 후 `BUILD SUCCESSFUL in 21s`, `git diff --check`는
출력이 없었다.
- [x] **Task 5.2: 관리자 게시글 목록 조회 구현**
**Goal 실행 `P5-T2`:** target owner의 관리자용 커뮤니티 게시글 목록을 안전한 전용 DTO와 pagination으로 제공한다.
- **시작 조건:** `P5-T1` 완료와 Phase 5 오류 계약의 계획 반영.
- **완료 증거:** owner/inactive/pagination/DTO RED/GREEN과 public 조회 무비판적 복제 없음 증거.
- **범위 밖:** 게시글 생성·수정·고정·삭제.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt`
- [x] 미구현 목록, owner 격리, pagination 경계와 관리자 DTO 실패 test를 작성한다.
- [x] 최소 owner-scoped query와 `page/size` 보정으로 focused test를 통과시킨다.
- [x] 유료/media private 정보와 public viewer 상태를 부적절하게 노출하지 않는지 검증한다.
- [x] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록: 무엇: target 소유 활성 게시글 목록, 고정 정렬, pagination, legacy DTO 배열 형태, 유료 오디오 owner signed URL과 오류 계약.
왜: 공개 viewer 정책을 복제하지 않고 target `creatorMember`를 관리자 조회의 owner viewer로 고정하기 위해. 어떻게: RED로
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest --rerun-tasks`를
실행해 route 부재로 4개 상태 코드 assertion이 실패함을 확인했다. 리뷰 보완으로 row별 count 조회가 붙은 목록의 `size=51`을
추가 RED로 확인했고, `size` 허용 범위를 1..50으로 제한했다. 최종 GREEN으로 같은 focused 명령은
`BUILD SUCCESSFUL in 3m`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --rerun-tasks`는
`BUILD SUCCESSFUL in 3m 24s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 27s`, `git diff --check`는
출력이 없었다. 전체 회귀는 task 범위가 신규 관리자 community query에 한정되어 있어 실행하지 않았다.
- [x] **Task 5.3: 커뮤니티 게시글 생성 구현**
**Goal 실행 `P5-T3`:** target creatorMember 작성자로 이미지·오디오·유료 게시글을 기존 검증과 side effect parity로 생성한다.
- **시작 조건:** `P5-T1`, `P5-T2` 완료.
- **완료 증거:** 정상/media/paid validation RED/GREEN, writer/owner와 S3/event 결과, Progress 기록.
- **범위 밖:** 게시글 수정·고정·soft delete, 구매 기능.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
- [x] 정상/image/audio/paid validation과 invalid target 실패 test를 작성한다.
- [x] 해석된 creatorMember를 writer/owner로 사용하는 최소 생성 구현을 통과시킨다.
- [x] S3 media upload 결과와 validation/target 실패 시 DB·S3 no-side-effect를 검증한다.
- [x] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [x] **Task 5.4: 게시글 수정·고정·soft delete 구현**
**Goal 실행 `P5-T4`:** owner 게시글만 수정·고정/해제하고 soft delete 시 고정 상태와 시간을 함께 제거한다.
- **시작 조건:** `P5-T1`, `P5-T2` 완료.
- **완료 증거:** update/fixed/soft delete/cross-owner RED/GREEN과 transaction 결과 기록.
- **범위 밖:** hard delete, 구매·좋아요·댓글 관리.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
- [x] 수정·고정/해제·soft delete와 cross-owner 실패 test를 작성한다.
- [x] owner 검증 후 최소 mutation 구현으로 test를 통과시킨다.
- [x] soft delete가 한 transaction에서 `isActive=false`, `isFixed=false`, `fixedAt=null`을 적용하는지 검증한다.
- [x] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: content/comment/adult 수정, 이미지 교체, 고정/해제, soft delete, target/post/cross-owner/inactive 거부와 request part 누락 실제 endpoint 계약. 왜: `PUT` route와 owner-first mutation이 구현 전에는 존재하지 않음을 고정하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest`를 production 변경 전에 실행했다. 결과: 6개 테스트가 기대 200/400 대신 미구현 `PUT`의 405로 실패해 `BUILD FAILED in 50s`였다.
- 검증 기록(GREEN/REFACTOR): 무엇: active target과 active owner post 사전 검증, legacy 수정/고정 위임, soft delete fixed clearing 및 no-side-effect. 왜: legacy media/fixed 정책은 유지하면서 v2 경계의 cross-owner/inactive mutation을 차단하기 위해. 어떻게: focused test, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`, `./gradlew ktlintCheck`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 3m 52s`, community package 회귀는 `BUILD SUCCESSFUL in 59s`, ktlint는 `BUILD SUCCESSFUL in 29s`였다. 전체 `./gradlew test`는 신규 community update slice의 direct focused/community 회귀가 실행됐으므로 실행하지 않았다.
- 최종 fresh community 회귀: `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'` 실행 결과 10개 Gradle task가 수행되어 `BUILD SUCCESSFUL in 5m 34s`였다.
- 코드 품질 보완 RED: 최대 고정 3개인 owner가 네 번째 게시글을 `postImage`와 `isFixed=true`로 수정할 때 legacy 최대 고정 오류를 반환하면서도 imagePath와 S3 `putObject`가 변경되지 않아야 하는 test를 추가했다. 기존 호출 순서에서 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest`를 실행한 결과 7개 중 새 test 1개가 imagePath 변경 assertion에서 실패해 `BUILD FAILED in 48s`였다.
- 코드 품질 보완 GREEN: `isActive != false && isFixed != null`인 고정 호출을 legacy 이미지 수정 앞에 두고 같은 focused 명령을 실행한 결과 7개 test가 `BUILD SUCCESSFUL in 59s`였다. 이어 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`는 `BUILD SUCCESSFUL in 1m 10s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 32s`였다.
- [x] **Task 5.5: 최대 고정 수·동시성·side effect 검증**
**Goal 실행 `P5-T5`:** 최대 고정 게시글 3개 정책이 동시 요청과 실패에서도 깨지지 않도록 고정한다.
- **시작 조건:** `P5-T4` 완료.
- **완료 증거:** 세 번째/네 번째 고정, 동시 요청, cross-owner와 DB/S3/event side effect RED/GREEN 기록.
- **범위 밖:** 새로운 lock/dependency 도입, 기존 고정 정책 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostConcurrencyTest.kt`
- [x] 순차 고정 요청에서 3개 허용·4번째 거부와 재고정 count 생략을 특성화한다.
- [x] 기존 legacy repository count/update 순서를 유지하고 production 변경 없이 test를 통과시킨다.
- [x] invalid target/ownership 고정 실패 시 DB/S3/event 0건을 검증한다.
- [x] concurrency 제약, focused/community test와 `ktlintCheck` 결과를 Progress에 기록한다.
- **TDD 예외/특성화:** `AiCharacterAdminCommunityPostConcurrencyTest`를 production 변경 전에 추가해 첫 focused
실행했으나 3개 test가 모두 통과했다. 이는 P5-T4 facade가 legacy 고정 호출을 수정·이미지 업로드보다 먼저 수행하고,
`CreatorCommunityService.updateCommunityPostFixed`가 미고정 post에만 활성 고정 수를 조회하는 기존 동작이 이미 요구를
충족했기 때문이다. 따라서 production 코드, lock, DB constraint, dependency를 추가하지 않았다.
- **동시성 관찰 제한:** 현재 legacy 정책은 `countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue` 뒤 entity를 갱신하는
count/update 순서이며 lock 또는 DB constraint가 없다. 서로 다른 미고정 post의 실제 병렬 요청은 두 요청이 같은 count를
읽을 수 있어 결정적으로 재현·검증할 수 없으므로 sleep/flaky test를 추가하지 않고, 2개 고정 상태에서 세 번째 성공 뒤
네 번째 거부되는 순차 특성화만 고정했다. 이 Task 범위는 기존 정책 변경을 포함하지 않는다.
- 검증 기록(특성화): 무엇: 세 번째 활성 고정 성공, 네 번째 활성 고정 400 최대 고정 메시지, 최대 상태의 이미 고정된 post
재고정, invalid target/cross-owner fixed multipart 요청의 DB/imagePath/S3/event 무변경. 왜: 기존 legacy 고정 정책과
owner-first side-effect 차단을 production 변경 없이 고정하기 위해. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest`를
실행했다. 결과: `BUILD SUCCESSFUL in 45s`, 10 actionable tasks 중 3 executed, 7 up-to-date였다.
- 검증 기록(영향 범위): 무엇: 전체 v2 admin community package 회귀와 Kotlin lint. 왜: 신규 focused test의 controller/facade와
legacy community 경계 회귀를 확인하기 위해. 어떻게:
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`, `./gradlew ktlintCheck`를 실행했다.
결과: package test는 `BUILD SUCCESSFUL in 1m 14s`, 10 actionable tasks 중 1 executed, 9 up-to-date였고, ktlint는
`BUILD SUCCESSFUL in 23s`, 7 actionable tasks 중 2 executed, 5 up-to-date였다.
- [x] **Task 5.6: Phase 5 보안·오류·회귀 검증**
**Goal 실행 `P5-T6`:** 모든 community endpoint의 ADMIN·오류·ownership 계약과 기존 public/legacy 회귀를 고정한다.
- **시작 조건:** `P5-T2`~`P5-T5` 완료.
- **완료 증거:** endpoint 권한 매트릭스, 오류 status/key/KO·EN·JA, legacy/public 회귀와 Progress 기록.
- **범위 밖:** Phase 6 FanTalk 기능.
**Files:**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostContractTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- [x] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다.
- [x] target/post/media/fixed-count 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
- [x] HUMAN/cross-character 게시글 mutation 거부와 legacy/public community 계약을 검증한다.
- [x] Phase 5 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- **TDD 예외/특성화 (2026-07-28):** production 변경 전에
`AiCharacterAdminCommunityPostContractTest`를 추가하고
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostContractTest --rerun-tasks`를
실행했다. invalid target/post, paid media, 최대 고정 수, 필수 multipart `request` part의 KO/EN/JA
`ApiResponse.error`, HUMAN target·cross-character mutation의 DB/S3 무변경이 모두 기존 구현에서 통과했다.
요구 동작이 이미 충족된 순수 특성화이므로 production 코드, dependency, legacy/public endpoint를 변경하지 않았다.
- 검증 기록(focused, 2026-07-28): 무엇: community `GET`/`POST`/`PUT`의 JWT 비ADMIN 및 stale ADMIN claim
차단, 실제 endpoint 오류·소유권 계약. 왜: prefix 공통 sample/series 검증만으로는 community mapping 전체를
보장할 수 없기 때문이다. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --rerun-tasks`,
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostContractTest --rerun-tasks`를
실행했다. 결과: 각각 `BUILD SUCCESSFUL in 4m 45s`(10 actionable tasks 모두 실행),
`BUILD SUCCESSFUL in 4m 6s`(10 actionable tasks 모두 실행)였다.
- 검증 기록(Phase 5 Gate, 2026-07-28): 무엇: legacy 특성화와 v2 community package 회귀, Kotlin lint.
왜: P5-T1~P5-T6의 목록·생성·수정·고정·soft delete 및 기존 community 계약을 최종 확인하기 위해. 어떻게:
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --rerun-tasks`,
`./gradlew ktlintCheck --rerun-tasks`를 실행했다. 결과: package test는
`BUILD SUCCESSFUL in 5m 52s`(10 actionable tasks 모두 실행), ktlint는
`BUILD SUCCESSFUL in 35s`(7 actionable tasks 모두 실행)였다. 전체 `./gradlew test`는 커뮤니티 경계와
공통 인가 production 코드가 변경되지 않아 실행하지 않았다.
#### Phase 5 Gate
**Goal 실행 `P5-GATE`:** Phase 5 community 사용자 흐름과 고정·side-effect·회귀 품질을 최종 판정한다.
- [x] **`P5-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P5-T1`~`P5-T6` 완료.
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`와
`./gradlew ktlintCheck` 성공, Progress 기록.
- **범위 밖:** 실패와 무관한 Phase 6 구현.
#### Phase 5 후속 리뷰 보완
- [x] **Task 5.7: 커뮤니티 JSON 오류와 pagination을 OpenAPI에 정합화**
**Goal 실행 `P5-R1`:** `REV-025`의 multipart JSON 파싱 실패·미지 필드를 일관된 400으로 처리하고,
`REV-026`의 계약에 없는 목록 `size <= 50` 제한을 제거한다.
- **추적 review ID:** `REV-025`, `REV-026`.
- **시작 조건:** `P4-R1-GATE` 완료와 `phase5-community-review.md` 판정 존재.
- **완료 증거:** create/update의 malformed·필수 필드 누락·미지 필드 JSON이 400
`common.error.invalid_request`이고 DB/S3/event 부작용이 0회인 actual endpoint 테스트, `size=51` 요청이 문서 계약대로
상한 검증에 막히지 않는 목록 테스트, community/common 회귀와 Progress 기록.
- **범위 밖:** OpenAPI에 pagination 상한 추가, legacy/public community controller 변경, media/fixed/notification 정책 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** create/update `request` part의 malformed JSON, 필수 필드 누락, 미지 필드가 exact 400 envelope과
DB/S3/event 0회를 반환하는지 확인한다.
- [x] **RED:** `GET .../community-posts?size=51`이 계약 밖 상한 오류 없이 정상 pagination으로 처리되는지 확인한다.
- [x] **GREEN:** legacy service 호출 전에 strict reader로 request DTO를 검증하고 JSON mapping 예외를
`common.error.invalid_request`로 변환하며 목록의 `size > 50` guard만 제거한다.
- [x] **REFACTOR:** community/common 영향 범위 회귀, `ktlintCheck`, diff check를 실행한다.
- 검증 기록(RED): 무엇: community create/update `request` part의 malformed JSON, create 필수 field 누락, create/update 미지 field와 목록 `size=51` 계약을 actual endpoint로 고정했다. 왜: `REV-025`~`REV-026`이 runtime에서 실제 실패하는지 확인하기 위해. 어떻게: 아래 focused create/update/query 명령을 production 변경 전 실행했다. 결과: 19개 중 3개가 JSON 경계와 `size=51` 상한 때문에 실패해 `BUILD FAILED in 1m 50s`였다. OpenAPI 확인 결과 update request는 required field가 없어 update 필수 field 누락 케이스는 제거했다.
- 검증 기록(GREEN/REFACTOR): 무엇: create/update를 legacy service 호출 전 strict reader로 검증하고 Jackson parse/mapping 오류를 400 `common.error.invalid_request`로 변환했으며 목록의 `size <= 50` 상한만 제거했다. 왜: OpenAPI `additionalProperties: false`와 `Size` maximum 부재 계약을 runtime에 맞추기 위해. 어떻게: focused create/update/query 명령, community/common 회귀, `ktlintCheck`, `git diff --check`를 실행했다. 결과: focused 명령은 `BUILD SUCCESSFUL in 1m 24s`, community/common 회귀는 `BUILD SUCCESSFUL in 2m`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1m 2s`, `git diff --check`는 출력이 없었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 5 후속 리뷰 Gate
**Goal 실행 `P5-R1-GATE`:** `REV-025`~`REV-026` 수정 뒤 Community 3개 operation의 JSON 오류와 pagination 경계를
재검토한다.
- [x] **`P5-R1-GATE` 완료:** `P5-R1` 완료 후 actual endpoint no-side-effect와 community/common 회귀,
lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P5-R1` 완료.
- **완료 증거:** 잘못된 JSON의 400 통일, 미지 필드 거부, 계약 밖 size 상한 제거, 영향 범위 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
- 검증 기록: 무엇: `P5-R1-GATE`에서 Community 3개 operation의 JSON 오류와 pagination 경계를 재판정했다. 왜: `REV-025`~`REV-026` 처리 후 actual endpoint no-side-effect와 영향 범위 회귀를 확인하기 위해. 어떻게: `P5-R1`과 같은 focused/community/common 회귀, `ktlintCheck`, `git diff --check` 증거를 기준으로 리뷰 문서와 Progress를 갱신했다. 결과: 잘못된 JSON의 400 통일, 미지 field 거부, `size=51` 허용과 영향 범위 회귀가 모두 통과했다.
#### Phase 5 2차 후속 리뷰 보완
- [x] **Task 5.8: 최대 고정 3개 동시성 보장**
**Goal 실행 `P5-R2`:** `REV-033`의 count-then-update 경쟁 조건을 owner 단위로 직렬화해 실제 동시 요청에서도
활성 고정 게시글이 3개를 초과하지 않도록 한다.
- **추적 review ID:** `REV-033`.
- **시작 조건:** `P5-R1-GATE` 완료와 `phase5-community-review.md` 2차 리뷰 판정 존재.
- **완료 증거:** 두 독립 transaction의 결정적 동시 요청 RED, owner lock 순서 증거, 최종 고정 수 3개와
한 요청 성공·한 요청 400, 실패 요청의 S3/event 무변경, community/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 신규 DDL/unique constraint/dependency, 최대 수 정책 변경, legacy/public endpoint 변경,
sleep 또는 반복 확률에 의존하는 flaky test.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostConcurrencyTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt`
- [x] **RED:** 활성 고정 2개와 서로 다른 미고정 게시글 2개를 준비하고, 두 독립 transaction을 barrier/lock probe로
같은 owner에 동시에 진입시켜 현재 최종 고정 수가 4개가 될 수 있음을 결정적으로 재현한다.
- [x] **RED:** 기존 순차 테스트와 별개로 실제 병렬 요청임을 thread/transaction ID와 barrier 도달 assertion으로 확인하고,
sleep·무작위 반복으로 성공 확률을 높이는 방식은 사용하지 않는다.
- [x] **GREEN:** 기존 `MemberRepository.findByIdForUpdate`를 재사용해 fixed/unfixed count·update 전에 owner row를
잠그고, legacy 최대 3개 검증과 mutation을 같은 transaction에서 직렬화한다.
- [x] **GREEN:** 같은 동시성 테스트에서 최종 고정 수 3개, 한 요청의 최대 고정 오류, DB/imagePath/S3/event 결과를
확인한다.
- [x] **REFACTOR:** 재고정·해제·soft delete와 순차 세 번째/네 번째 요청을 유지하고 community/common 영향 범위 회귀,
`ktlintCheck`, `git diff --check`를 실행해 Progress에 기록한다.
- 검증 기록(RED): 무엇: 활성 고정 2개 상태에서 서로 다른 미고정 게시글 2개를 병렬 fixed 요청으로 보내 두 요청이 같은 count 경계를 통과하는 race를 고정했다. 왜: `REV-033`의 count-then-update 경쟁 조건을 순차 테스트가 아닌 실제 병렬 요청으로 재현하기 위해. 어떻게: `CreatorCommunityRepository.countByMemberIdAndIsFixedIsTrueAndIsActiveIsTrue` 첫 호출을 latch로 지연하고 두 번째 요청을 진입시킨 뒤 `AiCharacterAdminCommunityPostConcurrencyTest`를 production 변경 전 실행했다. 결과: 신규 병렬 테스트가 기대 `[200, 400]` 대비 `[200, 200]`과 최종 4개 고정으로 실패해 RED를 확인했다.
- 검증 기록(GREEN/REFACTOR): 무엇: fixed 변경 요청에서 legacy count/update 전에 `MemberRepository.findByIdForUpdate(creatorMemberId)`로 owner row를 잠그고, 병렬 요청을 직렬화했다. 왜: 신규 DDL 없이 owner 단위 최대 고정 3개 불변식을 같은 transaction 안에서 보장하기 위해. 어떻게: focused concurrency, community/common 영향 범위 회귀, `./gradlew ktlintCheck`, `git diff --check`를 실행했다. 결과: focused는 `BUILD SUCCESSFUL in 46s`, community/common 회귀는 `BUILD SUCCESSFUL in 1m 19s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostConcurrencyTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 5 2차 후속 리뷰 Gate
**Goal 실행 `P5-R2-GATE`:** `REV-033`의 owner lock과 최대 고정 수 동시성 불변식을 재검토한다.
- [x] **`P5-R2-GATE` 완료:** `P5-R2` 완료 후 동시성 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P5-R2` 완료.
- **완료 증거:** `REV-033` 처리 완료, 결정적 병렬 재현, 최대 3개와 실패 no-side-effect, 순차/soft-delete 회귀 성공.
- **범위 밖:** Gate에서 production code, DB schema 또는 공개 API 계약 변경.
- 검증 기록: 무엇: `P5-R2-GATE`에서 owner lock과 최대 고정 수 동시성 불변식을 재판정했다. 왜: `REV-033` 처리 후 병렬/순차 fixed 정책과 community/common 영향 범위가 모두 유지되는지 확인하기 위해. 어떻게: focused concurrency, community/common 회귀, lint, diff check 결과를 fresh 확인하고 리뷰 문서와 Progress를 갱신했다. 결과: 모든 Gradle 명령이 `BUILD SUCCESSFUL`이고 diff check 출력이 없어 Phase 5 후속 리뷰를 완료로 판정했다.
#### Phase 5 5차 리뷰 보완
- [x] **Task 5.9: 커뮤니티 primitive 필드의 required·null 계약 강제**
**Goal 실행 `P5-R3`:** 커뮤니티 생성의 필수 boolean 누락·null과 optional `price`의 explicit null, 수정
`isFixed`의 explicit null을 생략 또는 JVM 기본값으로 보정하지 않고 S3·DB·event 전에 400으로 거부한다.
- **추적 review ID:** `REV-043`.
- **시작 조건:** `P4-R4-GATE` 완료와 `phase5-community-review.md` 5차 정적 리뷰 판정 존재.
- **완료 증거:** 생성 required boolean 누락·null, `price: null`, 수정 `isFixed: null`의 actual endpoint
RED/GREEN/no-side-effect, optional 생략·정상 mutation 회귀, community/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 전역 `ObjectMapper` 설정, 레거시 DTO/service 변경, OpenAPI schema·기본값 변경, community 정책 확장.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreateCommunityPostRequest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 생성 `isCommentAvailable`, `isAdult`의 누락·null과 `price: null`, 수정 `isFixed: null`이 현재
false·0 또는 생략으로 처리되는지 actual endpoint로 고정하고 DB·S3·event 결과를 단언한다.
- [x] **GREEN:** v2 create/update 경계에서 required primitive의 존재와 모든 non-null primitive의 명시적 null을
검증해 `common.error.invalid_request` 400으로 변환한다.
- [x] **CONTRACT TEST:** 생성 `price` 생략은 `0`, 수정 `isFixed` 생략은 변경 없음으로 유지하고 정상
create/update/fix/soft delete와 기존 미지 필드 거부를 확인한다.
- [x] **REFACTOR:** 기존 strict parse 결과를 재사용하는 최소 검증으로 제한하고 community/common 영향 범위 회귀,
`ktlintCheck`, `git diff --check`를 실행해 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 5 5차 리뷰 Gate
**Goal 실행 `P5-R3-GATE`:** `REV-043`의 커뮤니티 primitive required/nullability와 생략 기본값·부작용 경계를 재검토한다.
- [x] **`P5-R3-GATE` 완료:** `P5-R3` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 리뷰 문서와
Progress를 갱신한다.
- **시작 조건:** `P5-R3` 완료.
- **완료 증거:** review ID 처리 완료, invalid primitive 400/no-side-effect, optional 생략과 정상 mutation 회귀 성공.
- **범위 밖:** Gate에서 production code, 레거시 API 또는 공개 API schema 변경.
- 검증 기록: 무엇: `REV-043`의 커뮤니티 primitive required/nullability 경계를 처리했다. 왜: 생성 required boolean과
`price`, 수정 `isFixed`의 null/누락이 기본값 또는 생략으로 보정되면 잘못된 mutation이 진행될 수 있기 때문이다. 어떻게:
actual multipart POST/PUT RED/GREEN, create/update focused, community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를
fresh 실행하고 리뷰 문서를 갱신했다. 결과: invalid primitive 요청은 400/no-side-effect로 고정됐고 optional 생략·정상 mutation 회귀는 유지됐다.
#### Phase 5 목록 계약 변경 보완
- [x] **Task 5.10: 커뮤니티 목록 timezone 제거와 pagination metadata 제공**
**Goal 실행 `P5-R4`:** 커뮤니티 목록을 `timezone` 없이 조회하고 active owner 게시글의 전체 개수와 현재 page/size,
다음 페이지 여부, 기존 item 목록을 반환한다.
- **추적 근거:** `DEC-P5-LIST-001`.
- **시작 조건:** `P5-R3-GATE` 완료와 PRD·OpenAPI의 승인된 목록 계약 존재.
- **완료 증거:** timezone 없는 actual GET의 RED/GREEN, `totalCount/page/size/hasNext/items` exact response,
첫·중간·마지막·범위 밖 page와 active owner count 회귀, community/common 영향 범위 회귀와 Progress 기록.
- **범위 밖:** 목록 item 필드·정렬 변경, public/legacy community endpoint 변경, 검색/filter 추가, Spring `Page` 공개,
신규 dependency·DDL.
**Interfaces:**
- Consumes: `characterId`, `page` 기본값 `0`, `size` 기본값 `20`.
- Produces:
`AiCharacterAdminCommunityPostListResponse(totalCount: Long, page: Int, size: Int, hasNext: Boolean, items: List<AiCharacterAdminCommunityPostDto>)`.
- `totalCount`: target creatorMember 소유이면서 `isActive=true`인 게시글 전체 개수.
- `hasNext`: `pageable.offset + items.size < totalCount`.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostContractTest.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/prd.md`
- [x] **RED:** `timezone` 없이 호출한 GET이 현재 400을 반환하는 것과, timezone을 전달한 정상 호출의 `data`가
pagination object가 아니라 직접 배열인 계약 차이를 actual endpoint로 고정한다.
- [x] **RED:** active owner 게시글을 `size + 1`개 이상 준비하고 첫 page의 `totalCount`, `page`, `size`,
`hasNext=true`, 마지막 page의 `hasNext=false`, 범위 밖 page의 빈 `items`를 exact JSON으로 고정한다.
- [x] **GREEN:** controller/facade에서 `timezone` parameter와 사용되지 않는 검증을 제거하고 `page`, `size`만 전달한다.
- [x] **GREEN:** repository에 active owner count query 하나를 추가하고 기존 목록 query·고정 우선 정렬은 유지한다.
- [x] **GREEN:** facade가 count와 현재 page items로 `AiCharacterAdminCommunityPostListResponse`를 구성하고
`hasNext`를 `pageable.offset + items.size < totalCount`로 계산한다.
- [x] **CONTRACT TEST:** item의 기존 18개 필드, owner/inactive 격리, `page < 0`·`size < 1` 400과
문서에 없는 size 상한 부재를 유지하고 OpenAPI status를 `implemented`로 갱신한다.
- [x] **REFACTOR:** Spring `Page`나 공용 pagination abstraction을 추가하지 않고 community package와 공통
authorization/error 회귀, `ktlintCheck`, `git diff --check`를 실행해 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostContractTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
jq -e '
.paths["/api/v2/admin/ai-characters/{characterId}/community-posts"].get as $operation
| ([$operation.parameters[] | .["$ref"]] | index("#/components/parameters/Timezone") | not)
and ($operation["x-implementation-status"] == "implemented")
and (.components.schemas.CommunityPostListResponse.required
== ["totalCount", "page", "size", "hasNext", "items"])
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 5 목록 계약 변경 Gate
**Goal 실행 `P5-R4-GATE`:** `DEC-P5-LIST-001`의 query 제거와 pagination metadata·owner count 계약을 재검토한다.
- [x] **`P5-R4-GATE` 완료:** `P5-R4` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 5 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R4` 완료.
- **완료 증거:** timezone 없는 목록 성공, exact pagination wrapper, active owner total/hasNext와 기존 item·정렬·오류
회귀 성공, OpenAPI `implemented` 복구.
- **범위 밖:** Gate에서 production code, item schema 또는 public/legacy API 변경.
#### Phase 5 후속 기능 보완
- [x] **Task 5.11: 커뮤니티 댓글 CRUD**
**Goal 실행 `P5-R5`:** target AI 캐릭터 소유 활성 커뮤니티 게시글의 원댓글·답글을 조회하고 target AI 명의로
작성·수정하며, 해당 게시글에 달린 댓글·답글은 작성자와 관계없이 row 단위로 soft delete한다.
- **추적 review ID:** `REV-048`.
- **시작 조건:** `P4-R6-GATE` 완료와 PRD·OpenAPI의 승인된 댓글 행위자·소유권 계약 존재.
- **완료 증거:** 5개 actual endpoint, root/reply 조회, target AI 작성, 작성자 제한 수정, owner 범위 삭제,
cross-resource/parent/character 격리, idempotent delete와 exact response 회귀.
- **범위 밖:** 캐릭터 직접 댓글 삭제, 댓글 hard delete·cascade, 게시글 CRUD 의미 변경, 레거시/public endpoint 변경.
**Interfaces:**
- `GET|POST /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments`
- `PUT|DELETE /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}`
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies`
- 조회 query: 필수 `timezone`, `page`, `size`; response `GetCommunityPostCommentListResponse(totalCount, items)`.
- 작성 body: 필수 `comment`, optional/nullable `parentId`, optional `isSecret=false`.
- 수정 body: 필수 `comment`; mutation 성공 `data: null`.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCommentTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** root/reply 목록의 `timezone/page/size`, `totalCount/items`, target 소유 활성 게시글 경계를 actual
GET으로 고정한다.
- [x] **RED:** root와 reply 작성 시 저장된 `member`가 target `creatorMember`이고, `parentId`가 같은 게시글의 활성
root가 아니면 400/no insert/no event인지 고정한다.
- [x] **RED:** target AI가 작성한 활성 댓글/답글만 수정되고 팬 작성, 다른 게시글·캐릭터 댓글 수정은
400/no mutation인지 고정한다.
- [x] **RED:** target 소유 게시글의 팬/AI 댓글·답글 삭제는 해당 row만 비활성화하고 하위 답글은 유지하며, 이미
비활성인 row는 200 no-op인지 고정한다.
- [x] **GREEN:** 기존 `CreatorCommunityService`의 댓글 조회·작성·수정 의미를 재사용하되 facade에서 target,
active owner, 동일 리소스 root parent, actor 권한을 먼저 검증한다.
- [x] **CONTRACT TEST:** 미지 필드, 잘못된 page/size/timezone, cross-resource ID의 400 envelope와 모든
mutation의 `data: null`을 확인한다.
- [x] **REFACTOR:** 댓글 전용 공용 abstraction이나 cascade 로직을 추가하지 않고 community/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCommentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 5 후속 기능 Gate
**Goal 실행 `P5-R5-GATE`:** `REV-048`의 댓글 actor·owner·parent·soft delete 경계를 재검토한다.
- [x] **`P5-R5-GATE` 완료:** `P5-R5` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 5 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R5` 완료.
- **완료 증거:** 5개 operation, 레거시 목록 parity, AI 작성·수정 제한, owner 범위 row soft delete,
cross-resource no-side-effect 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 5 UTC 날짜 계약 보완
- [x] **Task 5.12: 커뮤니티 댓글·답글 timezone 제거와 UTC date 정합화**
**Goal 실행 `P5-R6`:** 신규 관리자 커뮤니티 댓글·답글 조회에서 `timezone` query를 제거하고 기존 `date`
필드 값을 ISO-8601 UTC(`Z`)로 반환한다.
- **추적 review ID:** `REV-051`.
- **시작 조건:** `P3-R14-GATE` 완료와 `DEC-UTC-DATE-001` 및 OpenAPI 2.2.0 계약 존재.
- **완료 증거:** 커뮤니티 댓글·답글 2개 actual GET의 query·UTC exact JSON RED/GREEN, 기존
`totalCount/items`·page/size·ownership·block/secret 의미 보존, legacy/public 회귀와 Progress 기록.
- **범위 밖:** 커뮤니티 게시글 목록 item 날짜 변경, 댓글 mutation 의미 변경, legacy/public request/response 변경,
신규 pagination wrapper·dependency·DDL.
**Interfaces:**
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments`
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies`
- 두 GET의 query는 `page`, `size`만 사용한다. response는 기존 `totalCount`, `items`와 item의 `date` 필드명을
유지하며 `date` 값만 ISO-8601 UTC(`Z`)로 고정한다.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCommentTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** `timezone` 없는 root/reply GET이 현재 400이고, timezone별 로컬 문자열을 반환하는 현재 `date`
계약이 UTC exact JSON과 다른 것을 actual endpoint로 고정한다.
- [x] **GREEN:** controller/facade signature와 timezone 검증을 제거하고 v2 owner-scoped 댓글 query/mapping에서
기존 `toUtcIso()`를 재사용해 `createdAt`을 `date`에 UTC로 직렬화한다.
- [x] **CONTRACT TEST:** root/reply `date`, `totalCount/items`, page/size, target/owner/cross-post 경계와 기존
block/secret 필터가 유지되고, 추가 `timezone` query가 결과에 영향을 주지 않는지 확인한다.
- [x] **REFACTOR:** legacy/public 댓글 repository·service의 timezone 동작은 변경하지 않고 v2 경계의 최소
query/mapping만 둔다. community/common 및 직접 영향 legacy 회귀, `ktlintCheck`, OpenAPI 상태,
`git diff --check`를 기록한다.
- **`P5-R6` / `P5-R6-GATE` 검증(2026-07-29):** RED는 production 변경 전 focused 댓글 테스트에서 timezone 없는
root/reply GET이 기존 필수 query 때문에 400을 반환해 2건 실패했고 `BUILD FAILED in 38s`였다. controller/facade의
timezone 입력·검증을 제거하고 legacy 조회 결과의 `date`만 `createdAt.toUtcIso()`로 재매핑한 뒤 같은 focused 명령은
`BUILD SUCCESSFUL in 43s`였다. community/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 11s`,
`ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였다. OpenAPI 36개 operation은 모두 `implemented`,
`alignment-required`는 0개임을 `jq`로 확인했고, `git diff --check`는 출력 없이 종료했다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCommentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest \
--tests kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 36
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 36
and ([$operations[] | select(.["x-implementation-status"] == "alignment-required")] | length) == 0
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 5 UTC 날짜 계약 Gate
**Goal 실행 `P5-R6-GATE`:** `REV-051`의 커뮤니티 댓글·답글 UTC 계약과 legacy/public 격리를 재검토한다.
- [x] **`P5-R6-GATE` 완료:** `P5-R6` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 5 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R6` 완료.
- **완료 증거:** 커뮤니티 2개 GET의 timezone 제거·UTC `date`, 기존 pagination·ownership·block/secret과
legacy/public 계약 회귀 성공, OpenAPI 해당 2개 operation `implemented`.
- **범위 밖:** Gate에서 production code 또는 public/legacy API schema 변경.
#### Phase 5 JSON media type 계약 후속 보완
- [x] **Task 5.13: 커뮤니티 댓글 작성·수정의 application/json 강제**
**Goal 실행 `P5-R7`:** 커뮤니티 댓글 작성·수정 endpoint가 OpenAPI의 유일한 request media type인
`application/json`만 받고, 그 밖의 media type은 공통 415 계약으로 거부하도록 정합화한다.
- **추적 review ID:** `REV-053`.
- **시작 조건:** `P4-R7-GATE` 완료와 OpenAPI의 두 JSON requestBody 및 415 response 계약 존재.
- **완료 증거:** POST·PUT actual endpoint가 정상 JSON은 기존처럼 처리하고 `text/plain` 등 미지원 media type은
localized 415 `ApiResponse.error`, 표준 `Accept` header, DB/event no-side-effect를 반환한다.
- **범위 밖:** JSON schema·댓글 actor/owner/parent 의미, legacy/public endpoint, 공통 exception handler,
신규 dependency·DDL 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCommentTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** POST·PUT에 유효 JSON 문자열을 `text/plain`으로 보내면 현재 415가 아닌 handler 진입 결과가 나오는지
actual endpoint와 no-side-effect로 고정한다.
- [x] **GREEN:** 두 mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`만 추가한다.
- [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, 작성 insert/event 0회와 수정 row 불변,
정상 JSON 회귀를 확인한다.
- [x] **REFACTOR:** facade/parser와 댓글 도메인 동작을 변경하지 않고 community/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCommentTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 5 JSON media type 계약 후속 Gate
**Goal 실행 `P5-R7-GATE`:** `REV-053` 수정 뒤 두 mutation의 JSON-only·415·no-side-effect 경계를 재검토한다.
- [x] **`P5-R7-GATE` 완료:** `P5-R7` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 5 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R7` 완료.
- **완료 증거:** POST·PUT의 정상 JSON과 미지원 media type 415/header/envelope/no-side-effect 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 5 multipart request part 계약 후속 보완
- [x] **Task 5.14: 커뮤니티 게시글 생성·수정 request part의 application/json 강제**
**Goal 실행 `P5-R8`:** 커뮤니티 게시글 생성·수정 multipart의 `request` part가 OpenAPI encoding대로
`application/json`일 때만 handler에 진입하고, 그 밖의 part media type은 공통 415 계약으로 거부되도록 정합화한다.
- **추적 review ID:** `REV-058`.
- **시작 조건:** `P5-R7-GATE` 완료와 OpenAPI의 두 multipart request encoding 계약 존재.
- **완료 증거:** POST·PUT actual endpoint가 JSON part는 기존 strict parse·media/fixed/owner 의미를 유지하고,
`text/plain`·content type 누락 등은 localized 415 `ApiResponse.error`, 표준 `Accept` header,
S3·DB·event no-side-effect를 반환한다.
- **범위 밖:** JSON schema·strict reader·media/fixed/concurrency 의미, 댓글 endpoint, legacy/public endpoint,
OpenAPI·신규 dependency·DDL 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** POST·PUT의 유효 JSON 문자열을 `text/plain` request part로 보내 현재 handler에 진입하는지 actual
endpoint와 no-side-effect로 고정한다.
- [x] **GREEN:** v2 controller 경계에서 request part의 `application/json` 호환 여부만 확인하고 기존 String
strict reader에 동일 payload를 전달한다.
- [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, content type 누락과 정상 JSON,
필수 part 누락 400 및 기존 media/fixed/owner 회귀를 확인한다.
- [x] **REFACTOR:** community facade/domain 로직을 변경하지 않고 community/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
```bash
./gradlew test \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 5 multipart request part 계약 후속 Gate
**Goal 실행 `P5-R8-GATE`:** `REV-058` 수정 뒤 Community post POST·PUT의 part-level JSON-only·415 경계를 재검토한다.
- [x] **`P5-R8-GATE` 완료:** `P5-R8` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 5 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R8` 완료.
- **완료 증거:** 정상 JSON·미지원/누락 media type·필수 part·no-side-effect 및 media/fixed/owner 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 5 multipart part 이름 계약 후속 보완
- [x] **Task 5.15: 커뮤니티 생성·수정의 미정의 multipart part 거부**
**Goal 실행 `P5-R9`:** Community post 생성은 `audioFile`, `postImage`, `request`, 수정은
`postImage`, `request` 외 multipart part를 business mutation 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-063`.
- **시작 조건:** `P4-R8-GATE` 완료와 `phase5-community-review.md` 7차 정적 리뷰 판정 존재.
- **완료 증거:** POST·PUT 미정의 part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect,
수정의 `audioFile` 거부 및 정상 media/fixed/owner·필수 part·415 회귀.
- **범위 밖:** OpenAPI schema, media/fixed/concurrency 의미, 전역 multipart resolver, 댓글·legacy/public endpoint,
신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 생성·수정에 `unexpected` part를 추가하고, 수정에는 OpenAPI에 없는 `audioFile`을 추가해 현재
정상 mutation으로 진행되는 경로와 side effect를 actual endpoint로 고정한다.
- [x] **GREEN:** 실제 part 이름 집합을 생성 `{audioFile, postImage, request}`, 수정
`{postImage, request}`와 비교해 초과 이름을 `AiCharacterAdminApiException(HttpStatus.BAD_REQUEST,
"common.error.invalid_request")`로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 400 envelope, 정상 생성·수정, request part 누락·415,
media/fixed/owner 경계와 no-side-effect를 확인한다.
- [x] **REFACTOR:** Community controller/test만 최소 변경하고 community/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
#### Phase 5 multipart part 이름 계약 후속 Gate
**Goal 실행 `P5-R9-GATE`:** `REV-063` 수정 뒤 Community POST·PUT의 operation별 허용 part 이름과 기존
media type 경계를 재검토한다.
- [x] **`P5-R9-GATE` 완료:** `P5-R9` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 5
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R9` 완료.
- **완료 증거:** 미정의 part와 수정 `audioFile` 400/no-side-effect, 정상 media/fixed/owner·필수 part·415 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 5 multipart 일반 form-field part 후속 보완
- [x] **Task 5.16: 커뮤니티 생성·수정의 전체 multipart part 이름 검증**
**Goal 실행 `P5-R10`:** Community post POST·PUT에서 파일 part뿐 아니라 filename 없는 일반 form-field part를
포함한 모든 multipart part 이름을 검사해 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` 외
이름을 mutation 전에 400 `common.error.invalid_request`로 거부한다.
- **추적 review ID:** `REV-069`.
- **시작 조건:** `P4-R10-GATE` 완료와 `phase5-community-review.md` 8차 정적 리뷰 판정 존재.
- **완료 증거:** filename 없는 `unexpected` part의 KO/EN/JA 400 envelope와 S3·DB·event no-side-effect,
기존 파일형 미정의 part·수정 `audioFile` 거부·정상 media/fixed/owner·필수 part·request part 415 회귀 성공.
- **범위 밖:** OpenAPI schema, media/fixed/concurrency 의미, 전역 multipart resolver, legacy/public endpoint,
신규 dependency·DDL.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** filename 없는 `unexpected` 일반 form-field part를 정상 request와 함께 POST·PUT으로 보내 현재
`fileMap.keys` 검사를 우회하는 경로와 mutation 부작용을 actual endpoint로 고정한다.
- [x] **GREEN:** servlet request의 전체 part 이름 집합을 operation별 허용 집합과 비교해 초과 이름을 facade 진입
전에 공통 400으로 거부한다.
- [x] **CONTRACT TEST:** KO/EN/JA 오류 envelope, 기존 파일형 미정의 part, 수정 `audioFile` 거부,
정상 media/fixed/owner·필수 part·request part 415와 no-side-effect를 확인한다.
- [x] **REFACTOR:** Community controller/test만 최소 변경하고 community/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조와 `git diff --check`를 기록한다.
#### Phase 5 multipart 전체 part 이름 후속 Gate
**Goal 실행 `P5-R10-GATE`:** `REV-069` 수정 뒤 Community POST·PUT의 파일·일반 form-field를 포함한 전체 part
이름과 operation별 media type 경계를 재검토한다.
- [x] **`P5-R10-GATE` 완료:** `P5-R10` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고 Phase 5
리뷰와 Progress를 갱신한다.
- **시작 조건:** `P5-R10` 완료.
- **완료 증거:** filename 없는 미정의 part 400/no-side-effect와 기존 multipart 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest --tests '*shouldRejectFilenameLessUndefinedMultipartPartBeforeSideEffects*'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
- 검증 기록(RED): 무엇: Community POST·PUT filename 없는 `unexpected` multipart part. 왜: `fileMap.keys` 검사가 일반 form-field part를 보지 못하는지 고정하기 위해. 어떻게: Phase 2~5 multipart와 Phase 4 genre focused RED 묶음을 실행했다. 결과: 신규 multipart/genre 36건이 실패했다.
- 검증 기록(GREEN): 무엇: Community multipart 전체 part 이름 검사. 왜: 파일형 part와 일반 form-field part 모두 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` allow-list를 따라야 하기 때문이다. 어떻게: controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 검사하도록 바꾸고 같은 focused 묶음을 재실행했다. 결과: `BUILD SUCCESSFUL in 1m 17s`였다.
- 검증 기록(GATE): community/common 영향 범위 회귀, lint, diff 결과는 `P7-R10-GATE`에 통합 기록한다.
---
### Phase 6: FanTalk 목록·답변·팬 원글 삭제 vertical slice
#### 목표
선택한 AI 캐릭터의 FanTalk 목록을 관리자 전용 경계에서 조회하고 자신의 활성 root FanTalk에 creator reply를 작성하며
팬 작성 root를 soft delete하는 v2 관리자 API를 제공한다.
#### 범위와 비범위
- 포함: 관리자용 root FanTalk/creator reply 목록, root FanTalk 존재/활성/owner 검증, creator reply 저장, 팬 작성
root row soft delete, 언어 감지와 기존 응답 의미 parity.
- 제외: FanTalk 원글 작성, nested reply, hard delete·cascade, 구매/댓글형 기능, AI 캐릭터 일반 사용자 활동.
#### 선행 Phase 및 의존성
- Phase 1 target resolver.
- 기존 FanTalk 저장 엔티티와 응답 DTO 의미 특성화.
#### API endpoint와 request/response contract
- 정식 전체 schema는 `api-contract.openapi.json`의 FanTalk operation 3개를 따른다.
- `GET /api/v2/admin/ai-characters/{characterId}/fan-talks?page=0&size=20`
- Response: 공개 v2 `CreatorChannelFanTalkTabResponse`와 동일한
`fanTalkCount`, `fanTalks`, `page`, `size`, `hasNext` 및 nested root/reply 필드
- `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`
- Request: `CreateAiCharacterAdminFanTalkReplyRequest(content: String)`
- Response: `AiCharacterAdminFanTalkReplyResponse(fanTalkId, replyId, creatorMemberId, content, createdAtUtc)`
- `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`는 팬 작성 root만 soft delete하고
`data: null`을 반환한다.
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: 관리자 목록용 owner-scoped root/reply 조회와 root/active/creator owner 검증 adapter를 추가한다.
- Service: 관리자 목록은 공개 v2 DTO 형태로 조립하되 viewer/block 필터를 적용하지 않는다. reply는 target 검증 후 기존
저장/언어 감지 로직을 필요한 만큼 재사용한다.
#### DB migration
- 없음.
#### transaction과 concurrency 고려사항
- root FanTalk 조회와 reply 저장은 같은 transaction에서 수행한다.
- 중복 답변 허용 여부는 기존 domain 정책을 따른다. 기존 정책이 없다면 이번 API는 별도 중복 차단을 추가하지 않는다.
#### 보안 및 개인정보 위험
- 다른 character FanTalk, HUMAN creator FanTalk, inactive FanTalk, nested parent에는 답변하지 않는다.
- AI character Member 로그인/impersonation 없이 writer/creator만 해석된 creatorMember로 저장한다.
#### acceptance criteria
- target AI character는 자신의 활성 root FanTalk에만 답변할 수 있다.
- 관리자는 공개 v2와 동일한 필드 형태로 target AI character의 root FanTalk와 creator reply를 조회할 수 있다.
- cross-character, nested parent, inactive/missing FanTalk는 4xx이며 reply 저장과 이벤트 발행이 없다.
- 저장된 답변의 writer/creator는 해석된 creatorMember와 일관된다.
#### targeted test
- Characterization: `LegacyFanTalkReplyCharacterizationTest`.
- V2 RED/GREEN: `AiCharacterAdminFanTalkQueryTest`, `AiCharacterAdminFanTalkReplyServiceTest`.
- Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`
#### 전체 회귀 테스트 영향
- 기존 FanTalk 조회/작성 관련 테스트가 통과해야 한다.
#### rollback 전략
- 신규 FanTalk 목록·reply v2 admin route/facade를 제거한다.
- 신규 DDL이 없으므로 schema rollback은 없다.
#### 권장 commit 경계
- `feat: add ai character admin fan talk slice`
- [x] **Task 6.1: 기존 FanTalk reply 의미 특성화 baseline 고정**
**Goal 실행 `P6-T1`:** 기존 FanTalk의 root 판별, 언어 감지, 응답 의미와 writer/creator 저장 결과를 비교 기준으로 고정한다.
- **시작 조건:** `P5-GATE` 완료.
- **완료 증거:** production 변경 전 특성화 테스트 통과, 중복 답변 정책과 Progress 기록.
- **범위 밖:** 신규 v2 reply production code 구현.
**Files:**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/LegacyFanTalkReplyCharacterizationTest.kt`
- [x] valid root reply의 언어 감지, response 의미와 writer/creator 저장 baseline을 작성한다.
- [x] root/nested/active 판별과 기존 중복 답변 정책을 관찰해 기록한다.
- [x] Phase 6 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
- [x] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
- [x] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- **관찰된 legacy baseline:** `ExplorerService.writeCheers`는 조회된 parent가 root인지 또는 active인지 검증하지 않아
inactive nested parent에도 답변을 연결한다. 조회 결과가 없는 parent ID는 parent 없이 root 글로 저장하고, 같은 root에 대한
creator 답변 중복도 허용한다. `languageCode`가 blank일 때만 `CREATOR_CHEERS` 언어 감지 이벤트를 발행한다. creator 조회
실패는 `SodaException(messageKey = "member.validation.user_not_found")`, 차단은
`explorer.creator.blocked_cheers`로 조립된 `SodaException.message`를 반환한다. 이 service 경계는 HTTP status를 결정하지 않는다.
- **Phase 6 v2 domain/client 오류 결정:** target 또는 FanTalk missing/inactive/cross-character/nested parent, 빈 content와
request binding 실패는 저장·이벤트 전에 400 `common.error.invalid_request`로 거부한다. KO `잘못된 요청입니다.`, EN
`Invalid request.`, JA `無効なリクエストです`를 반환한다. 예상하지 못한 server/infrastructure 오류는 500
`common.error.unknown`과 공통 KO/EN/JA message를 사용한다. legacy의 root 전환·nested/inactive 허용과 HTTP status
미결정은 신규 v2에 복제하지 않는다.
- **TDD 예외/특성화 (2026-07-28):** `LegacyFanTalkReplyCharacterizationTest`를 production 변경 없이 추가한 뒤
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`를
실행했다. valid root reply의 parent/member/creator/response/event, missing parent root 전환과 nonblank 언어 이벤트 미발행,
inactive nested parent 허용, 중복 답변 허용, missing creator key 및 blocked creator message 6건이 첫 실행부터 통과했다.
기존 구현의 의도된 동작을 고정하는 특성화이므로 production 코드나 dependency를 추가하지 않았다.
- **검증 기록:** 무엇: legacy FanTalk reply 의미 특성화와 Kotlin lint. 왜: Phase 6 v2 저장 전에 재사용할 저장·응답·이벤트
동작과 새 경계에서 차단할 legacy 허용 범위를 분리하기 위해. 어떻게: 위 focused test 명령과 `./gradlew ktlintCheck`를 실행했다.
결과: focused test는 `BUILD SUCCESSFUL in 9s`(10 actionable tasks 중 3 executed, 7 up-to-date)였다. 첫 lint 실행은 새 test의
import 정렬 1건으로 `BUILD FAILED in 22s`였고, import만 정렬한 뒤 재실행한 `ktlintCheck`는
`BUILD SUCCESSFUL in 26s`(7 actionable tasks 중 2 executed, 5 up-to-date)였다. production 변경이 없고 focused test가
직접 legacy service 경계를 실행하므로 전체 `./gradlew test`는 실행하지 않았다.
- [x] **Task 6.2: FanTalk 관리자 목록 조회 구현**
**Goal 실행 `P6-T2`:** 선택한 AI 캐릭터의 root FanTalk와 creator reply를 공개 v2 응답 필드 형태로 조회한다.
- **시작 조건:** `P6-T1` 완료와 Phase 6 오류 계약의 계획 반영.
- **완료 증거:** owner-scoped 목록·nested reply·pagination exact JSON RED/GREEN과 Progress 기록.
- **범위 밖:** 공개 v2 endpoint 변경, viewer/block 필터 재사용, FanTalk 원글 작성.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDto.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkQueryTest.kt`
- [x] **RED:** `fanTalkCount/fanTalks/page/size/hasNext`와 root/reply 전체 필드, target owner, pagination 실패
test를 작성한다.
- [x] **GREEN:** 공개 v2 DTO 필드 형태를 유지하면서 관리자 target 정책으로 조회하는 최소 구현을 통과시킨다.
- [x] **REFACTOR:** 공개 v2 endpoint 계약 불변, viewer/block 필터 비적용과 ADMIN 인가를 회귀하고 기록한다.
- [x] **Task 6.3: FanTalk root reply 저장 구현**
**Goal 실행 `P6-T3`:** 선택한 AI 캐릭터의 활성 root FanTalk에 creator reply를 저장하고 전용 응답을 반환한다.
- **시작 조건:** `P6-T1`, `P6-T2` 완료와 Phase 6 오류 계약의 계획 반영.
- **완료 증거:** 정상 저장·언어 감지·DTO·writer/creator RED/GREEN과 Progress 기록.
- **범위 밖:** FanTalk 원글, nested reply, 일반 사용자 대리 작성.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyDto.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyFacade.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyCreateTest.kt`
- [x] 미구현 정상 root reply, 언어 감지와 응답 DTO 실패 test를 작성한다.
- [x] root 조회와 reply 저장을 같은 transaction에서 수행하는 최소 구현을 통과시킨다.
- [x] writer/creator가 resolver의 creatorMember와 일치하고 principal impersonation이 없음을 검증한다.
- [x] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [x] **Task 6.4: FanTalk target·root·ownership 거부 구현**
**Goal 실행 `P6-T4`:** cross-character, nested, inactive, missing FanTalk를 저장·이벤트 없이 거부한다.
- **시작 조건:** `P6-T1`~`P6-T3` 완료.
- **완료 증거:** 네 거부 분기의 정확한 오류 계약, DB/event 0건과 Progress 기록.
- **범위 밖:** 새로운 중복 답변 차단 정책.
**Files:**
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyOwnershipTest.kt`
- [x] cross-character/nested/inactive/missing FanTalk과 inactive target 거부 테스트를 작성하고 현재 guard 동작을 특성화했다.
- [x] 기존 `resolveActiveTarget`과 owner-scoped `findActiveRoot`가 저장 전에 target·root·active·owner를 검증함을 확인했다.
- [x] 각 실패의 정확한 400 `common.error.invalid_request` KO·EN·JA envelope와 reply insert/event 0건을 검증했다.
- [x] focused, reply create, legacy characterization, FanTalk package test와 `ktlintCheck` 결과를 Progress에 기록했다.
- **TDD 예외/특성화 (2026-07-28):** production 변경 전 `AiCharacterAdminFanTalkReplyOwnershipTest`를 추가해 처음 실행했으나,
cross-character root, nested parent, inactive root, missing FanTalk, inactive target의 KO/EN/JA 400 envelope와 reply row/event
무변경이 모두 통과했다. 이는 P6-T3의 `resolveActiveTarget`과 `findActiveRoot(creatorMemberId, fanTalkId)`가 이미 target active,
owner, active, root 조건을 저장·이벤트 전에 보장하기 때문이다. 요구 동작이 충족된 특성화이므로 production code, dependency,
legacy/public endpoint, 중복 답변 정책을 변경하지 않았다.
- [x] **Task 6.5: Phase 6 보안·오류·회귀 검증**
**Goal 실행 `P6-T5`:** FanTalk 목록·reply endpoint의 ADMIN·오류 계약과 기존 FanTalk 회귀를 고정한다.
- **시작 조건:** `P6-T2`~`P6-T4` 완료.
- **완료 증거:** 권한 매트릭스, request binding/domain 오류, legacy 회귀와 Progress 기록.
- **범위 밖:** Phase 7 외 전체 기능 수정.
**Files:**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- [x] endpoint의 ADMIN 이중 인가와 stale claim을 검증한다.
- [x] 빈 content/binding/domain 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
- [x] 기존 FanTalk 조회·작성 계약과 AI 로그인/token/impersonation 부재를 확인한다.
- [x] Phase 6 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 검증 기록(RED): 무엇: 빈 문자열·공백 reply content의 저장 전 거부와 malformed/missing JSON binding envelope. 왜: 기존 `createReply`가
빈 content를 저장해 Phase 6 오류 계약을 위반했기 때문이다. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest --rerun-tasks`를
실행했다. 결과: 6개 테스트 중 빈 문자열·공백 content의 KO/EN/JA 3개가 400 기대 대비 200으로 실패해 `BUILD FAILED in 5m 42s`였다.
- 검증 기록(GREEN/REFACTOR): 무엇: `AiCharacterAdminFanTalkReplyContractTest`의 빈/공백 content, malformed/missing body와
content binding KO/EN/JA `ApiResponse.error` envelope, 실제 FanTalk list/reply의 JWT 비ADMIN·stale ADMIN claim, 기존
query/write/legacy 회귀. 왜: 신규 관리자 endpoint의 이중 인가와 오류·저장 의미를 함께 고정하기 위해. 어떻게:
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest`,
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`,
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`, `./gradlew ktlintCheck`를 순차 실행했다.
결과: contract는 `BUILD SUCCESSFUL in 53s`, authorization은 `BUILD SUCCESSFUL in 46s`, FanTalk package는
`BUILD SUCCESSFUL in 50s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`였다. 첫 `ktlintCheck`는 새 contract test의
unused import 1건으로 `BUILD FAILED in 34s`였고 import 제거 뒤 재실행했다. 기존
`AiCharacterAdminFanTalkReplyCreateTest`는 관리자 principal이 아닌 target `creatorMember`를 reply의 writer·creator로
저장하고 admin과 다름을 단언하므로 AI 로그인/token/impersonation 부재를 중복 없이 확인했다.
#### Phase 6 Gate
**Goal 실행 `P6-GATE`:** Phase 6 FanTalk 목록·reply의 조회·root·ownership·저장·회귀 품질을 최종 판정한다.
- [x] **`P6-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P6-T1`~`P6-T5` 완료.
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와
`./gradlew ktlintCheck` 성공, Progress 기록.
- **범위 밖:** 실패와 무관한 신규 기능.
#### Phase 6 후속 리뷰 보완
- [x] **Task 6.6: FanTalk query policy와 reply JSON 경계를 OpenAPI에 정합화**
**Goal 실행 `P6-R1`:** `REV-027`의 목록 pagination을 공개 v2와 같은 보정 정책으로 복구하고,
`REV-028`의 reply request에서 `additionalProperties: false`를 실제로 강제한다.
- **추적 review ID:** `REV-027`, `REV-028`.
- **시작 조건:** `P5-R1-GATE` 완료와 `phase6-fantalk-review.md` 판정 존재.
- **완료 증거:** `page < 0 -> 0`, `size < 20 -> 20`, `size > 50 -> 50` actual endpoint 테스트,
reply 미지 필드 400/no insert/no event 테스트, FanTalk/public policy 정적 대조와 Progress 기록.
- **범위 밖:** 공개 v2 FanTalk query policy 변경, reply 저장/언어 감지/ownership 정책 변경, OpenAPI schema 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkQueryTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/domain/CreatorChannelFanTalkQueryPolicy.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 음수 page, 20 미만 size, 50 초과 size가 400이 아니라 각각 0/20/50으로 보정되는 actual 목록 테스트를
작성한다.
- [x] **RED:** reply JSON에 계약 밖 필드가 있으면 400 `common.error.invalid_request`이고 reply insert/event가
0회인지 확인한다.
- [x] **GREEN:** 관리자 목록에 공개 v2와 동일한 pagination 정규화를 적용하고 reply body만 strict reader로 역직렬화한다.
- [x] **REFACTOR:** 중복 정책은 기존 query policy의 가시성과 의존 방향을 확인한 뒤 최소한으로 재사용하고,
FanTalk/common 영향 범위 회귀, `ktlintCheck`, diff check를 실행한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkQueryTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest
./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
./gradlew ktlintCheck
git diff --check
```
#### Phase 6 후속 리뷰 Gate
**Goal 실행 `P6-R1-GATE`:** `REV-027`~`REV-028` 수정 뒤 FanTalk 2개 operation의 query/body 경계를 재검토한다.
- [x] **`P6-R1-GATE` 완료:** `P6-R1` 완료 후 actual endpoint no-side-effect와 FanTalk/common 회귀,
lint·diff를 fresh 실행하고 리뷰 문서와 Progress를 갱신한다.
- **시작 조건:** `P6-R1` 완료.
- **완료 증거:** 공개 v2 pagination 보정과 관리자 목록 일치, reply 미지 필드 거부, 영향 범위 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 6 후속 기능 보완
- [x] **Task 6.7: 팬 작성 FanTalk 원글 soft delete**
**Goal 실행 `P6-R2`:** target AI 캐릭터 채널에 팬이 작성한 FanTalk root를 row 단위로 soft delete하고 연결된 creator
reply row는 변경하지 않는다.
- **추적 review ID:** `REV-049`.
- **시작 조건:** `P5-R5-GATE` 완료와 PRD·OpenAPI의 승인된 FanTalk 삭제 계약 존재.
- **완료 증거:** 팬 작성 활성 root 삭제, 목록·count 제외, creator reply row 유지, already inactive no-op,
creator 작성/root 아닌 row/cross-character no mutation과 Progress 기록.
- **범위 밖:** 캐릭터 직접 댓글 삭제, FanTalk hard delete·cascade, 팬 작성 여부 재정의, 공개 v2 endpoint 변경.
**Interfaces:**
- `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`
- Request body 없음; response `ApiResponse.ok(null)`.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDeleteTest.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** target 채널의 팬 작성 활성 root 삭제가 root `isActive=false`, creator reply row 불변,
목록·`fanTalkCount` 제외, `data: null`인지 actual DELETE/GET으로 고정한다.
- [x] **RED:** target AI가 작성한 row, reply row, 다른 채널 root는 400/no mutation이며 같은 target의 이미 비활성인
팬 root는 200 no-op인지 고정한다.
- [x] **GREEN:** repository가 target creator, root, fan writer를 함께 식별하고 facade는 활성 row만
`isActive=false`로 변경한다. reply collection과 row는 수정하지 않는다.
- [x] **CONTRACT TEST:** body 없는 DELETE, ADMIN 이중 인가, target inactive/invalid ID와 공통 오류 envelope를
확인한다.
- [x] **REFACTOR:** 기존 `CreatorCheers` soft-delete field와 현재 repository만 사용하고 FanTalk/common 영향 범위
회귀, `ktlintCheck`, OpenAPI 상태, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkDeleteTest
./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
./gradlew ktlintCheck
git diff --check
```
#### Phase 6 후속 기능 Gate
**Goal 실행 `P6-R2-GATE`:** `REV-049`의 target root·fan writer·row-only soft delete 경계를 재검토한다.
- [x] **`P6-R2-GATE` 완료:** `P6-R2` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 6 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P6-R2` 완료.
- **완료 증거:** 팬 root만 삭제, creator reply row 유지, idempotent delete, cross-target no-side-effect와
OpenAPI operation `implemented`.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 6 JSON media type 계약 후속 보완
- [x] **Task 6.8: FanTalk 답변 작성의 application/json 강제**
**Goal 실행 `P6-R3`:** FanTalk 답변 작성 endpoint가 OpenAPI의 유일한 request media type인
`application/json`만 받고, 그 밖의 media type은 공통 415 계약으로 거부하도록 정합화한다.
- **추적 review ID:** `REV-054`.
- **시작 조건:** `P5-R8-GATE` 완료와 OpenAPI의 reply JSON requestBody 및 415 response 계약 존재.
- **완료 증거:** actual POST가 정상 JSON은 기존 축약 응답·저장·event 의미를 유지하고 `text/plain` 등 미지원
media type은 localized 415 `ApiResponse.error`, 표준 `Accept` header, reply insert/event no-side-effect를 반환한다.
- **범위 밖:** reply JSON schema·strict parser·root/ownership·언어 감지, FanTalk 목록/삭제,
legacy/public endpoint, 신규 dependency·DDL 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** 유효 reply JSON 문자열을 `text/plain`으로 보내면 현재 415가 아닌 handler 진입 결과가 나오는지
actual endpoint와 insert/event no-side-effect로 고정한다.
- [x] **GREEN:** reply POST mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`만 추가한다.
- [x] **CONTRACT TEST:** KO/EN/JA 415 envelope, `Accept` header, reply insert/event 0회와 정상 JSON 축약 응답을
확인한다.
- [x] **REFACTOR:** facade/parser와 FanTalk 도메인 동작을 변경하지 않고 FanTalk/common 영향 범위 회귀,
`ktlintCheck`, OpenAPI 정적 대조, `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 6 JSON media type 계약 후속 Gate
**Goal 실행 `P6-R3-GATE`:** `REV-054` 수정 뒤 reply POST의 JSON-only·415·no-side-effect 경계를 재검토한다.
- [x] **`P6-R3-GATE` 완료:** `P6-R3` 완료 후 focused/영향 범위 회귀와 lint·diff를 fresh 실행하고
Phase 6 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P6-R3` 완료.
- **완료 증거:** 정상 JSON과 미지원 media type 415/header/envelope/no-side-effect 회귀 성공.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 6 FanTalk 답변 수정 후속 기능
- [x] **Task 6.9: 레거시 계약을 유지하는 FanTalk 답변 수정 API**
**Goal 실행 `P6-R4`:**
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`에서 target AI가 작성하고
target의 활성 root에 직접 연결된 reply만 레거시 `PUT /explorer/profile/cheers` 의미로 수정한다.
- **추적 review ID:** `REV-059`.
- **시작 조건:** `P6-R3-GATE` 완료와 PRD·OpenAPI 2.3.0의 확정 답변 수정 계약 존재.
- **완료 증거:** optional/nullable `content`·`isActive`, 동시 입력, 빈 객체 no-op, 비활성 reply 재활성화,
target/root/direct-reply ownership, JSON-only·strict body, ADMIN/common 오류, 레거시 성공 `data`, no-event를 actual
endpoint와 영향 범위 회귀로 확인하고 OpenAPI 상태를 `implemented`로 갱신.
- **범위 밖:** FanTalk 원글 작성, nested reply 작성, 별도 reply DELETE/hard delete, root cascade, 언어 감지·
`languageCode` 변경, legacy/public endpoint, 신규 dependency·DDL 변경.
**Interfaces:**
- `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`
- Request JSON: `content?: string | null`, `isActive?: boolean | null`; 두 필드 동시 입력 허용, `{}`와 explicit null은
성공 no-op, 미지 필드는 400.
- Response: `ApiResponse<CreatorChannelFanTalkResponse>`. `data.fanTalkId`는 수정한 `replyId`,
`creatorReplies`는 빈 배열.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyUpdateTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyUpdateContractTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **RED:** content-only, isActive-only, 두 필드 동시 수정, `{}`·explicit null no-op과 응답의 reply ID·빈
`creatorReplies`를 actual PUT으로 고정한다. 빈 문자열·공백 content도 레거시처럼 non-null 값으로 반영되는지 포함한다.
- [x] **RED:** 비활성 reply의 `isActive=true` 재활성화와 content 수정을 허용하되, 비활성 root, 다른
character/root의 reply, 팬 작성 row, root row, 잘못된 direct-parent 관계는 400/no mutation인지 고정한다.
- [x] **RED:** malformed JSON, 미지 필드, 미지원 media type, JWT/DB role 조합과 KO/EN/JA 오류에서 DB/event
no-side-effect와 415 `Accept` header를 확인한다.
- [x] **GREEN:** strict request reader와 JSON `consumes`를 사용하고, repository가 reply ID·target
creator/writer·path root ID·활성 root·root parent null을 한 query 경계에서 검증한다. reply의 `isActive`는 조회 조건에
넣지 않는다.
- [x] **GREEN:** facade는 non-null `content`와 `isActive`만 entity에 반영하고 `languageCode`와 event를 건드리지 않는다.
응답은 기존 `CreatorChannelFanTalkResponse.from(reply, cloudFrontHost)`를 재사용한다.
- [x] **REFACTOR:** 신규 응답 DTO·dependency·DDL·추상화를 만들지 않고 FanTalk/common·legacy modifyCheers
영향 범위 회귀, `ktlintCheck`, OpenAPI 37개 상태와 `git diff --check`를 기록한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyUpdateTest
./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 \
--tests kr.co.vividnext.sodalive.explorer.ExplorerServiceTest
./gradlew ktlintCheck
git diff --check
```
#### Phase 6 FanTalk 답변 수정 후속 Gate
**Goal 실행 `P6-R4-GATE`:** `REV-059` 구현 뒤 레거시 field/state/response parity와 관리자 target/root/reply 경계를
재검토한다.
- [x] **`P6-R4-GATE` 완료:** `P6-R4` 완료 후 focused/영향 범위 회귀와 OpenAPI·lint·diff를 fresh 실행하고
Phase 6 리뷰와 Progress를 갱신한다.
- **시작 조건:** `P6-R4` 완료.
- **완료 증거:** 답변 수정 정상·no-op·재활성화·cross-target no-side-effect, ADMIN/common 오류,
37번째 controller mapping과 `implemented` 상태 일치.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 6 FanTalk 비활성 root 삭제 문서 계약 후속 보완
- [x] **Task 6.10: FanTalk 비활성 root 삭제의 no-op 문서 정합화**
**Goal 실행 `P6-R5`:** `DELETE /fan-talks/{fanTalkId}`의 동일 target·팬 작성 root가 이미 비활성인 경우 성공
no-op이라는 OpenAPI·구현·회귀 테스트의 현재 계약에 맞춰 PRD와 `api-contract.md`의 상충 설명을 동기화한다.
- **추적 review ID:** `REV-070`.
- **시작 조건:** `phase6-fantalk-review.md` 8차 정적 리뷰 판정과 OpenAPI/구현/test의 동일한 no-op 근거 존재.
- **완료 증거:** PRD Edge Cases와 `api-contract.md`의 FanTalk delete 설명이 동일 target 비활성 팬 root 200
`data: null` no-op, 미존재·다른 target·creator root·reply 400으로 일치하고 OpenAPI diff는 없음.
- **범위 밖:** runtime/controller/facade/repository/test 변경, OpenAPI schema/path/status 변경, reply 삭제 의미,
legacy/public endpoint.
- **결정 근거:** OpenAPI를 기계 계약 원본으로 두고 현재 구현·회귀와 일치하는 no-op을 유지한다. PRD의 400 문장이
최신 제품 의도라면 이 Goal을 시작하지 않고 OpenAPI·runtime/test 변경 범위를 먼저 재확정한다.
- **TDD 예외 사유:** 실행 동작을 변경하지 않고 상충하는 설명 문서만 현재 기계 계약과 구현 증거에 맞추는 문서 Task다.
- **대체 검증 방법:** OpenAPI DELETE description, facade/repository 분기,
`shouldNoopInactiveFanRootInSameTarget` 테스트 소스와 두 설명 문서를 정적으로 대조한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDeleteTest.kt`
- [x] **DOCUMENT:** PRD와 `api-contract.md`의 상충 문장을 OpenAPI·runtime의 비활성 동일-target 팬 root 성공
no-op 계약으로 최소 수정한다.
- [x] **STATIC:** 미존재·다른 target·creator root·reply는 400이라는 구분과 root row-only soft delete 의미가
유지되는지 대조한다.
- [x] **SCOPE:** production/test/OpenAPI diff가 없고 문서 링크·용어·상태가 일치하는지 `git diff --check`와
정적 검색으로 확인한다.
#### Phase 6 FanTalk 삭제 문서 계약 후속 Gate
**Goal 실행 `P6-R5-GATE`:** `REV-070` 수정 뒤 PRD·plan·OpenAPI·계약 설명·구현 증거의 FanTalk 삭제 의미를
재검토한다.
- [x] **`P6-R5-GATE` 완료:** `P6-R5` 완료 후 정적 대조와 diff check를 fresh 실행하고 Phase 6 리뷰와 Progress를
갱신한다.
- **시작 조건:** `P6-R5` 완료.
- **완료 증거:** 비활성 동일-target 팬 root no-op와 나머지 거부 경계가 모든 규범 문서에서 일치.
- **범위 밖:** Gate에서 production/test/OpenAPI 변경.
```bash
rg -n '비활성|no-op|FanTalk.*삭제' \
docs/20260724_AI캐릭터_관리자_API/prd.md \
docs/20260724_AI캐릭터_관리자_API/api-contract.md \
docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
./gradlew tasks --all
git diff --check
```
- 검증 기록(DOCUMENT): 무엇: FanTalk 비활성 root 삭제의 no-op 문서 정합화. 왜: OpenAPI·구현·회귀는 같은 target 비활성 팬 root 삭제를 200 `data:null` no-op으로 고정하지만 PRD 일부 문장이 400 거부로 설명했기 때문이다. 어떻게: PRD와 `api-contract.md`의 삭제 설명을 같은 target 비활성 팬 root no-op, creator root·reply·다른 target·미존재 root 400으로 동기화했다. 결과: runtime/test/OpenAPI 변경 없이 설명 문서만 갱신했다.
- 검증 기록(GATE): 정적 대조와 diff check 결과는 `P7-R10-GATE`에 통합 기록한다.
---
### Phase 7: Final Integration & Quality Gate
#### 목표
신규 AI 캐릭터 관리자 API 전체가 source spec을 충족하고 기존 legacy/public 계약을 회귀시키지 않았음을 검증한다.
#### 범위와 비범위
- 포함: 전체 targeted test, 기존 회귀 테스트, ktlint, dependency/DDL/API contract 점검, 문서 검증 기록 누적.
- 제외: 신규 기능 추가, unrelated refactor.
#### 선행 Phase 및 의존성
- Phase 1~6 완료.
#### API endpoint와 request/response contract
- `api-contract.openapi.json`의 37개 operation이 모두 구현되어야 한다.
- 모든 신규 endpoint가 JWT ADMIN + 현재 DB ADMIN 이중 인가와 공통 오류 envelope/i18n을 공유해야 한다.
- legacy/public endpoint URI와 성공·오류 status/body/message diff가 없어야 한다.
#### entity, repository, service 변경
- 신규 변경 없음. Phase 1~6 변경의 누락 import, unused code, package 의존 방향만 정리한다.
- 신규 v2 application/domain이 기존 controller 또는 v2 API response DTO를 역참조하지 않는지 점검한다.
#### DB migration
- 없음. 새 DDL/migration 파일이 없는지 확인한다.
#### transaction과 concurrency 고려사항
- 각 write slice의 transaction 시작부 target/ownership 검증이 유지되는지 점검한다.
- 동시성 관련 targeted test가 실패 없이 통과해야 한다.
#### 보안 및 개인정보 위험
- JWT 또는 현재 DB role이 비ADMIN인 접근, stale ADMIN claim, AI login/token/impersonation, private path 노출, cross-owner write가
없는지 전체 점검한다.
#### acceptance criteria
- Phase별 targeted test가 모두 통과한다.
- 모든 신규 endpoint에서 stale ADMIN claim은 403이고, 등록된 모든 API 오류 분기는 정확한 비2xx status +
`ApiResponse.error` + KO/EN/JA message를 반환한다. Spring CORS 정책 거부 403 body는 envelope 계약의 예외다.
- legacy/public 401/403/domain 오류의 status/body/message가 특성화 baseline과 동일하다.
- 전체 회귀 필요성 판정 결과 실행 대상이면 `./gradlew test`가 통과하고, 생략 대상이면 근거와 대체 targeted/영향 범위 회귀
결과가 기록된다.
- Kotlin 파일 변경이 있으면 `./gradlew ktlintCheck`가 통과한다.
- 신규 dependency와 신규 DDL이 없다.
- source spec acceptance criteria 25개를 각 Phase 결과와 대조해 누락이 없다.
#### targeted test
- Run:
```bash
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
./gradlew ktlintCheck
```
- Conditional full regression: 공통 경계·여러 Phase 영향 또는 targeted 결과로 영향 범위를 판단할 수 없을 때만
`./gradlew test`를 추가 실행한다.
#### 전체 회귀 테스트 영향
- legacy `/admin/*`, `/creator-admin/*`, public `/api/v2/*` 관련 테스트 전체가 회귀 범위다.
- 기존 성공·오류 request/response 계약 변경이 없음을 controller/DTO diff와 legacy 오류 특성화 테스트로 확인한다.
#### rollback 전략
- 신규 `/api/v2/admin/ai-characters` controller bean 비활성화 또는 신규 package 제거로 기능 표면을 되돌린다.
- DB schema 변경이 없으므로 rollback은 code revert 중심이다.
#### 권장 commit 경계
- `test: verify ai character admin api integration`
- [x] **Task 7.1: 전체 targeted 및 필요 시 전체 회귀 test 실행**
- **Goal 실행 `P7-T1`:** Phase 1~6 targeted test를 실행하고 위험 근거에 따라 전체 회귀 필요성을 판정해 결과를 확정한다.
- **시작 조건:** `P2-GATE`~`P6-GATE` 완료와 Phase 1 완료 증거 확인.
- **완료 증거:** targeted 결과와 전체 회귀 실행 또는 생략 판정·근거를 Progress와 하단 검증 기록에 누적.
- **범위 밖:** 실패와 무관한 기능 추가·리팩터링.
- TDD 예외 사유: 구현 완료 후 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다.
- 대체 검증 방법: Phase 1~6 targeted test, `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, 기존 admin
security test를 먼저 실행한다. 공통 경계·여러 Phase 영향 또는 targeted 실패로 영향 범위를 판단할 수 없을 때만 전체 회귀를 실행한다.
- REFACTOR: 실패가 있으면 관련 Phase Task로 되돌려 최소 수정 후 다시 실행한다.
- Verify: 위 targeted command를 실행하고 전체 회귀 필요성을 판정한다. 실행 시 `./gradlew test` 결과를, 생략 시 근거와 대체
회귀 범위를 이 문서 하단 검증 기록에 남긴다.
- [x] Phase 1~6 targeted test를 실행하고 각 결과를 기록한다.
- [x] 공통 경계·여러 Phase 변경과 targeted 결과를 근거로 전체 회귀 필요성을 판정한다.
- [x] 필요하면 `./gradlew test`의 exit code·실패 수를 기록하고, 불필요하면 생략 근거와 대체 회귀 범위를 기록한다.
- [x] 실패가 있으면 소유 Phase에 별도 회귀 수정 Goal을 추가하고 `P7-T1`을 완료 처리하지 않는다.
- [x] **Task 7.2: API contract와 변경 범위 점검**
- **Goal 실행 `P7-T2`:** API·architecture·dependency·DDL·diff와 문서 추적성을 read-only로 최종 점검한다.
- **시작 조건:** `P7-T1` 완료.
- **완료 증거:** 아래 점검 체크박스, `ktlintCheck`, source spec acceptance criteria 추적 결과와 Progress 기록.
- **범위 밖:** 신규 기능 추가, 확정되지 않은 계약 보정.
- TDD 예외 사유: diff/architecture 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다.
- 대체 검증 방법: legacy/public 성공·오류 status/body/message 특성화 baseline 통과, controller/DTO schema 변경 없음, 신규
dependency 없음, 신규 DDL 없음, 신규 v2 application/domain에서 기존 controller와 v2 response DTO 역참조 없음.
- REFACTOR: 불필요한 import, 역방향 의존, 관련 없는 변경을 제거하고 diff를 다시 확인한다.
- Verify: `git diff --name-only`, `./gradlew ktlintCheck`
- [x] `git diff --name-only`와 `git diff --check`로 변경 범위와 문서/코드 오류를 확인한다.
- [x] `build.gradle.kts`와 migration/DDL 경로를 확인해 신규 dependency·DDL 0건을 기록한다.
- [x] legacy/public controller·DTO 외부 계약 diff와 신규 application/domain의 역방향 import 0건을 확인한다.
- [x] source spec acceptance criteria 25개를 Phase Goal/Gate 완료 증거에 대조한다.
- [x] `./gradlew ktlintCheck`와 `./gradlew tasks --all` 결과를 기록한다.
#### Phase 7 Gate
**Goal 실행 `P7-GATE`:** 모든 Phase의 완료 증거와 최신 전체 검증을 대조해 AI 캐릭터 관리자 API의 최종 완료 여부를 판정한다.
- [x] **`P7-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 문서 상태를 `구현 완료`로 갱신한다.
- **시작 조건:** `P7-T1`, `P7-T2` 완료.
- **완료 증거:** 미완료 Goal·미처리 review finding·보류 없는 차단 사항 0건, 아래 완료 조건과 최종 Progress 기록.
- **범위 밖:** Gate에서 직접 production code 수정, test 삭제·skip·완화.
- [x] Phase 1~6의 Task/Gate 완료 증거와 하단 검증 기록이 일치한다.
- [x] 모든 확정 review finding이 수정 완료 또는 근거 있는 제외로 종결됐다.
- [x] 최신 targeted·ktlint·문서 명령이 성공했고, 전체 회귀는 필요성 판정에 따라 성공 결과 또는 생략 근거가 기록됐다.
- [x] 남은 항목과 최종 상태를 Progress 및 최종 보고 형식으로 기록한다.
#### Phase 7 후속 리뷰 보완
- [x] **Task 7.3: 구현 현황 문서와 23개 operation metadata 동기화**
**Goal 실행 `P7-R1`:** `REV-029`의 계획·계약 설명·OpenAPI 구현 상태 metadata를 모든 Phase 후속 Gate가 완료된 실제
23개 controller mapping과 동기화한다.
- **추적 review ID:** `REV-029`.
- **시작 조건:** `P2-R6-GATE`, `P3-R9-GATE`, `P4-R1-GATE`, `P5-R1-GATE`, `P6-R1-GATE` 완료.
- **완료 증거:** plan 상태표/Endpoint Contract Summary와 `api-contract.md`가 23개 구현 완료를 표시하고,
OpenAPI 23개 operation의 `x-implementation-status`가 모두 `implemented`이며 controller mapping도 정확히 23개인 정적
대조, OpenAPI validate/client 생성과 Progress 기록.
- **범위 밖:** path/request/response schema 변경, operation 추가·삭제, production 코드 변경, 과거 완료 기록 삭제.
- **TDD 예외 사유:** 실행 코드를 변경하지 않는 구현 현황·계약 metadata 문서 정합성 Task다.
- **대체 검증 방법:** OpenAPI operation/status 개수와 controller mapping을 기계 집계하고 validator/client generator로
schema 비변경을 확인한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/**/*Controller.kt`
- [x] plan 현재 상태와 Endpoint Contract Summary를 Phase 2~6 후속 Gate 완료 상태로 동기화한다.
- [x] `api-contract.md`의 구현/정합화/예정 operation 수를 실제 23개 구현 완료 상태로 동기화한다.
- [x] OpenAPI 23개 operation의 `x-implementation-status`를 `implemented`로 바꾸고 operation/status 개수를 단언한다.
- [x] 신규 prefix controller mapping이 OpenAPI와 정확히 23개로 일치하고 계약 밖 route가 0개인지 대조한다.
- [x] OpenAPI validate, TypeScript client 생성/compile, `./gradlew tasks --all`, diff check 결과를 Progress에 기록한다.
```bash
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 23
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @openapitools/openapi-generator-cli validate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @openapitools/openapi-generator-cli generate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json \
-g typescript-fetch \
-o /tmp/ai-character-admin-typescript-client
./gradlew tasks --all
git diff --check
```
#### Phase 7 후속 리뷰 Gate
**Goal 실행 `P7-R1-GATE`:** 모든 후속 리뷰 finding과 23개 operation 구현 현황 문서가 종결됐는지 최종 판정한다.
- [x] **`P7-R1-GATE` 완료:** `P7-R1` 완료 후 Phase 2~6 후속 Gate와 문서/OpenAPI/controller 집계를 fresh 대조하고
상태를 `구현 완료`로 갱신한다.
- **시작 조건:** `P7-R1` 완료.
- **완료 증거:** `REV-021`~`REV-029` 처리 완료, 미완료 Goal 0건, 23개 operation 문서/metadata/mapping 일치,
validator/client/문서 명령과 diff check 성공.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
#### Phase 7 2차 통합 재판정
- [x] **Task 7.4: Phase 3~5 후속 보완 통합 검증**
**Goal 실행 `P7-R2`:** `REV-030`~`REV-033` 수정 후 23개 operation과 공통 오류·ownership·동시성 계약을
다시 통합 검증한다.
- **시작 조건:** `P3-R10-GATE`, `P4-R2-GATE`, `P5-R2-GATE` 완료.
- **완료 증거:** 네 review ID 처리 완료, Phase 1~6 targeted와 전체 회귀, `ktlintCheck`, OpenAPI/controller 집계,
dependency/DDL/diff 점검과 Progress 기록.
- **범위 밖:** 신규 기능·operation/schema 추가, 완료된 Phase 1·2·6 동작 변경, unrelated refactor.
- **TDD 예외 사유:** 여러 Phase의 회귀 수정 후 검증·판정 전용 Task이며 별도 production 동작을 추가하지 않는다.
- **대체 검증 방법:** 각 소유 Phase Gate의 RED/GREEN 증거를 재사용하지 않고 통합 명령을 fresh 실행한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] `REV-030`~`REV-033`이 모두 처리 완료이고 미완료 소유 Goal이 없는지 확인한다.
- [x] Phase 1~6 targeted 명령과 `./gradlew test` 전체 회귀를 fresh 실행한다. 여러 domain/Phase의 production 수정과
final Gate이므로 이번에는 전체 회귀를 생략하지 않는다.
- [x] `./gradlew ktlintCheck`, OpenAPI 23개 operation/status, controller mapping 23개, 신규 dependency/DDL 0건과
`git diff --check`를 확인한다.
- [x] Phase별 리뷰의 수정 후 검증 기록과 현재 상태표·Progress를 동기화한다.
- 검증 기록: 무엇: `REV-030`~`REV-033` 수정 후 Phase 1~6 targeted, 전체 회귀, lint, OpenAPI/controller/dependency/DDL/diff 상태를 fresh 재검증했다. 왜: Phase 3~5 후속 보완이 여러 domain production/test를 변경했으므로 최종 Gate에서 전체 회귀를 생략하지 않기 위해. 어떻게: 아래 targeted 명령, `./gradlew test`, `./gradlew ktlintCheck`, `jq` operation/status assertion, controller mapping 23개 assertion, dependency/DDL diff, `git diff --check`를 실행했다. 결과: targeted는 `BUILD SUCCESSFUL in 2m 45s`, 전체 회귀는 `BUILD SUCCESSFUL in 7m 58s`, ktlint는 `BUILD SUCCESSFUL in 1s`, OpenAPI assertion은 `true`, controller mapping 23개 assertion과 dependency/DDL diff, diff check는 출력 없이 통과했다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
./gradlew test
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 23
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 7 2차 통합 Gate
**Goal 실행 `P7-R2-GATE`:** 모든 후속 수정과 검증 증거를 대조해 최종 완료 여부를 재판정한다.
- [x] **`P7-R2-GATE` 완료:** `P7-R2` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를
`구현 완료`로 되돌린다.
- **시작 조건:** `P7-R2` 완료.
- **완료 증거:** `REV-030`~`REV-033` 처리 완료, targeted/전체 회귀/lint 성공, 23개 operation/mapping 유지,
dependency/DDL 추가 없음, Phase별 리뷰와 Progress 동기화.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록: 무엇: `P7-R2-GATE`에서 미완료 Goal·미처리 finding·차단 사항 0건과 최종 문서 상태를 재판정했다. 왜: Phase 3~5 후속 Gate 완료 뒤 최종 완료 상태를 복구하기 위해. 어떻게: 하단 finding 표의 `REV-030`~`REV-033` 처리 완료, Phase 3~5 review 문서 판정, targeted/전체 회귀/lint/OpenAPI/controller/diff 결과를 대조했다. 결과: 후속 finding은 모두 처리 완료이고 23개 operation/mapping/status가 유지되어 문서 상태를 `구현 완료`로 갱신했다.
#### Phase 7 3차 통합 재판정
- [x] **Task 7.5: Phase 2~4 후속 보완 통합 검증**
**Goal 실행 `P7-R3`:** `REV-034`~`REV-037` 수정 후 23개 operation과 레거시 parity, 공통 오류·ownership 계약을
다시 통합 검증한다.
- **시작 조건:** `P2-R7-GATE`, `P3-R11-GATE`, `P4-R3-GATE` 완료.
- **완료 증거:** 네 review ID 처리 완료, Phase 1~6 targeted와 전체 회귀, `ktlintCheck`, OpenAPI/controller 집계,
dependency/DDL/diff 점검과 Progress 기록.
- **범위 밖:** 신규 기능·operation/schema 추가, 완료된 Phase 1·5·6 동작 변경, unrelated refactor.
- **TDD 예외 사유:** 여러 Phase의 회귀 수정 후 검증·판정 전용 Task이며 별도 production 동작을 추가하지 않는다.
- **대체 검증 방법:** 각 소유 Phase Gate의 증거를 재사용하지 않고 통합 명령을 fresh 실행한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] `REV-034`~`REV-037`이 모두 처리 완료이고 미완료 소유 Goal이 없는지 확인한다.
- [x] Phase 1~6 targeted 명령과 `./gradlew test` 전체 회귀를 fresh 실행한다. 세 domain의 production 수정과 final
Gate이므로 전체 회귀를 생략하지 않는다.
- [x] `./gradlew ktlintCheck`, OpenAPI 23개 operation/status, controller mapping 23개, 신규 dependency/DDL 0건과
`git diff --check`를 확인한다.
- [x] Phase별 리뷰의 수정 후 검증 기록과 현재 상태표·Progress를 동기화한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
./gradlew test
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 23
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 7 3차 통합 Gate
**Goal 실행 `P7-R3-GATE`:** 모든 후속 수정과 검증 증거를 대조해 최종 완료 여부를 재판정한다.
- [x] **`P7-R3-GATE` 완료:** `P7-R3` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를
`구현 완료`로 되돌린다.
- **시작 조건:** `P7-R3` 완료.
- **완료 증거:** `REV-034`~`REV-037` 처리 완료, targeted/전체 회귀/lint 성공, 23개 operation/mapping 유지,
dependency/DDL 추가 없음, Phase별 리뷰와 Progress 동기화.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록: 무엇: `REV-034`~`REV-037` 통합 재판정. 왜: 세 domain production 수정 후 23개 operation과 공통 오류·ownership 계약이 유지되는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`, `./gradlew test`, `./gradlew ktlintCheck`, OpenAPI 23개 operation/status `jq`, controller mapping `rg` 23개, `git diff --check`, 변경 파일명 기반 dependency/DDL 점검을 실행했다. 결과: targeted와 전체 test, lint, jq가 모두 성공했고 mapping은 23개, `git diff --check`는 출력 없음, 신규 dependency/DDL 파일 변경은 없었다.
#### Phase 7 4차 리뷰 보완
- [x] **Task 7.6: 완료 Task 상태와 Phase 4 DELETE 계약 설명 동기화**
**Goal 실행 `P7-R4`:** 완료 증거가 존재하는 후속 Task 헤더와 현재 상태표를 동기화하고, Phase 4 시리즈 콘텐츠 해제 설명을
OpenAPI와 실제 controller route에 맞춘다.
- **추적 review ID:** `REV-038`, `REV-039`.
- **시작 조건:** `P7-R3-GATE` 완료와 `phase7-integration-review.md` 4차 정적 리뷰 판정 존재.
- **완료 증거:** `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5` 헤더가 기존 완료 증거와 같은 `[x]` 상태이고,
Phase 4 DELETE 설명이 `/contents/{contentId}` path·request body 없음으로 정정되며 상태표·Progress·리뷰 문서가
다시 완료 상태로 동기화된다.
- **범위 밖:** production/test/OpenAPI 변경, 기존 완료 증거 삭제·덮어쓰기, API operation 추가·삭제.
- **TDD 예외 사유:** 실행 동작이 아닌 구현 계획의 완료 상태와 이미 확정된 API 설명을 정정하는 문서 전용 Task다.
- **대체 검증 방법:** 미완료 Task 헤더 집계, OpenAPI 23개 operation/status, controller mapping과 Phase 4 DELETE route,
문서 명령 및 diff를 정적으로 대조한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
- [x] 기존 후속 Gate와 2026-07-29 검증 기록을 근거로 완료된 네 Task 헤더만 `[x]`로 동기화한다.
- [x] Phase 4 endpoint 설명의 시리즈 콘텐츠 해제를
`DELETE /series/{seriesId}/contents/{contentId}`와 request body 없음으로 정정한다.
- [x] 상단 상태표, Goal Progress, 발견된 문제 표와 Phase 7 리뷰 판정을 `구현 완료` 상태로 동기화한다.
- [x] `./gradlew tasks --all`, OpenAPI 23개 operation/status `jq`, controller mapping 집계,
미완료 Task header `rg`, `git diff --check` 결과를 누적 기록한다.
```bash
./gradlew tasks --all
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 23
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
rg -n '^- \[ \] \*\*Task' docs/20260724_AI캐릭터_관리자_API/plan-task.md
rg -n '@(Get|Post|Put|Delete)Mapping' \
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter
git diff --check
```
#### Phase 7 5차 통합 보완
- [x] **Task 7.7: Phase 2~5 후속 보완 통합 재판정**
**Goal 실행 `P7-R5`:** Phase 2~5의 primitive required/nullability와 커뮤니티 목록 계약 보완 뒤 23개 관리자
operation의 계약, 공통 보안·오류, ownership과 legacy 회귀를 통합 재판정한다.
- **추적 근거:** `REV-040`~`REV-043`, `DEC-P5-LIST-001`.
- **시작 조건:** `P2-R8-GATE`, `P3-R12-GATE`, `P4-R4-GATE`, `P5-R3-GATE`, `P5-R4-GATE` 완료.
- **완료 증거:** Phase 2~5 focused/영향 범위 회귀와 통합 회귀·lint 성공, OpenAPI operation/status와 controller mapping
23개 유지, dependency/DDL 추가 없음, Phase별 리뷰·상태표·Progress 동기화.
- **범위 밖:** Gate 목적과 무관한 production refactor, 전역 Jackson 정책 변경, 공개 API schema 변경.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- [x] Phase 2~5의 focused와 각 package/common 영향 범위 회귀가 fresh 성공했는지 Gate 증거를 대조한다.
- [x] 커뮤니티 목록의 timezone 제거, pagination wrapper와 active owner count·hasNext 계약을 확인한다.
- [x] JWT ADMIN 이중 인가, target/owner 오류, JSON 오류 envelope과 no-side-effect 회귀를 통합 범위에서 확인한다.
- [x] OpenAPI 23개 operation/status와 controller mapping 23개, dependency/DDL 무변경을 확인한다.
- [x] Phase별 review의 finding 상태와 상단 상태표·Progress를 최종 판정에 맞게 동기화한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
./gradlew test
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 23
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 23
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 7 5차 통합 Gate
**Goal 실행 `P7-R5-GATE`:** 모든 Phase 2~5 후속 수정과 검증 증거를 대조해 최종 완료 여부를 판정한다.
- [x] **`P7-R5-GATE` 완료:** `P7-R5` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를
`구현 완료`로 갱신한다.
- **시작 조건:** `P7-R5` 완료.
- **완료 증거:** `REV-040`~`REV-043`과 `DEC-P5-LIST-001` 처리 완료, focused/통합/전체 회귀와 lint 성공,
23개 operation/mapping 및 23개 `implemented` 유지, dependency/DDL 추가 없음, Phase별 리뷰와 Progress 동기화.
- **범위 밖:** Gate에서 production code 또는 공개 API schema 변경.
- 검증 기록: 무엇: Phase 2~5 후속 보완과 Community 목록 계약 변경 뒤 최종 통합 상태를 재판정했다. 왜:
primitive required/nullability와 목록 wrapper 변경이 23개 관리자 operation, 공통 보안·오류, ownership, legacy 회귀를
깨뜨리지 않는지 확인해야 했기 때문이다. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`,
`./gradlew test`, `./gradlew ktlintCheck`, OpenAPI 23개 operation/status `jq`, controller mapping `rg`,
dependency/DDL 경로 diff 검색, `git diff --check`를 fresh 실행했다. 결과: targeted는 `BUILD SUCCESSFUL in 2m 21s`,
전체 test는 `BUILD SUCCESSFUL in 5m 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 881ms`, OpenAPI assertion은 `true`,
controller mapping은 23개, dependency/DDL 검색과 `git diff --check`는 출력이 없었다. 미완료 Goal·미처리 finding·차단 사항은 0건이다.
#### Phase 7 후속 기능 통합 보완
- [x] **Task 7.8: 36개 operation 후속 기능 통합 재판정**
**Goal 실행 `P7-R6`:** Phase 2~6의 후속 기능과 시리즈 상세 정합화가 끝난 뒤 36개 관리자 operation의 계약,
공통 보안·오류, actor·ownership, soft delete와 legacy/public 회귀를 통합 재판정한다.
- **추적 근거:** `REV-044`~`REV-049`, `DEC-COMMENT-001`, `DEC-CHAR-COMMENT-001`,
`DEC-FANTALK-DELETE-001`, `DEC-REGISTRATION-REFERENCE-001`, `DEC-SERIES-DETAIL-001`.
- **시작 조건:** `P2-R9-GATE`, `P3-R13-GATE`, `P4-R5-GATE`, `P4-R6-GATE`, `P5-R5-GATE`,
`P6-R2-GATE` 완료.
- **완료 증거:** Phase 2~6 focused/영향 범위 회귀와 통합·전체 회귀·lint 성공, OpenAPI 36개
`implemented`와 controller mapping 36개 일치, 미구현 캐릭터 직접 댓글 route 없음, dependency/DDL 추가 없음,
Phase별 리뷰·상태표·Progress 동기화.
- **범위 밖:** 승인 범위 밖 기능, 캐릭터 직접 댓글 API, 전역 refactor, public/legacy API schema 변경.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase6-fantalk-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- [x] Phase 2~6 신규 Goal의 focused와 각 package/common 영향 범위 회귀가 fresh 성공했는지 Gate 증거를 대조한다.
- [x] 댓글 actor·parent·owner 경계, row-only soft delete, FanTalk fan root 삭제와 reply row 유지 결과를 통합 확인한다.
- [x] 원작·장르 참조 조회와 시리즈 목록/상세 item parity, JWT ADMIN 이중 인가와 공통 오류 계약을 확인한다.
- [x] OpenAPI operation/status와 controller mapping이 36개이며 캐릭터 직접 댓글 operation/mapping이 없는지 확인한다.
- [x] 신규 dependency/DDL 무변경과 legacy/public 회귀를 확인하고 Phase별 review·상태표·Progress를 최종 동기화한다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
./gradlew test
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 36
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 36
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 7 후속 기능 통합 Gate
**Goal 실행 `P7-R6-GATE`:** 모든 후속 기능·정합화 Goal과 검증 증거를 대조해 최종 완료 여부를 판정한다.
- [x] **`P7-R6-GATE` 완료:** `P7-R6` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를
`구현 완료`로 갱신한다.
- **시작 조건:** `P7-R6` 완료.
- **완료 증거:** `REV-044`~`REV-049` 처리 완료, focused/통합/전체 회귀와 lint 성공, 36개
operation/mapping/`implemented` 일치, 범위 제외 route 0개, dependency/DDL 추가 없음, Phase별 문서 동기화.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 7 UTC 날짜 계약 통합 보완
- [x] **Task 7.9: UTC 날짜 계약 36개 operation 통합 재판정**
**Goal 실행 `P7-R7`:** Phase 3·5 UTC 날짜 계약 구현 뒤 36개 관리자 operation의 OpenAPI·runtime·보안·legacy/public
격리를 통합 재판정한다.
- **추적 근거:** `REV-050`, `REV-051`, `DEC-UTC-DATE-001`.
- **시작 조건:** `P3-R14-GATE`, `P5-R6-GATE` 완료.
- **완료 증거:** 36개 operation과 controller mapping 유지, 36개 `implemented`, OpenAPI에서 `timezone` parameter/schema
0건, 영향 6개 operation의 UTC 계약·legacy/public 회귀·lint 성공, Phase별 리뷰·상태표·Progress 동기화.
- **범위 밖:** 승인된 6개 operation 밖 날짜 schema 변경, 전역 timezone/Jackson 정책 변경, dependency·DDL 추가,
public/legacy API 변경.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- [x] Phase 3 오디오 4개와 Phase 5 커뮤니티 2개 operation의 focused/Gate 증거를 대조한다.
- [x] OpenAPI 36개 operation·36개 `implemented`, controller mapping 36개와 timezone parameter/schema 0건을 확인한다.
- [x] 오디오 생성·상세·댓글과 커뮤니티 댓글의 UTC exact JSON, 기존 ownership/인가/오류·legacy/public 회귀를 확인한다.
- [x] 신규 dependency/DDL·범위 밖 날짜 schema 변경이 없고 Phase별 review finding·상태표·Progress가 일치하는지 확인한다.
- **`P7-R7` 검증(2026-07-29):** `P3-R14-GATE`와 `P5-R6-GATE` 증거를 대조했다. OpenAPI는 36개 operation,
36개 `implemented`, 0개 `alignment-required`, query `timezone` parameter 0개, `components.parameters.Timezone`
0개였고, 신규 prefix controller mapping도 36개였다. 영향 6개 operation의 UTC exact JSON은 오디오 focused
`BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks` 재실행 `BUILD SUCCESSFUL in 4m 33s`와 각 Gate의
legacy/public 영향 범위 회귀로 확인했다. 내부 legacy 재사용을 위한 `timezone = UTC` 상수 호출 외 신규 관리자
외부 계약의 timezone query/body는 남지 않았다.
```bash
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' \
--tests kr.co.vividnext.sodalive.content.AudioContentServiceTest \
--tests kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest
./gradlew ktlintCheck
jq -e '
[.paths[] | to_entries[] | select(.key | IN("get", "post", "put", "delete")) | .value] as $operations
| ($operations | length) == 36
and ([$operations[] | select(.["x-implementation-status"] == "implemented")] | length) == 36
and ([.. | objects | select(.name? == "timezone" and .in? == "query")] | length) == 0
and (.components.parameters.Timezone? == null)
' docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
#### Phase 7 UTC 날짜 계약 통합 Gate
**Goal 실행 `P7-R7-GATE`:** `P3-R14-GATE`, `P5-R6-GATE`와 통합 증거를 대조해 최신 계약 구현 완료를 판정한다.
- [x] **`P7-R7-GATE` 완료:** `P7-R7` 완료 후 미완료 Goal·미처리 finding·차단 사항 0건을 확인하고 문서 상태를
`구현 완료`로 갱신한다.
- **시작 조건:** `P7-R7` 완료.
- **완료 증거:** `REV-050`~`REV-051` 처리 완료, 영향 범위 회귀와 lint 성공, 36개
operation/mapping/`implemented` 일치, timezone parameter/schema 0건, dependency/DDL 추가 없음, Phase별 문서 동기화.
- **범위 밖:** Gate에서 production code 또는 public/legacy API 변경.
#### Phase 7 HTTP 경계 통합 재판정
- [x] **Task 7.10: optional pagination과 JSON·multipart media type 계약 통합 재판정**
**Goal 실행 `P7-R8`:** `P2-R10-GATE`, `P3-R15-GATE`, `P3-R16-GATE`, `P4-R7-GATE`, `P5-R7-GATE`,
`P5-R8-GATE`, `P6-R3-GATE`, `P6-R4-GATE`의 결과를 대조해 37개 관리자 operation의 query 기본값, request media
type과 FanTalk 답변 수정 계약을 최종 재판정한다.
- **추적 review ID:** `REV-052`~`REV-059`.
- **시작 조건:** Phase 2~6의 여덟 소유 Gate 완료.
- **완료 증거:** OpenAPI 37개 operation/고유 operationId와 controller 37개 mapping 일치, 영향 14개 operation의
optional pagination, JSON-only 또는 multipart part-level JSON/415, FanTalk 답변 수정 계약 및 공통 오류
header/envelope 회귀 성공, 미처리 finding 0건,
dependency·DDL 추가 없음과 Phase별 리뷰·Progress 동기화.
- **범위 밖:** 확정된 FanTalk 답변 수정 외 신규 route/schema/기능, legacy/public API 변경, 관련 없는 refactor.
- [x] **STATIC:** OpenAPI 문법·내부 `$ref`·operationId·request media type·pagination parameter와 controller mapping을 대조한다.
- [x] **REGRESSION:** Phase 3·5·6 focused와 공통 error/authorization 영향 범위 결과를 대조하고 전체 회귀 필요성을 판정한다.
- [x] **SCOPE:** dependency/DDL/legacy-public 변경이 없고 37개 route가 일치하는지 확인한다.
- [x] **DOCUMENT:** `REV-052`~`REV-059`, 상태표, Phase별 리뷰, Goal Progress와 검증 기록을 동기화한다.
#### Phase 7 HTTP 경계 통합 Gate
**Goal 실행 `P7-R8-GATE`:** `P7-R8` 증거와 미처리 finding을 대조해 구현 완료 복구 여부를 판정한다.
- [x] **`P7-R8-GATE` 완료:** 미완료 Goal·미처리 finding·차단 사항 0건일 때만 문서 상태를 `구현 완료`로 갱신한다.
- **시작 조건:** `P7-R8` 완료.
- **완료 증거:** 영향 14개 operation을 포함한 37개 계약/mapping 정합성, 회귀·lint·diff 성공, 문서 동기화.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 7 multipart part 이름·문서 상태 통합 재판정
- [x] **Task 7.11: exact multipart part 계약과 37개 구현 상태 통합 정합화**
**Goal 실행 `P7-R9`:** `P2-R11-GATE`, `P3-R17-GATE`, `P4-R8-GATE`, `P5-R9-GATE` 결과를 대조해
8개 multipart operation의 허용 part 이름을 OpenAPI와 일치시키고, 완료 상태가 오래된 계획·계약 설명을 실제
37개 구현 상태와 동기화한다.
- **추적 review ID:** `REV-060`~`REV-064`.
- **시작 조건:** Phase 2~5의 네 소유 Gate 완료.
- **완료 증거:** 8개 multipart operation의 미정의 part 400/no-side-effect와 기존 part/media type 회귀,
OpenAPI 37개 operation/37개 `implemented`와 controller 37개 mapping 일치, `plan-task.md`와
`api-contract.md`의 route/완료/예정 수·FanTalk 답변 수정 상태 동기화, dependency·DDL 무변경.
- **범위 밖:** OpenAPI path/schema 변경, 신규 route, legacy/public API, 전역 multipart resolver, 관련 없는 문서 이력 삭제.
- **TDD 예외 사유:** production 동작은 Phase 2~5 소유 Task에서 TDD로 수정하며 이 Task는 통합 검증과 현황 문서
동기화만 수행한다.
- **대체 검증 방법:** 소유 Gate의 actual endpoint 증거를 대조하고 OpenAPI status와 controller mapping을 기계 집계한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **STATIC:** OpenAPI의 8개 multipart schema `additionalProperties: false`와 operation별 허용/필수 part를
controller 및 actual endpoint 회귀와 대조한다.
- [x] **REGRESSION:** Phase 2~5 focused와 공통 error/authorization 영향 범위, 전체 회귀 필요성을 판정해 실행 결과
또는 생략 근거를 기록한다.
- [x] **DOCUMENT:** Endpoint Contract Summary, `api-contract.md` 상단 집계·endpoint 표·client 생성 설명을
37개 route/37개 구현 완료/예정 0개로 동기화하고 과거 완료 이력은 보존한다.
- [x] **SCOPE:** OpenAPI 37개 operationId/status와 controller mapping, dependency·DDL·legacy/public 무변경,
`ktlintCheck`, `git diff --check`를 확인한다.
#### Phase 7 multipart part 이름·문서 상태 통합 Gate
**Goal 실행 `P7-R9-GATE`:** `REV-060`~`REV-064`의 소유 Gate와 통합 증거를 대조해 최신 계약 구현 완료 복구 여부를
판정한다.
- [x] **`P7-R9-GATE` 완료:** 미정의 part 회귀와 문서 상태가 모두 정합하고 미처리 finding·차단 사항이 0건일 때만
문서 상태를 `구현 완료`로 갱신한다.
- **시작 조건:** `P7-R9` 완료.
- **완료 증거:** 8개 multipart contract, 37개 operation/mapping/status, 문서 집계, 회귀·lint·diff,
dependency·DDL·legacy/public 무변경 확인.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
#### Phase 7 8차 리뷰 후속 통합과 finding 상태 동기화
- [x] **Task 7.12: Phase 2~6 후속 Gate 통합 및 리뷰 상태 정합화**
**Goal 실행 `P7-R10`:** `REV-065`~`REV-070` 소유 Gate의 완료 증거를 통합 대조하고, 이미 완료된
`REV-060`~`REV-064`와 신규 finding의 상태·상단 Phase 집계·현재 Goal을 실제 결과에 맞게 동기화한다.
- **추적 review ID:** `REV-071`.
- **시작 조건:** `P2-R12-GATE`, `P3-R18-GATE`, `P4-R9-GATE`, `P4-R10-GATE`, `P5-R10-GATE`,
`P6-R5-GATE` 완료.
- **완료 증거:** 파일·일반 form-field를 포함한 8개 multipart operation 회귀, Series 장르 0 이하 경계,
FanTalk 비활성 root 삭제 문서 계약, 37개 OpenAPI operation/controller mapping/status, finding 표와 Phase 집계가
모두 일치.
- **범위 밖:** 신규 route/schema/기능, legacy/public API, 관련 없는 완료 이력 삭제, 신규 dependency·DDL.
- **TDD 예외 사유:** production 수정은 각 소유 Phase Task에서 수행하고 이 Task는 통합 검증과 상태 문서 동기화만
담당한다.
- **대체 검증 방법:** 각 소유 Gate의 actual endpoint 증거를 대조하고 OpenAPI operation/status, controller mapping,
문서 finding 상태를 기계 집계한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase4-series-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase5-community-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase6-fantalk-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **INTEGRATION:** `REV-065`~`REV-070` 소유 Gate의 focused/영향 범위 또는 문서 정적 검증 증거를 대조한다.
- [x] **STATIC:** OpenAPI 37개 operationId/status와 controller 37개 mapping, 8개 multipart schema,
dependency·DDL·legacy/public 무변경을 확인한다.
- [x] **DOCUMENT:** `REV-060`~`REV-071` 상태, 상단 Phase 완료 수, 현재 Phase/Goal과 Phase별 리뷰 결론을
실제 완료 상태에 맞춘다.
- [x] **SCOPE:** 필요한 범위의 회귀·`ktlintCheck`·`git diff --check` 결과 또는 생략 근거를 기록한다.
#### Phase 7 8차 리뷰 후속 통합 Gate
**Goal 실행 `P7-R10-GATE`:** 8차 리뷰 finding과 문서 상태가 모두 종결됐는지 최종 판정한다.
- [x] **`P7-R10-GATE` 완료:** 미처리 finding·차단 사항이 0건이고 최신 계약·구현·문서가 일치할 때만 문서 상태를
`구현 완료`로 되돌린다.
- **시작 조건:** `P7-R10` 완료.
- **완료 증거:** Phase 2~6 소유 Gate, OpenAPI/controller 집계, Phase별 리뷰·finding 표·상단 상태 정합.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' \
--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
./gradlew ktlintCheck
./gradlew tasks --all
jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
- 검증 기록(RED): Phase 2~5 multipart 일반 form-field와 Phase 4 `genreId <= 0` focused RED 묶음에서 신규 multipart/genre 36건 실패를 확인했다. 최초 compile error 2회는 community test helper의 `MockPart` 연결 방식 문제였고, production 변경 전 테스트 구성만 고쳐 재실행했다.
- 검증 기록(GREEN): 같은 focused 묶음을 재실행해 `BUILD SUCCESSFUL in 1m 17s`를 확인했다.
- 검증 기록(INTEGRATION): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --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 --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest` → `BUILD SUCCESSFUL in 4m 11s`.
- 검증 기록(STATIC): OpenAPI 집계 `operations=37 uniqueOperationIds=37 implemented=37 alignmentRequired=0 planned=0`, controller mapping 37개, FanTalk 삭제 no-op 문서·OpenAPI·테스트 정적 대조 완료.
- 검증 기록(SCOPE): `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 51s`; `git diff --check` → 출력 없음. 전체 `./gradlew test`는 Phase 2~6 후속 변경의 직접 영향 범위를 위 focused 통합 명령이 포함하므로 생략했다.
#### Phase 7 9차 리뷰 후속 통합 재판정
- [x] **Task 7.13: preview 검증 복구 후 37개 operation 통합 재판정**
**Goal 실행 `P7-R11`:** `P3-R19-GATE`의 preview 검증 복구 증거를 포함해 Phase 1~7 리뷰 결론,
OpenAPI 37개 operation과 controller mapping, finding·상태 문서를 다시 대조한다.
- **추적 review ID:** `REV-072`.
- **시작 조건:** `P3-R19-GATE` 완료.
- **완료 증거:** `REV-072` 처리 완료, Phase 3 preview actual endpoint/legacy 회귀 증거, OpenAPI 37개
operationId와 controller 37개 mapping, 미처리 finding·차단 사항 0건, 상태표·Phase별 리뷰·Progress 동기화.
- **범위 밖:** 신규 route/schema/기능, legacy/public 계약 변경, 관련 없는 완료 이력 삭제, 신규 dependency·DDL.
- **TDD 예외 사유:** production 보완은 Phase 3에서 TDD로 수행하며 이 Task는 통합 증거와 문서 상태만 재판정한다.
- **대체 검증 방법:** `P3-R19-GATE` 결과를 대조하고 OpenAPI operation/status, controller mapping,
Phase별 review/finding 상태를 기계 집계한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/reviews/phase7-integration-review.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/prd.md`
- Confirm: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- [x] **INTEGRATION:** `P3-R19-GATE`의 actual endpoint·legacy 회귀와 no-side-effect 증거를 대조한다.
- [x] **STATIC:** OpenAPI 37개 operationId/status와 controller 37개 mapping, dependency·DDL·legacy/public
무변경을 확인한다.
- [x] **DOCUMENT:** `REV-072`, 상단 Phase 완료 수, 현재 Phase/Goal, Phase 3·7 리뷰와 Progress를 실제 결과에
맞춘다.
- [x] **SCOPE:** 필요한 범위의 회귀·`ktlintCheck`·`git diff --check` 결과 또는 생략 근거를 기록한다.
- 검증 기록: 무엇: `P3-R19-GATE` 증거를 포함해 37개 operation 통합 상태를 재판정했다. 왜: `REV-072` 처리 전에는
route/schema 집계만으로 Phase 7 완료 판정을 유지할 수 없었기 때문이다. 어떻게: Phase 3 actual endpoint·legacy 회귀,
OpenAPI implemented count, controller mapping count, dependency/DDL diff, `ktlintCheck`, `git diff --check` 결과를 대조했다.
결과: OpenAPI `x-implementation-status=implemented` 37개, controller mapping 37개, 신규 dependency·DDL 변경 없음,
미처리 finding 0건으로 재판정했다.
#### Phase 7 9차 리뷰 후속 통합 Gate
**Goal 실행 `P7-R11-GATE`:** `REV-072`와 문서 상태가 모두 종결됐는지 최종 판정한다.
- [x] **`P7-R11-GATE` 완료:** 미처리 finding·차단 사항이 0건이고 최신 계약·구현·문서가 일치할 때만
문서 상태를 `구현 완료`로 되돌린다.
- **시작 조건:** `P7-R11` 완료.
- **완료 증거:** Phase 3 소유 Gate, OpenAPI/controller 집계, Phase별 리뷰·finding 표·상단 상태 정합.
- **범위 밖:** Gate에서 production code 또는 OpenAPI schema 변경.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test'
./gradlew ktlintCheck
jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
git diff --check
```
- Gate 검증 기록: 무엇: Phase 3 소유 Gate와 Phase 7 통합 문서 상태를 최종 대조했다. 왜: 최신 계약·구현·문서가 모두 일치할
때만 최종 상태를 `구현 완료`로 되돌릴 수 있기 때문이다. 어떻게: `jq` JSON 문법 확인, OpenAPI implemented 37개 집계,
controller mapping 파일별 4/5/10/9/8/1 합계 37개 집계, dependency·DDL diff, 영향 범위 회귀와 lint·diff 결과를 확인했다.
결과: `REV-072`는 처리 완료이고 Phase 1~7 미처리 finding·차단 사항은 0건이므로 Phase 7 Gate를 완료로 판정했다.
---
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P2-R1` | `P2-H1`, `P2-H2` | 아니요 | 리뷰 후보를 판정하고 계약 미확정이면 Decision Log에 기록 |
| 2 | `P2-T3` → `P2-T4` → `P2-T5` → `P2-T6` | `P2-R1` | 아니요 | 실패 소유 Goal에서 수정·검증을 끝낸 뒤 다음 Goal 수행 |
| 3 | `P2-GATE` | Phase 2 활성 Goal 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 추가 |
| 4 | `P3-R1` | `P2-GATE`, `P3-H1`, `P3-H2` | 아니요 | 리뷰 후보를 판정하고 계약 미확정이면 Decision Log에 기록 |
| 5 | `P3-T3` → `P3-T4` → `P3-T5` → `P3-T6` → `P3-T7` | `P3-R1` | 아니요 | 실패 소유 Goal에서 수정·검증을 끝낸 뒤 다음 Goal 수행 |
| 6 | `P3-GATE` | Phase 3 활성 Goal 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 추가 |
| 7 | `P2-R2` → `P2-R2-GATE` | 2차 Phase 2 review | 아니요 | `REV-009` 소유 Task에서 증거 보강 |
| 8 | `P3-R2` → `P3-R3` → `P3-R2-GATE` | `P2-R2-GATE`, 2차 Phase 3 review | 아니요 | `REV-010`~`REV-011` 소유 Task에서 증거 보강 |
| 9 | `P2-R3` → `P2-R3-GATE` | 4차 Phase 2 review | 아니요 | `REV-012` 소유 Task에서 증거 보강 |
| 10 | `P3-R4` → `P3-R3-GATE` | `P2-R3-GATE`, 4차 Phase 3 review | 아니요 | `REV-013`~`REV-014` 소유 Task에서 증거 보강 |
| 11 | `P2-R4` → `P2-R4-GATE` | 5차 Phase 2 review | 아니요 | `REV-015` actual transaction 증거 보강 |
| 12 | `P3-R5` → `P3-R6` → `P3-R4-GATE` | `P2-R4-GATE`, 5차 Phase 3 review | 아니요 | `REV-016`~`REV-017` 소유 Task에서 수정·증거 보강 |
| 13 | `P2-R5` → `P2-R5-GATE` | 6차 Phase 2 review | 아니요 | `REV-018` 문서 계약 동기화 |
| 14 | `P3-R7` → `P3-R8` → `P3-R5-GATE` | `P2-R5-GATE`, 6차 Phase 3 review | 아니요 | `REV-019`~`REV-020` 소유 Task에서 수정·증거 보강 |
| 15 | `P23-CONTRACT-1` → `P23-CONTRACT-2` → `P23-CONTRACT-3` → `P23-CONTRACT-GATE` | `P3-R5-GATE`, 사용자 계약 확정 | 아니요 | 문서 또는 runtime 불일치 소유 Goal에서 수정 |
| 16 | `P4-T1`~`P4-T6` → `P4-GATE` | `P23-CONTRACT-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 17 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 18 | `P6-T1`~`P6-T5` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 19 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 |
| 20 | `P2-R6` → `P2-R6-GATE` → `P3-R9` → `P3-R9-GATE` → `P4-R1` → `P4-R1-GATE` → `P5-R1` → `P5-R1-GATE` → `P6-R1` → `P6-R1-GATE` → `P7-R1` → `P7-R1-GATE` | 2026-07-28 Phase 1~7 정적 리뷰 | 아니요 | 확정 finding 소유 Goal에서 최소 수정·검증 후 다음 Gate 수행 |
| 21 | `P3-R10` → `P3-R10-GATE` → `P4-R2` → `P4-R2-GATE` → `P5-R2` → `P5-R2-GATE` → `P7-R2` → `P7-R2-GATE` | 2026-07-28 Phase별 후속 정적 리뷰 | 아니요 | `REV-030`~`REV-033` 소유 Goal에서 최소 수정·검증 후 통합 재판정 |
| 22 | `P2-R7` → `P2-R7-GATE` → `P3-R11` → `P3-R11-GATE` → `P4-R3` → `P4-R3-GATE` → `P7-R3` → `P7-R3-GATE` | 2026-07-28 3차 Phase별 정적 리뷰 | 아니요 | `REV-034`~`REV-037` 소유 Goal에서 최소 수정·검증 후 통합 재판정 |
| 23 | `P2-R8` → `P2-R8-GATE` → `P3-R12` → `P3-R12-GATE` → `P4-R4` → `P4-R4-GATE` → `P5-R3` → `P5-R3-GATE` → `P5-R4` → `P5-R4-GATE` → `P7-R5` → `P7-R5-GATE` | 2026-07-29 5차 Phase별 정적 리뷰와 Community 목록 계약 확정 | 아니요 | `REV-040`~`REV-043`과 `DEC-P5-LIST-001`을 최소 보완한 뒤 통합 재판정 |
| 24 | `P2-R9` → `P2-R9-GATE` → `P3-R13` → `P3-R13-GATE` → `P4-R5` → `P4-R5-GATE` → `P4-R6` → `P4-R6-GATE` → `P5-R5` → `P5-R5-GATE` → `P6-R2` → `P6-R2-GATE` → `P7-R6` → `P7-R6-GATE` | 2026-07-29 승인 후속 기능과 시리즈 상세 정합화 | 아니요 | `REV-044`~`REV-049` 소유 Goal에서 최소 구현·검증 후 다음 Phase Gate 수행 |
| 25 | `P3-R14` → `P3-R14-GATE` → `P5-R6` → `P5-R6-GATE` → `P7-R7` → `P7-R7-GATE` | 2026-07-29 UTC 날짜 계약 확정 | 아니요 | `REV-050`~`REV-051` 소유 Goal에서 6개 operation만 최소 정합화한 뒤 통합 재판정 |
| 26 | `P2-R10` → `P2-R10-GATE` → `P3-R15` → `P3-R15-GATE` → `P3-R16` → `P3-R16-GATE` → `P4-R7` → `P4-R7-GATE` → `P5-R7` → `P5-R7-GATE` → `P5-R8` → `P5-R8-GATE` → `P6-R3` → `P6-R3-GATE` → `P6-R4` → `P6-R4-GATE` → `P7-R8` → `P7-R8-GATE` | 2026-07-29 6차 Phase별 정적 리뷰와 FanTalk 답변 수정 계약 확정 | 아니요 | `REV-052`~`REV-059` 소유 HTTP 경계·신규 답변 수정만 최소 보완한 뒤 통합 재판정 |
| 27 | `P2-R11` → `P2-R11-GATE` → `P3-R17` → `P3-R17-GATE` → `P4-R8` → `P4-R8-GATE` → `P5-R9` → `P5-R9-GATE` → `P7-R9` → `P7-R9-GATE` | 2026-07-29 7차 Phase별 정적 리뷰 | 아니요 | `REV-060`~`REV-064`의 exact multipart part와 문서 상태만 최소 보완한 뒤 통합 재판정 |
| 28 | `P2-R12` → `P2-R12-GATE` → `P3-R18` → `P3-R18-GATE` → `P4-R9` → `P4-R9-GATE` → `P4-R10` → `P4-R10-GATE` → `P5-R10` → `P5-R10-GATE` → `P6-R5` → `P6-R5-GATE` → `P7-R10` → `P7-R10-GATE` | 2026-07-29 8차 Phase별 정적 리뷰 | 아니요 | `REV-065`~`REV-071` 소유 전체 multipart part·장르 ID·FanTalk 문서·상태만 최소 보완한 뒤 통합 재판정 |
| 29 | `P3-R19` → `P3-R19-GATE` → `P7-R11` → `P7-R11-GATE` | 2026-07-29 9차 Phase별 정적 리뷰 | 아니요 | `REV-072` preview 검증을 Phase 3에서 복구한 뒤 37개 operation 상태를 통합 재판정 |
## 변경 금지·중단 규칙
- PRD와 Endpoint Contract Summary를 근거 없이 변경하거나 제공되지 않은 DTO·enum·오류 status/key를 추정하지 않는다.
- 기존 완료 체크박스, 검증 기록과 Progress를 삭제·되돌리거나 덮어쓰지 않는다.
- review 후보는 재현·판정 전 production 수정으로 전환하지 않고, 확정 항목만 관련 Goal 또는 신규 회귀 수정 Goal로 처리한다.
- test를 삭제·skip·완화하거나 관련 없는 refactor·dependency·DDL을 추가해 Gate를 통과시키지 않는다.
- JWT, token, signed URL 전체, private path와 파일 본문을 문서·fixture·log에 기록하지 않는다.
- 일부 체크박스, 일부 test 또는 코드 작성만 끝난 상태에서는 Goal을 `complete`로 갱신하지 않는다.
## Goal Progress
기존 기록을 삭제하거나 덮어쓰지 않고 Goal 실행 결과를 차수별로 누적한다.
### Phase 1~7 10차 정적 리뷰 완료 — 2026-07-30
- 상태: 판정 완료, `구현 완료` 유지
- 무엇을: PRD·plan·OpenAPI 37개 operation과 Phase 1~7의 현재 production/test 소스를 Phase별로 다시 대조했다.
- 왜: 기존 컴파일·테스트 통과 기록과 별개로 `REV-072` 처리 뒤 계약·소유권·부작용 경계와 최종 문서 상태가 유지되는지
확인하기 위해서다.
- 어떻게: security/CORS/target resolver, Character, AudioContent·댓글, Series, Community·댓글, FanTalk의
controller/facade/service/repository/test를 `rg`·`sed`·`jq`로 정적 검토했다. 사용자 지시에 따라 Gradle 컴파일·테스트는
실행하지 않았다.
- Phase별 결과: Phase 1~6은 신규 확정 finding이 없고, Phase 7도 추가 통합 보완이 필요하지 않다. 기존 `REV-001`~`REV-072`
72건은 모두 `처리 완료` 상태다.
- 정적 검증: OpenAPI JSON과 내부 `$ref`, operation 37개·고유 operationId 37개·`implemented` 37개,
controller mapping Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4 = 37을 확인했다.
- 계획 전환: 신규 회귀 수정 Task/Goal/Gate 없음. Phase별 완료 수 7/7, 18/18, 29/29, 16/16, 16/16, 10/10,
13/13과 `구현 완료` 상태를 유지한다.
### `P3-R19` / `P3-R19-GATE` / `P7-R11` / `P7-R11-GATE` 완료 — 2026-07-30
- 상태: 완료, 최종 `구현 완료` 재판정
- 무엇을: v2 오디오 생성의 preview 쌍·형식·최소 15초 검증을 legacy creator 생성과 같은 공유 parsed request overload로
복구하고, Phase 3·7 리뷰와 문서 상태를 동기화했다.
- 왜: `REV-072`처럼 v2 생성 경로가 문자열 request overload의 `validatePreviewTime`을 우회하면 잘못된 preview 입력이
DB/S3/event 경계로 진행될 수 있기 때문이다.
- 어떻게: `AiCharacterAdminAudioContentCreateTest`에 한쪽만 입력·형식 오류·15초 미만 KO/EN/JA actual endpoint 테스트와 정상
preview metadata 검증을 추가했고, `AudioContentService.createAudioContent(CreateAudioContentRequest, ...)`에 검증 호출을 이동했다.
- RED: production 변경 전 invalid preview actual endpoint 9건이 400 기대 대비 200/부작용 경로로 실패했다. 테스트 JSON 조립 오류
수정 전에는 400 공통 `invalid_request`가 먼저 발생해 테스트를 바로잡은 뒤 GREEN을 재확인했다.
- 검증: `./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`; `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests 'kr.co.vividnext.sodalive.content.*AudioContent*Test'`
→ `BUILD SUCCESSFUL in 1m 22s`; `./gradlew ktlintCheck` → `BUILD SUCCESSFUL in 32s`; `jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
→ 출력 없음; OpenAPI implemented count → `37`; controller mapping count → 파일별 4/5/10/9/8/1 합계 37;
`git diff --check` → 출력 없음.
- 결과: `REV-072` 처리 완료. Phase 3은 29/29 완료, Phase 7은 13/13 완료이며 미처리 finding·차단 사항은 0건이다.
### Phase 1~7 9차 정적 리뷰 완료 — 2026-07-29
- 상태: 판정 완료, `P3-R19` 후속 구현 대기
- 무엇을: PRD·plan·OpenAPI 37개 operation과 Phase 1~7의 controller/facade/service/repository/test 소스를
대조해 Phase별로 리뷰 결과를 기록했다.
- 왜: 컴파일·테스트 통과와 별개로 신규 v2 오디오 생성의 preview 시간 검증이 기존 creator 생성과 동일한지 확인하고,
확정된 회귀만 기존 완료 이력을 보존한 신규 Goal로 전환하기 위해서다.
- 어떻게: `AiCharacterAdminAudioContentFacade.create`의 호출 대상과 `AudioContentService` 두 overload의 검증 위치,
v2 actual endpoint 테스트, OpenAPI operation/status를 `rg`·`sed`·`jq`로 정적으로 대조했다. 사용자 지시에 따라
Gradle·컴파일·테스트는 실행하지 않았다.
- 결과: 문자열 request overload에만 preview 검증이 있고 v2가 호출하는 parsed request overload에는 검증이 없는
`REV-072`를 High로 확정했다. Phase 1·2·4·5·6은 신규 finding이 없으며 Phase 7은 `P3-R19-GATE` 뒤 통합 재판정이
필요하다.
- 계획 전환: `Task 3.29` / `P3-R19` / `P3-R19-GATE`, 이어서 `Task 7.13` / `P7-R11` /
`P7-R11-GATE`.
- 정적 검증: OpenAPI JSON 문법 정상, operation 37개·고유 operationId 37개·`implemented` 37개를 확인했다.
- 다음 Goal: `P3-R19`.
### Phase 1~7 8차 정적 리뷰 완료 — 2026-07-29
- 상태: 판정 완료, 후속 구현 대기
- 무엇을: PRD·plan·OpenAPI 37개 operation과 Phase 1~7의 controller/facade/repository/test 소스를 대조해
`REV-065`~`REV-071`을 확정하고 Phase별 신규 Task/Gate로 전환했다.
- 왜: 기존 완료 기록을 되돌리지 않으면서 실제 구현과 multipart/장르/FanTalk 문서/상태 계약의 잔여 불일치를
이어서 수정할 수 있는 goal 단위로 남기기 위해서다.
- 어떻게: `rg`·`jq`·`nl`, Spring Web 5.3.29 및 기존 compile output의 `javap` 정적 증거를 사용했다. 사용자
지시에 따라 컴파일과 테스트는 실행하지 않았다.
- 검증: OpenAPI operation 37개·고유 ID 37개·`implemented` 37개, controller mapping 37개, Phase Task 수
7/18/28/16/16/10/12, 신규 Task 정의 각 1개, Markdown code fence 짝을 확인했다. `./gradlew tasks --all`은
task 목록만 조회해 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력 없이 성공했다.
- 다음 Goal: `P2-R12`.
### `P3-R15` / `P3-R15-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: 오디오 댓글·답글 목록 두 GET이 `page`, `size` 전체 또는 부분 생략 시 OpenAPI 기본값 `0`, `20`을 적용하도록 복구했다.
- 왜: `REV-052`가 controller의 필수 binding과 facade의 정확한 query-name 집합 검증이 optional pagination 계약을 함께 막는다고 확정했기 때문이다.
- 어떻게: controller `@RequestParam`에 기본값을 지정하고 facade는 `page`, `size`의 부분집합만 허용하면서 미지 query·음수 page·1 미만 size는 기존 400 경계를 유지했다. actual endpoint 테스트는 댓글·답글 각각의 전체 생략, `page`만, `size`만 요청을 20/1 item 경계로 확인했고 기존 UTC·오류 회귀를 함께 실행했다.
- 검증: RED 2건 후 focused 9건 `BUILD SUCCESSFUL in 40s`, content/common error 영향 범위 `BUILD SUCCESSFUL in 2m 58s`, `ktlintCheck` `BUILD SUCCESSFUL in 37s`.
- 전체 회귀: `./gradlew test`는 변경이 v2 오디오 댓글 controller/facade와 focused actual endpoint에 한정되고 영향 범위 회귀가 이를 포함하므로 생략했다.
- 다음 Goal: `P3-R16`.
### `P3-R16` / `P3-R16-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: AudioContent POST·PUT multipart `request` part를 `application/json` 호환 값으로만 제한했다.
- 왜: `REV-056`가 `@RequestPart String` binding이 text/plain과 Content-Type 누락을 수용해 OpenAPI 415 계약을 위반한다고 확정했기 때문이다.
- 어떻게: Character `P2-R10`과 같은 multipart header 검사와 `HttpMediaTypeNotSupportedException`을 controller 경계에만 적용했다. POST·PUT actual endpoint는 KO/EN/JA의 text/plain·누락 media type 415 `ApiResponse.error`, `Accept: application/json`, DB/S3/event 무변경을 검증했고, 기존 JSON strict parse·필수 part 400·UTC/file/series 회귀는 JSON fixture로 유지했다.
- 검증: RED 13건 뒤 focused 83건 `BUILD SUCCESSFUL in 56s`, content/common error 영향 범위 `BUILD SUCCESSFUL in 1m 55s`, `ktlintCheck` `BUILD SUCCESSFUL in 16s`, OpenAPI encoding 정적 대조과 `git diff --check` 출력 없음을 확인했다.
- 전체 회귀: `./gradlew test`는 controller와 실제 AudioContent endpoint test에 한정된 변경을 content/common 영향 범위 회귀가 포함하므로 실행하지 않았다.
### `P4-R7` / `P4-R7-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: Series POST·PUT multipart `request` part를 `application/json` 호환 값으로만 제한했다.
- 왜: `REV-057`가 `@RequestPart String` binding이 `text/plain`과 Content-Type 누락을 수용해 OpenAPI 415 계약을 위반한다고 확정했기 때문이다.
- 어떻게: Character·AudioContent와 같은 multipart header 검사와 `HttpMediaTypeNotSupportedException`을 Series controller 경계에만 적용했다. POST·PUT actual endpoint는 KO/EN/JA의 `text/plain`·누락 media type 415 `ApiResponse.error`, `Accept: application/json`, S3/DB/event 무변경을 검증했고, 필수 `request` part 누락 400 및 기존 JSON strict parse·image/genre/owner 회귀는 유지했다.
- 검증: RED 12건 후 focused 36건 `BUILD SUCCESSFUL in 43s`, series/common error 영향 범위 `BUILD SUCCESSFUL in 1m 4s`, `ktlintCheck` `BUILD SUCCESSFUL in 20s`, OpenAPI Series create/update encoding 정적 대조와 `git diff --check` 출력 없음을 확인했다.
- 전체 회귀: `./gradlew test`는 v2 Series controller와 실제 Series endpoint test에 한정된 변경이고 series/common error 영향 범위 회귀가 이를 포함하므로 실행하지 않았다.
### FanTalk 답변 수정 계약·구현 계획 보완 — 2026-07-29
- 상태: 계약 확정, 구현 대기
- 무엇을: 레거시 `PUT /explorer/profile/cheers`의 FanTalk 답변 수정 계약을 신규 관리자 경계의
`PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`로 이관했다.
- 왜: 기존 V2 관리자 FanTalk에는 목록·답변 작성·팬 원글 삭제만 있고, 선택한 AI 캐릭터가 작성한 답변의 내용·활성 상태를
수정할 operation이 없기 때문이다.
- 어떻게: `cheersId`만 path `replyId`로 이동하고 optional/nullable `content`, `isActive`, 동시 입력, 빈 객체 no-op,
비활성 reply 재활성화와 레거시 `CreatorChannelFanTalkResponse` 성공 `data`를 유지했다. target AI가 작성하고 path의
활성 root에 직접 연결된 reply만 허용하는 관리자 ownership 경계를 추가했다.
- 결과: PRD, OpenAPI 2.3.0, 계약 설명, Phase 6 `Task 6.9` / `P6-R4`·Gate와 Phase 7 `P7-R8` 종결 조건을
동기화했다. 전체 계약은 37개이며 기존 36개는 `implemented`, 신규 답변 수정 1개는 `planned`다.
- 검증: 문서와 OpenAPI만 변경했다. JSON 문법·내부 `$ref`·operationId·상태 집계와 diff를 정적으로 검증했고
문서 절차의 `./gradlew tasks --all`만 `BUILD SUCCESSFUL in 742ms`로 확인했다. 사용자 지시에 따라 컴파일·테스트·
lint는 실행하지 않았다.
- 다음 Goal: 기존 실행 순서의 `P2-R10`; FanTalk 기능 순서는 `P6-R3` → `P6-R4`.
### Phase 1~7 6차 정적 리뷰 완료 — 2026-07-29
- 상태: 후속 보완 Task 등록 완료, 구현 대기
- 무엇을: PRD, OpenAPI 36개 operation, controller/facade와 관련 계약 테스트를 현재 working tree 기준으로
Phase별 정적 대조했다.
- Phase 1 결과: 공통 ADMIN 이중 인가, target resolver, 오류/CORS 경계에서 신규 finding 없음.
- Phase 2 결과: Character 생성·수정의 `request` part가 OpenAPI의 `application/json` encoding과 달리
`@RequestPart String`으로 media type을 강제하지 않는 `REV-055`를 확정했다. `Task 2.16` / `P2-R10`으로 전환했다.
- Phase 3 결과: 오디오 댓글·답글 목록이 OpenAPI optional `page`/`size`와 달리 두 query를 필수로 요구하고,
facade도 실제 query 이름을 정확히 두 개 요구하는 `REV-052`를 확정했다. `Task 3.25` / `P3-R15`로 전환했다.
생성·수정 `request` part의 같은 불일치 `REV-056`은 `Task 3.26` / `P3-R16`으로 전환했다.
- Phase 4 결과: Series 생성·수정 `request` part의 같은 불일치 `REV-057`을 `Task 4.13` / `P4-R7`로 전환했다.
- Phase 5 결과: 커뮤니티 댓글 작성·수정 mapping에 JSON `consumes`가 없어 OpenAPI 415 계약을 보장하지 못하는
`REV-053`을 `Task 5.13` / `P5-R7`로 전환했다. 게시글 생성·수정 `request` part의 같은 불일치 `REV-058`은
`Task 5.14` / `P5-R8`로 전환했다.
- Phase 6 결과: FanTalk 답변 작성 mapping에 JSON `consumes`가 없어 OpenAPI 415 계약을 보장하지 못하는
`REV-054`를 확정했다. `Task 6.8` / `P6-R3`으로 전환했다.
- Phase 7 결과: 일곱 소유 Phase Gate 뒤 36개 operation을 재판정하는 `Task 7.10` / `P7-R8`을 추가했다.
- 검증: OpenAPI JSON 문법, 36개 operationId 고유성, requestBody media type, 공통 pagination parameter,
multipart encoding, controller mapping/`consumes`/`@RequestPart`와 dependency·DDL 변경 범위를 정적으로
대조했다. 사용자 지시에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
- 다음 Goal: `P2-R10`.
### `P5-R6` / `P5-R6-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: v2 관리자 커뮤니티 댓글·답글 GET에서 필수 `timezone` query를 제거하고, 기존 `date` 필드만 `createdAt` 기반 ISO-8601 UTC(`Z`)로 정합화했다.
- 왜: `REV-051`이 최신 OpenAPI의 page/size-only query 및 UTC date 계약과 실제 timezone 표시 문자열의 불일치를 확정했기 때문이다.
- 어떻게: controller/facade의 timezone 입력·검증만 제거하고 legacy service/repository 계약은 유지했다. 기존 `toUtcIso()`로 owner-scoped 조회 결과를 재매핑하고, actual endpoint 테스트로 root/reply UTC exact JSON 및 추가 timezone query 무영향을 고정했다.
- 결과: RED 2건 실패 후 focused GREEN, community/common·legacy 영향 범위 회귀, `ktlintCheck`, OpenAPI 36개 `implemented`/0개 `alignment-required`, `git diff --check` 검증을 통과했다. 전체 `./gradlew test`는 v2 커뮤니티 controller/facade/test와 해당 legacy service 경계에 변경을 한정했고 직접 영향 회귀가 이를 포함하므로 실행하지 않았다.
- 남은 항목: 없음. 후속 `P7-R7` 통합 재판정도 완료했다.
### `P7-R7` / `P7-R7-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: UTC 날짜 계약 변경 뒤 36개 관리자 operation의 OpenAPI 상태, controller mapping, timezone parameter/schema 제거, Phase 3·5 Gate 증거를 통합 재판정했다.
- 왜: `REV-050`과 `REV-051` 처리 뒤 최신 계약 기준으로 Phase 7 완료 상태를 복구해야 했기 때문이다.
- 어떻게: OpenAPI `jq` 집계, controller mapping 정적 집계, 오디오·커뮤니티 focused 재실행 및 각 Gate의 영향 범위 회귀·lint·diff 기록을 대조하고 문서 상태를 동기화했다.
- 결과: 36개 operation 모두 `implemented`, `alignment-required` 0개, query `timezone` parameter 0개, `components.parameters.Timezone` 0개, controller mapping 36개를 확인했다. 오디오 focused는 `BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks`는 `BUILD SUCCESSFUL in 4m 33s`였다.
- 남은 항목: 없음.
### `P5-R8` / `P5-R8-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: 커뮤니티 게시글 생성·수정 multipart의 `request` part를 `application/json` 호환 media type으로 제한했다.
- 왜: `REV-058`이 OpenAPI multipart encoding과 `@RequestPart String` permissive binding의 불일치를 확정했기 때문이다.
- 어떻게: create/update actual endpoint에 KO/EN/JA `text/plain` 및 Content-Type 누락 415 matrix를 먼저 추가해 RED를 확인한 뒤, controller에서 part header만 검사하고 기존 facade strict reader와 domain 로직은 유지했다.
- 결과: RED focused는 12개 invocation이 415 기대 실패로 `BUILD FAILED in 56s`, GREEN focused는 `BUILD SUCCESSFUL in 59s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 55s`였다. 전체 `./gradlew test`는 변경 범위가 v2 community 게시글 multipart request part 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 실행하지 않았다.
- 남은 항목: 없음. 다음 Goal은 `P6-R3`이다.
### `P5-R9` / `P5-R9-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: Community post 생성·수정 multipart의 실제 part 이름을 operation별 OpenAPI 허용 집합으로 제한했다.
- 왜: `REV-063`이 생성과 수정의 허용 part 집합이 다른데 controller가 전체 part 이름을 검사하지 않아 수정 `audioFile` 등 미정의 part를 무시하고 mutation을 진행할 수 있다고 확정했기 때문이다.
- 어떻게: controller 경계에서 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` allow-list를 적용하고 초과 part는 400 `common.error.invalid_request`로 거부했다. actual endpoint 테스트로 생성 `unexpected`, 수정 `unexpected`·`audioFile`의 DB/S3 no-side-effect를 고정했다.
- 검증: RED 3건 `BUILD FAILED in 3m 23s`, focused GREEN `BUILD SUCCESSFUL in 2m 30s`, community/common 영향 범위 `BUILD SUCCESSFUL in 1m 44s`, `ktlintCheck` `BUILD SUCCESSFUL in 55s`, `git diff --check` 출력 없음을 확인했다.
- 전체 회귀: `./gradlew test`는 변경이 v2 Community post controller와 실제 Community endpoint test에 한정되고 community/common 영향 범위 회귀가 이를 포함하므로 실행하지 않았다.
- 다음 Goal: `P7-R9`.
### `P6-R3` / `P6-R3-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: FanTalk 답변 작성 POST를 `application/json` request만 받도록 제한했다.
- 왜: `REV-054`가 OpenAPI requestBody media type과 controller mapping의 불일치를 확정했기 때문이다.
- 어떻게: `text/plain` actual endpoint 415 matrix를 먼저 추가해 RED를 확인한 뒤, reply POST mapping에 JSON `consumes`만 추가했다.
- 결과: RED focused는 3개 invocation이 415 기대 실패로 `BUILD FAILED in 33s`, GREEN focused는 `BUILD SUCCESSFUL in 41s`, FanTalk/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 47s`였다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk reply POST media type 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 실행하지 않았다.
- 남은 항목: 없음. 다음 Goal은 `P6-R4`다.
### `P6-R4` / `P6-R4-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: target AI가 작성하고 path의 활성 root에 직접 연결된 FanTalk reply만 수정하는 관리자 PUT endpoint를 추가했다.
- 왜: `REV-059`가 레거시 `PUT /explorer/profile/cheers`의 optional/nullable `content`, `isActive`, no-op, 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data` 계약을 V2 관리자 경계에 이관해야 한다고 확정했기 때문이다.
- 어떻게: `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`에 JSON `consumes`와 strict body reader를 추가하고, repository에서 reply ID·target creator/writer·active root·direct-parent를 한 query로 검증했다. facade는 non-null `content`와 `isActive`만 반영하고 `languageCode`와 event는 변경하지 않는다.
- 결과: RED focused는 15건이 미구현 route 404로 `BUILD FAILED in 49s`, GREEN 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`는 출력이 없었다.
- 전체 회귀: `./gradlew test`는 변경 범위가 v2 FanTalk reply update와 FanTalk/common/legacy 영향 범위에 포함되므로 실행하지 않았다.
- 남은 항목: 없음. 다음 Goal은 `P7-R8`이다.
### `P7-R8` / `P7-R8-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: `REV-052`~`REV-059` 처리 뒤 37개 관리자 operation의 HTTP 경계와 FanTalk 답변 수정 계약을 최종 통합 재판정했다.
- 왜: Phase 2~6의 여덟 소유 Gate가 모두 완료되어 OpenAPI, controller mapping, 회귀, lint, diff와 문서 상태를 하나의 최종 Gate에서 대조해야 했기 때문이다.
- 어떻게: OpenAPI operationId/status, controller mapping 수, 미처리 finding, dependency/DDL 변경 여부를 정적으로 확인하고, 영향 범위 focused 회귀와 전체 `./gradlew test`, `ktlintCheck`, `git diff --check`를 fresh 실행했다.
- 결과: OpenAPI는 operationId 37개/unique 37개/status `implemented` 37개였고 controller mapping은 37개였다. `REV-052`~`REV-059` 미처리 항목과 선행 Gate 미체크 항목은 없었다. 영향 범위 focused 회귀는 `BUILD SUCCESSFUL in 1m 26s`, 전체 `./gradlew test`는 `BUILD SUCCESSFUL in 8m 8s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다. 신규 dependency·DDL 변경도 없다.
- 남은 항목: 없음.
### `P7-R9` / `P7-R9-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: `REV-060`~`REV-064` 처리 뒤 8개 multipart operation의 part 이름 계약과 37개 operation 구현 상태 문서를 통합 재판정했다.
- 왜: Phase 2~5 Gate에서 operation별 multipart allow-list 보완이 끝났고, `api-contract.md`의 route/완료/예정 수와 FanTalk 답변 수정 상태가 과거 36개 구현/1개 planned로 남아 있었기 때문이다.
- 어떻게: 8개 multipart schema의 `additionalProperties: false`, required/props 집계와 Phase 2~5 Gate 증거를 대조하고, OpenAPI operation/status와 controller mapping을 새로 집계한 뒤 `api-contract.md` 상단 집계·endpoint 표·client 생성 설명을 37개 구현 완료로 동기화했다.
- 검증: OpenAPI는 operation 37개, 고유 operationId 37개, `implemented` 37개, `alignmentRequired` 0개, `planned` 0개였다. controller mapping은 Character 5 + AudioContent 10 + Series 10 + Community 8 + FanTalk 4로 총 37개였다. 8개 multipart schema는 모두 `additionalProperties=false`이고 허용 part 집합은 Character/Series `{image, request}`, AudioContent 생성 `{contentFile, coverImage, request}`·수정 `{coverImage, request}`, Community 생성 `{audioFile, postImage, request}`·수정 `{postImage, request}`로 확인했다. `ktlintCheck`는 `P5-R9-GATE`의 `BUILD SUCCESSFUL in 55s` 기록을 대조했고, 문서 수정 후 `git diff --check`를 재실행했다.
- 전체 회귀: production 동작은 `P2-R11-GATE`, `P3-R17-GATE`, `P4-R8-GATE`, `P5-R9-GATE`에서 focused와 영향 범위 회귀로 검증 완료했으므로 이 문서 동기화 Task에서는 실행하지 않았다.
- 남은 항목: 없음.
### `P2-R1` 실행 준비 — 2026-07-27
- 상태: 대기
- 무엇을: Phase 2~7의 Goal ID, 시작 조건, 완료 증거, 범위 밖, 체크박스와 Gate 구조를 준비했다.
- 왜: 기존 Phase 2·3 체크만으로 심층 리뷰와 최종 완료를 판정할 수 없었기 때문이다.
- 어떻게: PRD, 기존 계획과 `docs/sample/sample-plan-task.md`, `docs/sample/sample-review.md`를 대조했다.
- 남은 항목: `P2-R1` read-only 리뷰 실행.
- 다음 행동: 사용자가 goal 실행을 요청하면 `P2-R1`만 `create_goal`에 등록한다.
### `P2-R1` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: Phase 2 character slice의 PRD Feature B, Endpoint Contract Summary, production/test 구현을 대조했다.
- 왜: 기존 `P2-H1`, `P2-H2` 완료 이력 이후 `P2-GATE` 전에 확정 finding을 소유 Goal에 연결해야 하기 때문이다.
- 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`를 작성하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다.
- 결과: focused test는 `BUILD SUCCESSFUL in 51s`였고, `REV-001`~`REV-003`, `REV-007`, `REV-008`을 확정으로 유지해 `P2-T3`~`P2-T6`에 연결했다.
- 남은 항목: `P2-T3` 캐릭터 목록·검색·상세 보완부터 직렬 실행.
### `P2-T3` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 캐릭터 목록 응답을 상세 DTO에서 목록 전용 DTO로 분리했다.
- 왜: 목록 API가 계약에 없는 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다.
- 어떻게: RED로 `AiCharacterAdminCharacterControllerTest` exact field 비노출 assertion을 추가했고, `AiCharacterAdminCharacterListItemResponse`와 `toListItemResponse`를 최소 구현했다.
- 결과: RED는 `AiCharacterAdminCharacterControllerTest` line 66 실패로 확인했고, GREEN 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`와 `./gradlew ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다.
- 남은 항목: `P2-T4` 캐릭터 생성 흐름 보완.
### `P2-T4` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 생성 request의 서버 소유 field를 거부하고, 원작 존재를 외부 캐릭터 생성 전에 확인했다. 정상 생성의 원작 연결·AI creatorMember 표시 정보·언어 감지 event 및 중복·외부 API·S3 실패 결과를 v2/legacy test로 고정했다.
- 왜: `REV-002`, `REV-003`, `REV-007`에서 request 입력 의미와 외부 부작용 전 DB 검증 증거가 부족했기 때문이다.
- 어떻게: `DEC-P2-T4-001`로 canonical contract와 legacy failure boundary를 고정한 뒤 RED/GREEN test를 추가하고, focused/legacy 및 formatting 검증을 실행했다.
- 결과: specified focused/legacy test와 `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. 외부 API 삭제 endpoint나 신규 DB unique 제약은 기존 external contract·DDL 금지 범위 밖이라 추가하지 않았다.
- 남은 항목: `P2-T5` 캐릭터 수정·비활성화 흐름 보완.
### `P2-T5` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: PUT의 response 전용 `externalCharacterId`와 `isActive=false` 혼합 요청을 거부하고, 원작을 외부 수정 전에 검증했다. 일반 수정의 image 유지·교체, AI creatorMember 표시 정보, 번역 event, soft delete의 Member·콘텐츠 보존과 flush 후 `updatedAtUtc`를 회귀로 고정했다.
- 왜: `REV-002`, `REV-003`, `REV-007`에서 update 입력 의미, S3/외부/DB 실패 경계와 response timestamp 증거가 부족했기 때문이다.
- 어떻게: `DEC-P2-T5-001`로 PUT contract와 non-compensated external update 경계를 확정한 뒤 RED/GREEN test를 추가하고 지정 focused/legacy 및 formatting 검증을 실행했다.
- 결과: 지정 focused/legacy test와 `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. 외부 update restore API와 기존 image hard delete는 external contract·legacy parity 범위 밖이라 추가하지 않았다.
- 남은 항목: `P2-T6` Phase 2 보안·오류·회귀 보완.
### `P2-T6` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 문자열 `unmapped-path`를 character resource handler에서 제외해 prefix fallback의 404/i18n/CORS 계약으로 보냈고, 실제 목록·상세·생성·수정 endpoint의 ADMIN·binding/domain/multipart·CORS 증거를 보강했다.
- 왜: 문자열 path가 `Long` binding의 400으로 처리되어 `REV-001`을 위반했고, `REV-008`의 실제 endpoint matrix 증거가 부족했기 때문이다.
- 어떻게: 기존 404 KO/EN/JA·CORS 4건을 RED로 재현하고 GET/PUT path를 `[0-9]+`로 제한했다. 목록 binding, 상세 target, 생성/수정 multipart KO/EN/JA, 실제 네 endpoint non-ADMIN과 detail preflight를 parameterized/focused test로 확인했다.
- 결과: 지정 focused test는 `BUILD SUCCESSFUL in 1m 26s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 34s`였다. `AiCharacterAdminCharacterServiceTest`는 존재하지 않는 과거 계획 참조임을 정정 기록으로 보존했고, legacy/public contract와 DTO 의존 방향은 변경하지 않았다.
- 남은 항목: `P2-GATE`.
### `P2-GATE` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: Phase 2 캐릭터 관리의 PRD 추적성, 정상·실패 흐름, legacy 회귀를 최종 판정했다.
- 왜: Phase 3 실행 전 `P2-R1`, `P2-T3`~`P2-T6`의 완료 증거와 Gate 명령 성공이 필요하기 때문이다.
- 어떻게: Gate에 명시된 세 명령과 `git diff --check`를 실행했다.
- 결과: character focused 명령은 병렬 실행 중 XML 결과 파일 write 충돌로 한 번 실패했으나 동일 명령 단독 재실행은 `BUILD SUCCESSFUL in 1m 11s`였다. authorization/error 명령은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. `REV-001`~`REV-003`, `REV-007`, `REV-008`의 Phase 2 소유 항목은 처리 완료로 판정했다.
- 남은 항목: `P3-R1` Phase 3 요구사항·계약·코드 리뷰.
### `P3-T7` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 실제 Phase 3 테마·목록·상세·생성·수정 endpoint의 ADMIN/stale claim, CORS, ownership no-side-effect와 malformed resource path 오류 계약을 독립 focused test로 고정했다.
- 왜: 기존 공통 authorization/error test는 prefix 공통 계약을, 콘텐츠 focused test는 domain ownership을 보장했지만 실제 endpoint가 문자열 식별자를 404 fallback으로 보내는 증거가 없었다.
- 어떻게: `AiCharacterAdminAudioContentOwnershipTest`에서 malformed `characterId`/`contentId` RED를 먼저 확인하고, controller resource path를 `[0-9]+`로 제한했다. 기존 콘텐츠/authorization/error focused regression과 legacy characterization을 함께 실행했다.
- 결과: RED는 6건의 400/415 대 404 불일치로 확인했고, 최소 path 제약 적용 후 ownership focused 18건과 지정 content+authorization+error 회귀가 모두 `BUILD SUCCESSFUL`이었다. `ktlintCheck`도 통과했다.
- 남은 항목: `P3-GATE`.
### `P3-GATE` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: Phase 3 콘텐츠 관리와 signed URL의 PRD 추적성, pipeline 안전성, legacy 회귀를 최종 판정했다.
- 왜: Phase 4 진행 전 `P3-R1`, `P3-T3`~`P3-T7`의 완료 증거와 Gate 명령 성공이 필요하기 때문이다.
- 어떻게: Gate에 명시된 content focused, authorization/error, `ktlintCheck` 세 명령을 fresh 실행했다.
- 결과: content focused 명령은 `BUILD SUCCESSFUL in 2m 15s`, authorization/error 명령은 `BUILD SUCCESSFUL in 1m 29s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. `REV-004`~`REV-008`의 Phase 3 소유 항목은 처리 완료로 판정했다.
- 남은 항목: `P4-T1` 시리즈 요구사항·계약·코드 리뷰.
### Phase 2·3 2차 리뷰 완료 — 2026-07-27
- 상태: 리뷰 완료, 후속 수정 Goal 대기
- 무엇을: Phase 2·3 production/test와 기존 review·Gate 완료 기록을 PRD와 다시 대조했다.
- 왜: 기존 focused test 성공뿐 아니라 각 완료 체크박스가 요구한 실제 endpoint·failure-order 증거가 존재하는지 독립 검증하기 위해.
- 어떻게: 두 review 문서에 2차 리뷰를 누적하고 character/content/common test를 `--rerun-tasks`로 한 번에 실행한 뒤 XML별 test 수와 lint를 확인했다.
- 결과: targeted 14개 XML class의 199건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 9m 23s`, `ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 27s`였다. 기능 실패는 재현되지 않았으나 완료 기록보다 직접 증거가 좁은 `REV-009`~`REV-011`을 확정했다.
- 남은 항목: `P2-R2` → `P2-R2-GATE` → `P3-R2` → `P3-R3` → `P3-R2-GATE`.
### `P3-R2` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 실제 생성 endpoint의 multipart part별 KO/EN/JA 오류, invalid theme 선검증, cover upload 실패 rollback/event 0회 증거를 보강했다.
- 왜: `REV-010`에서 기존 생성 완료 증거가 계획의 직접 증거보다 좁다고 확정됐기 때문이다.
- 어떻게: `AiCharacterAdminAudioContentCreateTest`를 확장했고, 실패 원인은 production 계약 위반이 아니라 테스트 기대 message와 `NOT_SUPPORTED` fixture의 transaction 누락임을 확인해 테스트만 최소 정정했다.
- 결과: create 단독 명령은 `BUILD SUCCESSFUL in 1m 4s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
- 남은 항목: `P3-R3`.
### `P3-R3` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 수정 차집합, cover 유지/교체/실패, ownership/domain no-side-effect 증거를 Phase 3 actual endpoint 범위에서 보강했다.
- 왜: `REV-011`에서 기존 수정·ownership 완료 증거가 계획의 직접 증거보다 좁다고 확정됐기 때문이다.
- 어떻게: 기존 Phase 3 content test 보강분을 content/common 회귀로 재검증했다.
- 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
- 남은 항목: `P3-R2-GATE`.
### `P3-R2-GATE` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: Phase 3 후속 리뷰의 `REV-010`~`REV-011` 처리를 종결했다.
- 왜: 사용자 지시에 따라 Phase 3 후속 보완까지만 진행하고 Phase 4 이후는 시작하지 않기 위해서다.
- 어떻게: `phase3-audio-content-review.md`에 3차 후속 검증 기록을 누적하고 content/common 회귀와 `ktlintCheck`를 실행했다.
- 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. `ktlintCheck` 결과는 검증 기록에 누적한다.
- 남은 항목: 없음. Phase 4는 진행하지 않는다.
### Phase 2·3 4차 재리뷰 완료 — 2026-07-27
- 상태: 리뷰 완료, 후속 처리 요청(당시 판정)
- 무엇을: `P2-R2`, `P3-R2`~`P3-R3` 반영분과 완료 체크리스트를 production 흐름·test method 단위로 다시 대조했다.
- 왜: 회귀 통과만으로 `REV-009`~`REV-011`의 선언된 실패·ownership 증거 전체가 충족됐다고 판정할 수 없기 때문이다.
- 어떻게: 변경된 test 5개, character/content facade와 legacy service failure order를 확인하고 targeted 전체를 `--rerun-tasks`로 실행했다.
- 결과: 관련 14개 XML class의 228건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 9m 44s`, `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 41s`였다. 적용된 테스트는 유효하지만 DB/event 실패, 생성 후반 S3/event 실패와 ownership no-side-effect matrix가 완료 기록보다 좁아 `REV-012`~`REV-014`를 확정했다.
- 남은 항목: `P2-R3` → `P2-R3-GATE` → `P3-R4` → `P3-R3-GATE`.
### `P2-R3` / `P2-R3-GATE` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: character 생성·수정의 external/S3 실패 locale matrix와 event publish 실패 경계를 보강했다.
- 왜: `REV-012`에서 DB/event 실패 뒤 내부 rollback과 external/S3 잔존 결과 직접 증거가 부족하다고 확정됐기 때문이다.
- 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 KO/EN/JA 대표 실패와 facade 직접 event failure characterization을 추가했다.
- 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`였다.
- 남은 항목: `P3-R4`.
### `P3-R4` / `P3-R3-GATE` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: content 생성의 audio upload/event 실패와 실제 네 경로 unknown target no-side-effect matrix를 보강했다.
- 왜: `REV-013`~`REV-014`에서 생성 후반 실패 순서와 ownership/domain 부작용 없음 증거가 부족하다고 확정됐기 때문이다.
- 어떻게: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentOwnershipTest`를 확장하고 content/common 회귀와 `ktlintCheck`를 실행했다.
- 결과: create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`, `ktlintCheck`는 import 정리 후 `BUILD SUCCESSFUL in 17s`였다.
- 남은 항목: 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다.
### Phase 2·3 5차 재리뷰 완료 — 2026-07-27
- 상태: 리뷰 완료, 후속 수정 Goal 대기
- 무엇을: `P2-R3`, `P3-R4` 반영분의 transaction/rollback, multipart binding, ownership/domain 완료 증거를 PRD·production·test method 단위로 다시 대조했다.
- 왜: 통과하는 테스트가 기존 finding의 exact exception, actual transaction과 전체 side-effect assertion을 실제로 보장하는지 확인하기 위해서다.
- 어떻게: `phase2-character-review.md`, `phase3-audio-content-review.md`에 5차 리뷰를 누적하고 변경된 핵심 test 세 클래스를 `--rerun-tasks`로 실행한 뒤 XML 수치, lint와 diff check를 확인했다.
- 결과: 세 XML 합계 79건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 7m 50s`, `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 28s`, `git diff --check`는 출력이 없었다. 테스트는 통과했지만 `REV-015`~`REV-017`의 계약·완료 증거 누락을 확정했다.
- 남은 항목: `P2-R4` → `P2-R4-GATE` → `P3-R5` → `P3-R6` → `P3-R4-GATE`.
### `P3-R5` 완료 — 2026-07-27
- 상태: 완료
- 무엇을: 생성 controller와 facade의 `coverImage`·`audioFile`을 non-null `MultipartFile`로 고정하고, 세 필수 part 누락 KO/EN/JA actual endpoint 9건의 exact MVC binding 계약을 보강했다.
- 왜: nullable 파일 part가 facade와 legacy `AudioContentService`까지 전달되어 `MissingServletRequestPartException` 및 `common.error.invalid_request` 계약을 우회했기 때문이다.
- 어떻게: RED에서 `coverImage`·`audioFile` 누락 6건이 legacy content 전용 message로 실패함을 확인한 뒤, non-null binding으로 변경했다. 공통 helper는 각 요청의 400 `ApiResponse.error`, exact exception, DB count 0, S3 `putObject` 0회, event no-interaction을 단언한다.
- 결과: create/error focused 회귀는 `BUILD SUCCESSFUL in 1m 1s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. 전체 `./gradlew test`는 controller binding의 직접 영향 범위를 두 targeted 명령이 포함하고 release/Gate 범위가 아니므로 실행하지 않았다.
- 남은 항목: `P3-R6` 및 `P3-R4-GATE`.
### `P3-R6` 완료 — 2026-07-27
- 상태: 완료. `P3-R4-GATE`는 `P2-R4-GATE` 뒤 실행 대기다.
- 무엇을: `AiCharacterAdminAudioContentControllerTest`의 cross-owner detail/update, create/update 다른 owner `seriesIds`, invalid `releaseDateUtc` 실제 endpoint를 각각 KO/EN/JA matrix로 확장했다.
- 왜: 기존 단일 locale 또는 부분 assertion으로는 ownership/domain 검증이 S3 업로드, DB 변경, event publish보다 앞선다는 완료 증거가 부족했다.
- 어떻게: 테스트 우선으로 exact 400 `ApiResponse.error` envelope, 요청 전후 `AudioContent`·`SeriesContent` count와 field/연결 row, S3 `putObject` 0회, `ApplicationEventPublisher` no-interaction을 단언했다. target과 other-owner ID를 분리한 fixture라 owner/domain guard를 제거하면 success/status 또는 state assertion이 실패한다.
- 결과: focused characterization은 `BUILD SUCCESSFUL in 34s`로 기존 production 계약 충족을 확인해 test-only로 종료했다. content/common 회귀는 `BUILD SUCCESSFUL in 1m 13s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 12s`였다. 전체 `./gradlew test`는 변경이 Phase 3 content endpoint 테스트에 한정되고 focused·content/common 명령이 직접 범위를 포함하므로 실행하지 않았다.
- 남은 항목: `P2-R4-GATE` 후 `P3-R4-GATE`. Phase 4는 진행하지 않는다.
### `P2-R4-GATE` / `P3-R4-GATE` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: Phase 2·3 5차 리뷰 보완의 최종 Gate를 종결했다.
- 왜: `REV-015` actual transaction evidence, `REV-016` exact multipart binding, `REV-017` ownership/domain matrix가 모두 보강됐는지 fresh 검증으로 판정하기 위해서다.
- 어떻게: character mutation/error와 content create/controller/ownership focused 명령 및 `git diff --check`를 실행하고 review 문서의 상태를 처리 완료로 갱신했다.
- 결과: focused 명령은 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다. `P2-R4-GATE`, `P3-R4-GATE` 모두 완료했고 Phase 4는 사용자 진행 지시 전까지 시작하지 않는다.
- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시가 필요하다.
### Phase 2·3 6차 리뷰 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 5차 보완 결과와 Endpoint Contract Summary, multipart empty-file 경계, Phase 3 event no-interaction의 실제
관찰 대상을 다시 대조했다.
- 왜: 통과하는 focused test와 완료된 review ID 외에 client contract 위반, 제한 조건의 파일 손상 가능성 또는
detached mock으로 가려진 증거 공백이 남았는지 확인하기 위해서다.
- 어떻게: character/content production과 관련 test를 정적 대조하고 Phase 2·3 5차 Gate focused 5개 class를
`--rerun-tasks`로 재실행했다. 두 review 문서에 `REV-018`~`REV-020`을 누적하고 각 finding을 독립 Task/Gate에 연결했다.
- 결과: focused XML 합계 216건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 4m 16s`,
`ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 17s`, staged/unstaged `git diff --check`는 출력이 없었다.
테스트 통과와 별개로 문서 계약 1건, production empty-file 경계 1건, test evidence 1건을 확정했다.
- 보완 결과: `P2-R5`, `P2-R5-GATE`, `P3-R7`, `P3-R8`, `P3-R5-GATE`를 완료했다.
- 최종 검증: content/common 회귀는 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`, `git diff --check`는 출력이 없었다.
- 남은 항목: 없음. Phase 4는 사용자 진행 지시 전까지 시작하지 않는다.
### Phase 2·3 6차 보완 재점검 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `P2-R5`, `P2-R5-GATE`, `P3-R7`, `P3-R8`, `P3-R5-GATE`의 실제 코드·테스트·완료 기록을 다시
대조하고 하단 종합 finding 상태를 점검했다.
- 왜: 6차 보완의 empty-file·actual publisher·문서 계약 수정이 실제로 유지되는지와 완료된 finding이 미처리 상태로
남아 있지 않은지 확인하기 위해서다.
- 어떻게: Phase 2 character mutation과 Phase 3 create/update/controller/ownership 5개 class를 `--rerun-tasks`로
실행하고, `REV-001`~`REV-020`의 소유 Goal·Gate·Progress 기록을 하단 종합 표와 대조했다.
- 결과: 5개 XML 합계 130건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 4m 19s`였다. 6차 보완 범위의 추가
production 결함은 재현되지 않았다. 별도 문서 문제로 종결된 `REV-001`~`REV-009`가 종합 표에서 `확정`으로 남은 상태
불일치를 확인해 `처리 완료`로 동기화했다.
- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시 전까지 시작하지 않는다.
### `P23-CONTRACT-1` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 캐릭터, 테마·오디오 콘텐츠, 시리즈, 커뮤니티, FanTalk의 신규 관리자 API 23개 request/response를
OpenAPI 3.1 JSON과 설명 문서로 고정했다.
- 왜: 신규 path와 관리자 target만 적용하고 클라이언트 JSON 계약은 레거시 필드명·타입·optional/nullable·기본값과 성공
`data` 형태를 그대로 이관해야 하기 때문이다.
- 어떻게: 레거시 Kotlin DTO·controller·service와 schema를 대조하고 path ID만 body에서 제거했다. FanTalk 답변 축약 응답,
관리자 FanTalk 목록, 분리된 시리즈 미연결 콘텐츠 검색만 확정 예외로 반영했다. 독립 리뷰에서 확인한
`Accept-Language` fallback, 캐릭터 비활성화 혼합 입력, 시리즈 상세 문자열 `state`, Phase 4 선행 Gate 문제를 교정했다.
- 결과: JSON parse, 내부 `$ref` 누락 0건, 고유 operation 23개와 상태 9/14를 확인했다. Redocly lint와 OpenAPI Generator
validate가 통과했고 `typescript-fetch` 생성 후 TypeScript 5.9.3 `tsc --noEmit`도 성공했다.
`./gradlew tasks --all`은 `BUILD SUCCESSFUL in 898ms`였으며 독립 최종 리뷰는 Critical 0, Important 0이었다.
- 남은 항목: 현재 구현된 캐릭터 4개 endpoint의 runtime DTO를 맞추는 `P23-CONTRACT-2`. production code는 변경하지 않았다.
### `P23-CONTRACT-2`~`P7-GATE` 실행 계획 보완 — 2026-07-28
- 상태: 구현 시작 준비 완료
- 무엇을: `P23-CONTRACT-2`·`P23-CONTRACT-3`의 실제 facade와 전용 query/theme/ownership/legacy test 범위를 보강하고,
Task별 focused 명령을 추가했다. Phase 4~6은 각 OpenAPI tag를 정식 schema로, Phase 7은 OpenAPI 23개 operation을
최종 구현 기준으로 명시했다.
- 왜: 확정 계약이 있어도 실제 request parsing·response 조립을 담당하는 facade와 전용 테스트가 계획에서 누락되면
구현 중 범위가 다시 흔들릴 수 있기 때문이다.
- 어떻게: OpenAPI operation 23개와 Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2 분류를 Phase/Goal과
대조하고 기존 source/test 파일 존재, Goal ID 중복, 선행 Gate와 PRD Open Questions를 확인했다.
- 결과: 과거 축약 예시는 비규범 이력으로 분리했고 현재 Endpoint Contract Summary는 OpenAPI와 같은 9/14 상태 및 예외만
제공한다. Task 3.17부터 `P7-GATE`까지 시작 조건·파일·RED/GREEN/REFACTOR·검증 명령이 연결됐으며
`./gradlew tasks --all`은 `BUILD SUCCESSFUL in 922ms`였다.
- 제외: 생성 TypeScript의 `tsc` 실행은 클라이언트 build/CI 책임으로 두고 server runtime `P23-CONTRACT-GATE`에는 추가하지
않았다. `P23-CONTRACT-1`의 일회성 생성·컴파일 검증 기록은 유지한다.
- 다음 행동: production 변경 없이 문서 보완만 완료했다. 구현 시작 Goal은 `P23-CONTRACT-2`다.
### `P23-CONTRACT-2` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 캐릭터 목록·검색은 레거시 `totalCount/content`와 list item 필드를, 상세는 `id/characterUUID`와 전체 nested 필드를
반환하도록 기존 레거시 DTO mapper를 재사용했다. 생성은 `image`를 필수로 받고 생성·수정 성공은 exact `data: null`을 반환하며,
`isActive=false`와 다른 optional JSON field의 혼합 요청은 받아 비활성화만 반영하도록 했다.
- 왜: 현재 v2 전용 축약 DTO, optional 생성 이미지, mutation 상세 응답과 단독 soft-delete 제한이
`api-contract.openapi.json`의 확정 레거시 runtime 계약과 달랐기 때문이다.
- 어떻게: 지정된 두 actual endpoint 테스트에 list/detail exact field, 전체 create/update request, 필수 image, null mutation envelope,
mixed soft-delete 미반영 assertion을 RED로 추가한 뒤 controller/DTO/facade/mapper만 최소 변경했다.
- 결과: RED는 59건 중 의도한 9건 실패였고, GREEN focused 59건과 character/authorization/error 회귀 170건이 모두 통과했다.
`./gradlew ktlintCheck`도 `BUILD SUCCESSFUL in 48s`였다.
- 독립 리뷰 보완: Character list의 OpenAPI `size` minimum 1을 실제 pagination에 반영했다. 추가 RED는 focused 60건 중 1건
실패였고, 보완 후 focused 60건과 character/authorization/error 회귀 171건 및 `ktlintCheck`가 모두 통과했다. mixed
soft-delete는 미존재 `originalWorkId`까지 검증·반영 없이 무시함을 actual endpoint test로 강화했다.
- 남은 항목: `P23-CONTRACT-3`은 시작하지 않았다. Task 3.18과 Phase 4 이후 범위는 변경하지 않았다.
### `P23-CONTRACT-3` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 테마·오디오 콘텐츠 5개 endpoint를 `id/theme/image`, `search_word`와 legacy 목록 item, 필수 `timezone`과
`GetAudioContentDetailResponse`, 생성 `contentFile`·`CreateAudioContentRequest`·`data.contentId`, 수정
`UpdateCreatorAdminContentRequest`·`data: null` 계약으로 정합화했다.
- 왜: 기존 v2 전용 `description`, `releaseDateUtc`, `audioSignedUrl`, `status`, `seriesIds`, `themeName`, `imageUrl` alias와
생성·수정 상세 응답이 `api-contract.openapi.json`의 확정 레거시 runtime 계약과 달랐기 때문이다.
- 어떻게: exact JSON·query·multipart RED를 먼저 실행한 뒤 기존 legacy DTO와 목록 service를 재사용하고, 상세 owner guard,
create/update service 위임, signed URL 만료 계산과 기존 series row 보존을 유지했다.
- 결과: RED는 focused 61건 중 21건이 의도대로 실패했다. GREEN focused 61건과 content·authorization·error 10개 suite
206건이 failure/error/skipped 0으로 통과했고 `ktlintCheck`와 문서 변경 후 `./gradlew tasks --all`도 성공했다.
- 남은 항목: `P23-CONTRACT-GATE`. Phase 4 이후 범위는 변경하지 않았다.
### `P4-T1` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `LegacyCreatorAdminSeriesCharacterizationTest`로 기존 creator-admin series 생성·활성 목록·inactive 상세·혼합
수정/soft delete, 콘텐츠 부분 연결·무해한 해제·owner 전체 count·미연결 검색과 owner-less 순서 변경을 고정했다.
- 왜: Phase 4 v2가 재사용할 legacy 동작과 그대로 복제하면 안 되는 ownership·부분 성공·오류 status 경계를 production 변경 전에
분리해야 하기 때문이다.
- 어떻게: 실제 Spring/JPA/QueryDSL repository와 production service를 사용하고 S3 client와 event publisher만 격리한 focused
characterization을 첫 실행했으며, 이어 `ktlintCheck`를 실행했다.
- 결과: focused test는 `BUILD SUCCESSFUL in 47s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 19s`였다. production, DB schema,
dependency, security와 Task 3.17/3.18 코드는 변경하지 않았다.
- 오류 결정: legacy 입력 validation key는 신규 prefix에서 400으로 보존하고, missing/inactive/cross-owner 및 order/link 사전
검증 실패는 400 `common.error.invalid_request`, 예상하지 못한 오류는 500 `common.error.unknown`과 KO/EN/JA envelope로
고정했다. invalid ownership은 모든 DB/S3/event보다 먼저 실패해야 하며 legacy owner-less order path는 신규 v2에서 재사용하지 않는다.
- 남은 항목: `P4-T2` 시리즈 목록·상세 조회 구현. 전체 `./gradlew test`는 production 변경이 없는 test-only baseline이고 실제
service/repository를 통과한 focused 검증으로 직접 범위를 확인했으므로 실행하지 않았다.
- 테스트 결과 확인: JUnit XML 기준 7건, failure/error/skipped 0건이며 재실행도 `BUILD SUCCESSFUL in 2s`였다.
- 문서 명령 유효성: 문서 갱신 후 `./gradlew tasks --all`을 실행해 `test`, `ktlintCheck`, `tasks` 존재와
`BUILD SUCCESSFUL in 940ms`를 확인했다.
- 최종 fresh 검증: `./gradlew test --rerun-tasks --tests
kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.LegacyCreatorAdminSeriesCharacterizationTest`는 10개 task를 모두 실행해
`BUILD SUCCESSFUL in 4m 19s`, JUnit 7건 failure/error/skipped 0건이었다. `./gradlew ktlintCheck --rerun-tasks`는 7개 task를
모두 실행해 `BUILD SUCCESSFUL in 25s`였다. 컴파일의 기존 deprecated API warning 외 신규 경고·실패는 없었다.
### `P4-T2` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `/api/v2/admin/ai-characters/{characterId}/series` 목록과
`/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` 상세를 추가했다. 목록은 target owner의 활성 시리즈만 legacy
`totalCount/items`와 전체 item 필드로 반환하고, 상세는 활성 owner 리소스만 legacy 문자열 필드로 반환한다.
- 왜: AI 캐릭터 creator member로 로그인하지 않고도 관리자가 target resolver를 통해 해당 캐릭터의 시리즈를 읽되, legacy의
inactive 상세 허용과 cross-owner 접근을 신규 v2 경계로 가져오면 안 되기 때문이다.
- 어떻게: `AiCharacterAdminTargetResolver`를 먼저 실행하고 기존 creator series 목록 서비스와 legacy DTO를 재사용했다. 상세는
owner 조회 뒤 `isActive`를 확인하고 기존 entity의 detail mapping을 사용했으며, OpenAPI 최소값대로 `page>=0`, `size>=1`만
허용했다.
- 결과: production 전 RED 5건은 모두 미구현 404로 실패했다. 구현 후 focused 5건과 Task 4.1 포함 series 12건이 모두
failure/error/skipped 0으로 통과했고 `ktlintCheck`도 성공했다. mutation, 콘텐츠 연결·검색과 순서 변경은 구현하지 않았다.
- 남은 항목: `P4-T3` 시리즈 생성·수정·soft delete 구현. 전체 `./gradlew test`는 변경 범위가 신규 series 조회 slice에 한정되고
실제 Spring MVC/JPA 경계를 통과한 focused·legacy series 12건으로 직접 범위를 확인했으므로 실행하지 않았다.
- 문서 명령 유효성: Task 4.2 체크박스와 Progress 갱신 후 `./gradlew tasks --all`을 실행해 `test`, `ktlintCheck`, `tasks`가
존재하고 `BUILD SUCCESSFUL in 930ms`임을 확인했다.
- 독립 리뷰 보완: Phase 4 결정에 맞춰 inactive AI character target의 목록·상세를 400 `common.error.invalid_request`로
차단했다. 보완 RED 2건은 200으로 실패했고 공통 active target guard 적용 후 focused 7건, series 전체 14건과
`ktlintCheck`가 모두 통과했다. list item은 동일 DTO serializer를 공유하고 요일 배열 순서는 OpenAPI/legacy에서 고정하지 않으므로
item별 field set 반복과 임의 순서 고정은 추가하지 않았으며, pagination은 최소값·다음 page·잘못된 하한을 이미 직접 검증한다.
### `P4-T3` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: target owner의 시리즈 POST 생성, multipart PUT 수정과 DELETE soft delete를 추가하고 성공 `data: null`을 유지했다.
- 왜: legacy creator mutation을 재사용하면서도 inactive target/series와 missing·cross-owner 요청은 mutation 전에 같은 400으로
차단해야 했기 때문이다.
- 어떻게: 활성 target과 활성 owned series를 먼저 확인하고, 양수 `genreId`는 active genre 존재를 사전 확인한 뒤
`CreateSeriesRequest`, `ModifySeriesRequest`로 `CreatorAdminContentSeriesService.createSeries`/`modifySeries`를 호출했다.
keyword·S3·event·entity 갱신은 복제하지 않았다.
- 결과: RED 12건은 미구현 405로 실패했고, GREEN focused 12건과 fresh series 회귀 26건이 통과했다. 독립 리뷰에서 누락 genre
사전 검증을 보완해 focused 14건과 series 회귀 28건 및 `ktlintCheck`, `git diff --check`가 통과했다. invalid mutation의
DB/S3/event 0건과 legacy validation key 400을 실제 endpoint에서 확인했다.
- 남은 항목: `P4-T4` 시리즈 콘텐츠 조회·검색·연결·해제. Task 4.4 이후 범위는 변경하지 않았다.
### `P4-T4` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `/series/{seriesId}/contents` 연결 목록, `/contents/search` 미연결 검색, JSON `contentIdList` 연결과 JSON `contentId`
해제를 추가했다. 조회는 legacy DTO/owner 전체 link `totalCount`를 유지하고 mutation은 `data: null`을 반환한다.
- 왜: legacy 연결은 foreign/missing ID를 건너뛰고 해제 없는 link를 no-op 처리하므로, 신규 v2 경계에서 target·active owned series와
모든 content/link 상태를 mutation 전에 검증해 부분 반영을 막아야 했기 때문이다.
- 어떻게: legacy 목록/검색/연결/해제 service를 재사용하되, v2 repository에서 owner의 processed 또는 reserved eligible content를
확인하고 이미 연결된 content, duplicate ID, 없는 link를 `common.error.invalid_request`로 차단했다. 빈 목록은 legacy와 같이
`creator.admin.series.no_content_added`를 유지했다.
- 결과: production 전 RED 7건은 미구현 route의 404/405로 실패했고, 구현 후 focused 7건, series 회귀, Phase 3 content 회귀와
`ktlintCheck`가 모두 통과했다. Task 4.5 순서 변경과 Task 4.6 보안 matrix는 변경하지 않았다.
### `P4-T6` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: Phase 4 series endpoint의 ADMIN 이중 인가/stale claim matrix, target·series·content·pagination·order·binding 오류 envelope,
cross-owner mutation no-side-effect와 legacy series 회귀를 고정했다.
- 왜: Phase 4 series API가 legacy creator endpoint를 재사용하더라도 신규 관리자 prefix에서는 owner-first validation, KO/EN/JA
`common.error.invalid_request` envelope와 legacy regression이 endpoint 단위로 증명되어야 하기 때문이다.
- 어떻게: `AiCharacterAdminAuthorizationTest`에 series list/detail/contents/search/link/unlink/order/create/update/delete endpoint matrix를
추가했고, `AiCharacterAdminSeriesContractTest`에 inactive target과 active target의 missing/inactive/cross-owner series 오류를
분리해 검증했다. Cross-owner PUT은 DB title, event publisher, S3 `putObject`가 변하지 않음을 단언한다.
- 결과: contract focused test는 `BUILD SUCCESSFUL in 3m 41s`, authorization focused test는 순차 재실행에서
`BUILD SUCCESSFUL in 3m 3s`, Phase 4 series 회귀는 `BUILD SUCCESSFUL in 3m 23s`, `ktlintCheck`는
`BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. 병렬 Gradle 실행 중 한 authorization run은 unrelated
`DefaultHomeRecommendationQueryRepository` QueryDSL 참조 compile 오류로 실패했으나 동일 명령 순차 재실행은 통과했다.
- 남은 항목: `P4-GATE`.
### `P4-GATE` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: Phase 4 series vertical slice의 조회·mutation·content link·order·보안/오류 계약과 legacy 회귀를 최종 판정했다.
- 왜: Phase 5 community 구현으로 넘어가기 전에 `P4-T1`~`P4-T6`의 완료 증거와 Gate 명령 성공을 문서와 실제 검증으로 맞춰야 하기 때문이다.
- 어떻게: Gate에 명시된 Phase 4 series 전체 focused 회귀와 `ktlintCheck`를 `--rerun-tasks`로 fresh 실행하고 `git diff --check`를 확인했다.
- 결과: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 21s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 24s`, `git diff --check`는 출력이 없었다.
- 남은 항목: `P5-T1` 커뮤니티 기존 parity 특성화 baseline. Phase 5 production 구현은 아직 시작하지 않았다.
### `P5-T1` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 기존 community 생성 validation, media upload, FCM/recent-news side effect, 최대 고정 3개와 soft delete fixed clearing을
`LegacyCommunityPostCharacterizationTest`로 고정했다.
- 왜: 신규 v2 community 관리자 구현 전에 legacy parity와 신규 owner-first 오류 정책을 분리해 Phase 5 구현 기준을 흔들리지 않게 하기 위해서다.
- 어떻게: 기존 `CreatorCommunityService`를 mock dependency로 직접 실행하는 characterization test를 추가하고 production code는 변경하지 않았다.
- 결과: focused characterization은 `BUILD SUCCESSFUL in 2m 22s`, community package targeted test는 `BUILD SUCCESSFUL in 2m 22s`,
`ktlintCheck`는 unused import 2건 정리 후 `BUILD SUCCESSFUL in 21s`, `git diff --check`는 출력이 없었다.
- 남은 항목: `P5-T2` 관리자 게시글 목록 조회 구현.
### `P6-T2` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `/api/v2/admin/ai-characters/{characterId}/fan-talks`를 추가해 target owner의 활성 root FanTalk와 활성 creator
reply를 공개 v2 `CreatorChannelFanTalkTabResponse` 필드 형태로 반환했다.
- 왜: 공개 v2 조회는 viewer/block 조건을 적용하므로, 관리자 endpoint는 target resolver가 해석한 `creatorMember` 기준의
별도 query가 필요하다.
- 어떻게: root는 `createdAt desc, id desc`, reply는 `createdAt asc, id asc`으로 조회하고 page/size/hasNext/count를
전용 facade에서 조립했다. test는 차단 관계가 있는 관리자 fixture에서도 writer root가 포함되는지, 다른 target·inactive·fan
reply·inactive/nested reply가 제외되는지 확인했다.
- 결과: RED는 4건 모두 미매핑 404로 `BUILD FAILED in 1m 6s`였고, 구현 후 focused 재실행은
`BUILD SUCCESSFUL in 1m 3s`였다. legacy characterization은 `BUILD SUCCESSFUL in 9s`, FanTalk package 회귀는
`BUILD SUCCESSFUL in 51s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 24s`였다.
- 남은 항목: `P6-T3` FanTalk root reply 저장 구현.
### `P6-T3` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`가 선택 target의 활성 root에만 creator reply를 저장하고 축약 응답을 반환하도록 구현했다.
- 왜: 관리자 principal이 아닌 target의 `creatorMember`를 writer/creator로 저장하고, 레거시와 같은 `CREATOR_CHEERS` 언어 감지를 유지해야 하기 때문이다.
- 어떻게: `AiCharacterAdminFanTalkFacade`의 write transaction 안에서 active target과 owner-scoped active root를 조회한 뒤 `CreatorCheers(languageCode = null)`를 저장하고 `LanguageDetectEvent`를 발행했다. `AiCharacterAdminFanTalkReplyCreateTest`는 response, parent/member/creator row, blank languageCode와 event payload를 실제 MVC/JPA 경계에서 검증했다.
- 검증 기록(RED): production 변경 전 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyCreateTest`를 실행했다. 기대한 200 대신 미구현 endpoint 때문에 `AiCharacterAdminFanTalkReplyCreateTest.kt:98`에서 실패했고 `BUILD FAILED in 48s`였다.
- 검증 기록(GREEN): 같은 focused 명령을 다시 실행해 `BUILD SUCCESSFUL in 49s`를 확인했다.
- 검증 기록(legacy): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`를 실행해 `BUILD SUCCESSFUL in 8s`를 확인했다.
- 검증 기록(package): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`를 최종 실행해 `BUILD SUCCESSFUL in 11s`를 확인했다.
- 검증 기록(lint): `./gradlew ktlintCheck`를 실행해 `BUILD SUCCESSFUL in 32s`를 확인했다.
- 검증 기록(diff): `git diff --check`를 실행해 출력 없이 exit code 0을 확인했다.
- 전체 `./gradlew test`는 신규 관리자 FanTalk reply slice에 변경을 한정했고 focused, legacy characterization, FanTalk package 회귀가 직접 범위를 포함하므로 실행하지 않았다.
- 남은 항목: `P6-T4` target/root/ownership 거부 구현.
### `P6-T4` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `AiCharacterAdminFanTalkReplyOwnershipTest`로 cross-character root, nested parent, inactive root, missing FanTalk,
inactive target의 답변 거부를 KO/EN/JA로 고정했다.
- 왜: P6-T3의 owner-scoped active-root 조회와 target active guard가 reply 저장과 `LanguageDetectEvent` 발행보다 앞서는지 실제
MVC/JPA 경계에서 증명하기 위해서다.
- 어떻게: 각 거부 요청에서 `ApiResponse.error` 400 `common.error.invalid_request` locale envelope, `CreatorCheers` row count
무변경, reflection으로 교체한 실제 facade `ApplicationEventPublisher`의 무호출을 단언했다.
- TDD 예외/특성화: production 변경 전 새 ownership test의 첫 실행이 `BUILD SUCCESSFUL in 1m 1s`였고, 10 actionable tasks 중
3 executed, 7 up-to-date였다. 이는 요구한 거부 분기가 이미 P6-T3에 존재함을 확인한 결과이므로 production code를 변경하지
않았다.
- 검증 기록(focused): import 정리 후 `./gradlew test --tests
kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyOwnershipTest`를 재실행해
`BUILD SUCCESSFUL in 55s`, 10 actionable tasks 중 3 executed, 7 up-to-date를 확인했다.
- 검증 기록(reply create/legacy): `./gradlew test --tests
kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyCreateTest --tests
kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`는
`BUILD SUCCESSFUL in 56s`, 10 actionable tasks 중 1 executed, 9 up-to-date였다.
- 검증 기록(package): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`는
`BUILD SUCCESSFUL in 55s`, 10 actionable tasks 중 1 executed, 9 up-to-date였다.
- 검증 기록(lint): 첫 `./gradlew ktlintCheck`는 새 test의 unused import 1건으로 `BUILD FAILED in 16s`였고, 해당 import만
제거한 뒤 재실행은 `BUILD SUCCESSFUL in 26s`, 7 actionable tasks 중 2 executed, 5 up-to-date였다.
- 검증 기록(문서 명령): `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, 1 actionable task executed였고,
`test`, `ktlintCheck`, `tasks`가 존재함을 확인했다.
- 검증 기록(diff): `git diff --check`는 출력 없이 종료했다.
- 전체 `./gradlew test`는 production 변경이 없고 focused, reply create, legacy characterization, FanTalk package 회귀가 직접
범위를 포함하므로 실행하지 않았다.
- 남은 항목: `P6-T5` FanTalk 보안·오류·회귀 검증.
### Phase 1~7 정적 리뷰 완료 — 2026-07-28
- 상태: 리뷰 완료, 후속 처리 요청(당시 판정)
- 무엇을: PRD, plan-task, OpenAPI 23개 operation과 현재 production/test 구현을 Phase별로 정적 대조했다.
- 왜: 완료 기록과 실제 HTTP 경계·내부 책임·구현 현황 metadata가 같은 계약을 가리키는지 확인하고 확정 finding을 이어서
실행 가능한 Task로 전환하기 위해서다.
- 어떻게: controller/facade/repository/DTO와 관련 테스트의 호출·mapping·JSON parsing·pagination을 정적 추적하고,
`rg`, `jq`, `git diff` 기반으로 문서/operation/변경 범위를 대조했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는
실행하지 않았다.
- Phase 1 결과: 공통 target resolver, ADMIN 이중 인가, 오류/security 경계에서 신규 확정 finding 0건.
- Phase 2 결과: mutation 성공 응답이 `data: null`인데 사용하지 않는 facade response mapping 1건을 `REV-021`로 확정했다.
- Phase 3 결과: 호출되지 않는 관리자 content repository 확장과 전용 enum 1건을 `REV-022`로 확정했다.
- Phase 4 결과: OpenAPI에 없는 시리즈 DELETE route와 두 JSON body의 미지 필드 허용을 `REV-023`~`REV-024`로 확정했다.
- Phase 5 결과: multipart JSON parse 실패의 500 가능성·미지 필드 허용과 계약 밖 `size <= 50` 제한을
`REV-025`~`REV-026`으로 확정했다.
- Phase 6 결과: 공개 v2와 다른 pagination 거부 정책과 reply 미지 필드 허용을 `REV-027`~`REV-028`로 확정했다.
- Phase 7 결과: plan/api-contract/OpenAPI 구현 상태 metadata가 완료 구현과 불일치하는 문제를 `REV-029`로 확정했다.
- 다음 Goal: `P2-R6`.
- 보완 결과: `P2-R6`, `P2-R6-GATE`를 완료했고 `REV-021`을 처리 완료로 동기화했다.
- 다음 Goal: `P3-R9`.
- 보완 결과: `P3-R9`, `P3-R9-GATE`를 완료했고 `REV-022`를 처리 완료로 동기화했다.
- 다음 Goal: `P4-R1`.
- 보완 결과: `P4-R1`을 완료했고 `REV-023`~`REV-024`를 처리 완료로 동기화했다.
- 다음 Goal: `P4-R1-GATE`.
- 보완 결과: `P4-R1-GATE`를 완료했고 Phase 4 후속 리뷰를 종료했다.
- 다음 Goal: `P5-R1`.
- 보완 결과: `P5-R1`, `P5-R1-GATE`를 완료했고 `REV-025`~`REV-026`을 처리 완료로 동기화했다.
- 다음 Goal: `P6-R1`.
- 보완 결과: `P6-R1`을 완료했다. FanTalk 목록은 공개 v2 `CreatorChannelFanTalkQueryPolicy`를 재사용해 `page < 0 -> 0`, `size < 20 -> 20`, `size > 50 -> 50`으로 보정하고, reply body는 strict reader로 미지 필드 400/no insert/no event를 고정했다.
- 다음 Goal: `P6-R1-GATE`.
- 보완 결과: `P6-R1-GATE`를 완료했고 Phase 6 후속 리뷰를 종료했다.
- 다음 Goal: `P7-R1`.
- 보완 결과: `P7-R1`을 완료했다. plan/API 설명/OpenAPI status를 23개 구현 완료로 동기화했고 controller mapping 23개와 validator/client 생성·compile을 확인했다.
- 다음 Goal: `P7-R1-GATE`.
- 보완 결과: `P7-R1-GATE`를 완료했고 `REV-021`~`REV-029` 전체를 처리 완료로 종결했다.
- 다음 Goal: 없음.
### Phase 1~7 후속 정적 리뷰 완료 — 2026-07-28
- 상태: 리뷰 완료, `P3-R10` 시작 대기
- 무엇을: PRD, plan-task, OpenAPI와 현재 Phase 1~7 production/test를 완료 기록 이후 상태 기준으로 다시 정적 대조했다.
- 왜: 통과한 컴파일·테스트가 다루지 않은 의미 검증, 상태 전이, 필수 multipart binding과 실제 동시성 불변식을 확인하고
확정 항목을 실행 가능한 후속 Task로 전환하기 위해서다.
- 어떻게: 요청값에서 controller/facade/legacy service/repository/예외 handler까지 호출 흐름을 역추적하고,
관련 테스트가 실제 병렬·경계 상태를 검증하는지 `rg`, `sed`, `jq`, `git diff`로 확인했다. 사용자 요청에 따라
컴파일과 테스트는 실행하지 않았다.
- Phase 1 결과: 공통 target resolver, 보안, 오류 handler에서 신규 확정 finding 없음.
- Phase 2 결과: Character 4개 operation과 mutation pipeline에서 신규 확정 finding 없음.
- Phase 3 결과: 생성 `releaseDate`/`timezone`의 의미 오류가 500으로 분류되는 `REV-030`을 확정하고
`Task 3.20` / `P3-R10`으로 전환했다.
- Phase 4 결과: soft-delete된 linked content를 해제할 수 없는 `REV-031`, 생성 필수 `image`가 nullable binding인
`REV-032`를 확정하고 `Task 4.8` / `P4-R2`로 전환했다.
- Phase 5 결과: 순차 테스트만으로 완료 처리되어 실제 병렬 요청에서 최대 고정 3개를 보장하지 못하는 `REV-033`을
확정하고 `Task 5.8` / `P5-R2`로 전환했다.
- Phase 6 결과: FanTalk 2개 operation에서 신규 확정 finding 없음.
- Phase 7 결과: 23개 operation/mapping/status는 유지되지만 네 finding 처리 전 최종 완료 판정을 유지할 수 없어
`Task 7.4` / `P7-R2` 통합 재판정을 추가했다.
- 다음 Goal: `P3-R10`.
### Phase 1~7 3차 정적 리뷰 완료 — 2026-07-28
- 상태: 리뷰 완료, `P2-R7` 시작 대기
- 무엇을: PRD, plan-task, OpenAPI와 현재 Phase 1~7 production/test를 최신 완료 상태 기준으로 다시 정적 대조했다.
- 왜: 통과한 컴파일·테스트가 다루지 않은 empty multipart와 optional boolean, locale별 예약일 표시 의미를 확인하고,
확정 항목을 해당 Phase의 실행 가능한 후속 Task로 전환하기 위해서다.
- 어떻게: actual controller에서 facade, mapper, 레거시 controller/service, S3·DB·event 경계까지 호출 흐름을
`rg`, `sed`, `jq`, `git diff`로 추적했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다.
- Phase 1 결과: 공통 resolver, 보안, 오류/CORS 경계에서 신규 확정 finding 없음.
- Phase 2 결과: 생성의 빈 필수 image가 통과하는 `REV-034`, `isActive=true` 단독 수정이 레거시와 달리 거부되는
`REV-035`를 확정하고 `Task 2.13` / `P2-R7`으로 전환했다.
- Phase 3 결과: 미래 예약 콘텐츠 상세의 `releaseDate`가 항상 null인 `REV-036`을 확정하고
`Task 3.21` / `P3-R11`로 전환했다.
- Phase 4 결과: 생성·수정의 빈 image가 0-byte upload를 유발하는 `REV-037`을 확정하고
`Task 4.9` / `P4-R3`으로 전환했다.
- Phase 5 결과: Community 3개 operation과 owner lock에서 신규 확정 finding 없음.
- Phase 6 결과: FanTalk 2개 operation에서 신규 확정 finding 없음.
- Phase 7 결과: 23개 operation/mapping/status는 유지되지만 네 finding 처리 전 최종 완료 판정을 유지할 수 없어
`Task 7.5` / `P7-R3` 통합 재판정을 추가했다.
- 다음 Goal: `P2-R7`.
### Phase 1~7 4차 정적 리뷰 완료 — 2026-07-29
- 상태: 리뷰 완료, `P7-R4` 시작 대기
- 무엇을: PRD, plan-task, OpenAPI와 최신 Phase 1~7 production/test를 후속 Gate 완료 상태 기준으로 정적 대조했다.
- 왜: 컴파일·테스트 통과 이후에도 남을 수 있는 route/schema 의미와 완료 상태 기록 불일치를 확인하기 위해서다.
- 어떻게: controller/facade/mapper/repository에서 legacy service까지 호출 경로를 추적하고 `sed`, `rg`, `jq`,
`git diff --check`로 문서·23개 operation·controller mapping·dependency/DDL 변경 범위를 확인했다. 사용자 요청에 따라
Gradle, 컴파일, 테스트는 실행하지 않았다.
- Phase 1 결과: resolver, ADMIN 이중 인가, 오류/CORS/firewall 경계에서 신규 확정 finding 없음.
- Phase 2 결과: Character 4개 operation의 최신 empty image·`isActive=true` 보완을 확인했고 기능 finding 없음.
- Phase 3 결과: AudioContent 5개 operation의 예약 공개일·signed URL·ownership 경계를 확인했고 기능 finding 없음.
- Phase 4 결과: Series 9개 runtime operation은 일치하지만 Phase 4 endpoint 설명의 DELETE path/body가 현재 계약과 다른
문서 문제를 `REV-039`로 확정했다.
- Phase 5 결과: Community 3개 operation과 owner lock·soft delete에서 신규 확정 finding 없음.
- Phase 6 결과: FanTalk 2개 operation과 pagination/root ownership에서 신규 확정 finding 없음.
- Phase 7 결과: 완료 증거가 있는 네 Task 헤더가 `[ ]`로 남아 상단 완료 상태와 모순되는 `REV-038`을 확정했다.
- plan 전환: 두 문서 정합성 finding을 `Task 7.6` / `P7-R4`로 묶었다.
- 다음 Goal: `P7-R4`.
### `P7-R4` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: 완료 증거가 있는 `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5` 헤더와 상단 상태표를 완료 상태로 동기화하고, Phase 4 시리즈 콘텐츠 해제 설명을 path `contentId`와 request body 없음 계약으로 정정했다.
- 왜: `REV-038`~`REV-039`가 기능 문제가 아니라 후속 작업 판단을 오도하는 문서 정합성 문제로 확정됐기 때문이다.
- 어떻게: 기존 Gate와 2026-07-29 검증 기록은 보존하고 문서 상태만 갱신한 뒤 `./gradlew tasks --all`, OpenAPI 23개 operation/status `jq`, 미완료 Task header `rg`, controller mapping `rg`, `git diff --check`를 실행했다.
- 결과: `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 767ms`, OpenAPI assertion은 `true`, 미완료 Task header와 `git diff --check`는 출력이 없었고 controller mapping은 23개였다. `REV-038`~`REV-039`를 처리 완료로 판정했다.
- 남은 항목: 없음.
### `P2-R8` / `P2-R8-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: 캐릭터 생성 관계 `importance` 누락·null을 v2 request 경계에서 400 `common.error.invalid_request`로 거부하도록 보완했다.
- 왜: OpenAPI required non-null integer가 Jackson/Kotlin primitive 기본값 `0`으로 보정되면 잘못된 관계 입력이 외부 API·DB·S3·event 부작용으로 이어질 수 있기 때문이다.
- 어떻게: production 변경은 `AiCharacterAdminCharacterFacade.readRequest()`의 strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가하는 최소 범위로 제한하고, actual multipart POST RED/GREEN과 no-side-effect를 추가했다.
- 결과: RED 명령은 신규 2건이 `status().isBadRequest` 기대에서 실패했고, 보완 후 같은 명령과 `AiCharacterAdminCharacterControllerMutationTest`, character/common 영향 범위 회귀, `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. `git diff --check`는 출력이 없었다.
- 남은 항목: Phase 3 `P3-R12`.
### `P3-R12` / `P3-R12-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: 오디오 생성 `price` 누락·null과 primitive boolean explicit null을 v2 request 경계에서 400 `common.error.invalid_request`로 거부하도록 보완했다.
- 왜: OpenAPI required/non-null primitive가 Jackson/Kotlin 기본값 `0`/`false`로 보정되면 잘못된 생성 요청이 파일 업로드·DB·event 부작용으로 이어질 수 있기 때문이다.
- 어떻게: production 변경은 `AiCharacterAdminAudioContentFacade.readRequest()`의 strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`와 `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가하는 최소 범위로 제한하고, actual multipart POST RED/GREEN과 no-side-effect를 추가했다.
- 결과: RED 명령은 신규 8개 invocation이 `status().isBadRequest` 기대에서 실패했고, `themeId:null`은 기존 missing-theme guard로 이미 400이었다. 보완 후 같은 명령과 `AiCharacterAdminAudioContentCreateTest`, content/common 영향 범위 회귀, `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. `git diff --check`는 출력이 없었다.
- 남은 항목: Phase 4 `P4-R4`.
### Phase 1~7 5차 정적 리뷰 완료 — 2026-07-29
- 상태: 리뷰 완료, `P2-R8` 시작 대기
- 무엇을: PRD, plan-task, OpenAPI와 최신 Phase 1~7 production/test의 JSON request 경계를 primitive
required/non-null/default 의미까지 정적 대조했다.
- 왜: Kotlin non-null primitive도 Jackson 2.13.5 기본 설정에서는 누락·null이 JVM 기본값으로 보정될 수 있어,
컴파일·기존 테스트 통과만으로 OpenAPI nullability를 보장하지 못하기 때문이다.
- 어떻게: 각 multipart `request`의 strict reader 설정, Kotlin DTO primitive 타입, OpenAPI
required/nullable/default와 로컬 Jackson Kotlin/databind 2.13.5 source를 역추적했다. 사용자 요청에 따라 Gradle,
컴파일, 테스트는 실행하지 않았다.
- Phase 1 결과: 공통 resolver, security, 오류 handler에서 신규 확정 finding 없음. 전역 mapper 변경도 후속 범위에서
제외했다.
- Phase 2 결과: 관계 필수 `importance` 누락·null이 `0`으로 보정될 수 있는 `REV-040`을 확정하고
`Task 2.14` / `P2-R8`로 전환했다.
- Phase 3 결과: 생성 필수 `price` 누락·null과 non-null primitive의 explicit null이 기본값으로 보정될 수 있는
`REV-041`을 확정하고 `Task 3.22` / `P3-R12`로 전환했다.
- Phase 4 결과: 생성 `genreId`, `isAdult`의 explicit null이 기본값으로 보정될 수 있는 `REV-042`를 확정하고
`Task 4.10` / `P4-R4`로 전환했다.
- Phase 5 결과: 생성 필수 boolean·`price`와 수정 `isFixed`의 null/누락이 거부되지 않는 `REV-043`을 확정하고
`Task 5.9` / `P5-R3`로 전환했다.
- Phase 6 결과: FanTalk reply는 primitive 요청 필드가 없고 기존 문자열 null/blank 경계가 유지되어 신규 finding 없음.
- Phase 7 결과: 23개 operation/mapping/status는 유지되지만 네 finding 처리 전 최종 완료 판정을 유지할 수 없어
`Task 7.7` / `P7-R5` 통합 재판정을 추가했다.
- 다음 Goal: `P2-R8`.
### Community 목록 계약 변경 확정 — 2026-07-29
- 상태: `P5-R4` / `P5-R4-GATE` 완료
- 무엇을: Community 목록에서 사용되지 않는 `timezone` query를 제거하고 응답 `data`를
`totalCount`, `page`, `size`, `hasNext`, `items` wrapper로 변경했다.
- 왜: 관리자 UI가 active owner 게시글 전체 개수와 다음 page 추가 로딩 필요 여부를 판단해야 하기 때문이다.
- 어떻게: 기존 item 필드와 고정 우선 정렬은 유지하고 active owner count query 하나를 추가하는 최소 설계로 PRD,
OpenAPI, API 설명과 Phase 5 신규 Task/Gate를 동기화했다.
- runtime 상태: controller/facade의 `timezone` query를 제거하고 `data` pagination wrapper를 반환하도록 정합화했다.
- plan 전환: `Task 5.10` / `P5-R4`, `P5-R4-GATE`를 완료하고 `P7-R5` 시작 조건을 충족했다.
- 다음 Goal: `P7-R5`.
### `P7-R5` / `P7-R5-GATE` 완료 — 2026-07-29
- 상태: 완료
- 무엇을: Phase 2~5 primitive required/nullability 보완과 Community 목록 wrapper 계약을 23개 관리자 operation 기준으로 통합 재판정했다.
- 왜: 후속 production/API 계약 변경 뒤 공통 JWT ADMIN 이중 인가, target/owner 오류, JSON 오류 envelope, no-side-effect와 legacy 회귀가 유지되는지 확인하기 위해서다.
- 어떻게: targeted 통합, 전체 회귀, lint, OpenAPI 23개 `implemented`, controller mapping 23개, dependency/DDL 무변경, diff whitespace를 fresh 검증했다.
- 결과: targeted `BUILD SUCCESSFUL in 2m 21s`, 전체 test `BUILD SUCCESSFUL in 5m 46s`, `ktlintCheck` `BUILD SUCCESSFUL in 881ms`, OpenAPI assertion `true`, mapping 23개, dependency/DDL 검색과 `git diff --check` 출력 없음이었다.
- 남은 항목: 없음.
### 후속 기능 계획 확정 — 2026-07-29
- 상태: `P5-R5` / `P5-R5-GATE` 완료
- Phase 2 결과: 캐릭터 등록용 원작 검색을 `Task 2.15` / `P2-R9`로 추가했다.
- Phase 3 결과: 오디오 콘텐츠 댓글 CRUD 5개 operation을 `Task 3.23` / `P3-R13`으로 추가했다.
- Phase 4 결과: 시리즈 등록용 장르 목록을 `Task 4.11` / `P4-R5`, 상세 `data`의 목록 item 정합화를
`Task 4.12` / `P4-R6`으로 추가했다.
- Phase 5 결과: 커뮤니티 댓글 CRUD 5개 operation을 `Task 5.11` / `P5-R5`로 추가했다.
- Phase 6 결과: 팬 작성 FanTalk 원글 soft delete를 `Task 6.7` / `P6-R2`로 추가했다.
- Phase 7 결과: 기존 23개와 신규 13개를 합한 36개 operation 통합 재판정을 `Task 7.8` / `P7-R6`으로 추가했다.
- 제외 결과: 캐릭터에 직접 달리는 레거시 댓글 삭제는 v2 전환 뒤 미사용이라는 사용자 확정에 따라 operation과 Task를
추가하지 않았다.
- 검증: 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않고 JSON 문법, operation/status 집계, 내부 `$ref`,
시리즈 상세 schema 참조와 문서 diff만 정적으로 확인한다.
- P2-R9 결과: 캐릭터 등록용 원작 검색 endpoint를 기존 `AdminOriginalWorkService.searchOriginalWorksAll`과
`OriginalWorkResponse.from` 재사용으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다.
- P2-R9 검증: focused RED 3개 실패 확인 후 GREEN `BUILD SUCCESSFUL`, Character/common 영향 범위 회귀
`BUILD SUCCESSFUL`, `ktlintCheck` `BUILD SUCCESSFUL`, `git diff --check` 출력 없음.
- P3-R13 결과: 오디오 콘텐츠 댓글 CRUD 5개 endpoint를 기존 `AudioContentCommentService` 조회·작성·수정 의미 재사용과
v2 facade의 target/owner/parent/actor 선검증으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다.
- P3-R13 검증: focused RED 7건은 미구현 route의 404/405로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 3m 16s`,
content/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 3m 47s`, `ktlintCheck`는 import 순서 1건 수정 후
`BUILD SUCCESSFUL in 32s`, `git diff --check`는 출력 없음이었다.
- P4-R5 결과: 시리즈 등록용 장르 목록 endpoint를 기존 `AdminContentSeriesGenreService.getSeriesGenreList` 재사용으로
구현하고 OpenAPI 상태를 `implemented`로 갱신했다.
- P4-R5 검증: focused RED 2건은 미구현 route로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 1m 24s`,
series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 38s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`,
`git diff --check`는 출력 없음이었다.
- P4-R6 결과: 시리즈 상세 `data`를 목록 `items` 단일 항목과 동일한 `GetCreatorAdminContentSeriesListItem`
schema로 반환하도록 정합화하고 OpenAPI 상태를 `implemented`로 갱신했다.
- P4-R6 검증: focused RED는 레거시 상세 필드 차이로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 43s`였다.
focused query/contract 회귀는 `BUILD SUCCESSFUL in 38s`, series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 19s`,
`ktlintCheck`는 `BUILD SUCCESSFUL in 23s`, `git diff --check`는 출력 없음이었다.
- P5-R5 결과: 커뮤니티 댓글 CRUD 5개 endpoint를 기존 `CreatorCommunityService` 조회·작성·수정 의미 재사용과
v2 facade의 target/owner/parent/actor 선검증으로 구현하고 OpenAPI 상태를 `implemented`로 갱신했다.
- P5-R5 검증: focused RED 7건은 미구현 route로 실패했고, GREEN focused는 `BUILD SUCCESSFUL in 2m`였다.
community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 26s`, `ktlintCheck`는 import 순서 1건 수정 후
`BUILD SUCCESSFUL in 22s`, `git diff --check`는 출력 없음이었다.
- P6-R2 결과: 팬 작성 FanTalk 원글 삭제 endpoint를 `CreatorCheers.isActive` row 단위 soft delete로 구현하고
OpenAPI 상태를 `implemented`로 갱신했다.
- P6-R2 검증: focused RED 6건은 DELETE route 미구현으로 실패했고, GREEN 이후 누락 ID를 포함한 focused 7건은
`BUILD SUCCESSFUL in 29s`였다. FanTalk/common 영향 범위 회귀는 DELETE 인가 matrix 보강 후
`BUILD SUCCESSFUL in 58s`였다.
- P6-R2-GATE 검증: `AiCharacterAdminAuthorizationTest` 단독은 `BUILD SUCCESSFUL in 29s`, 최종 `./gradlew ktlintCheck`는
`BUILD SUCCESSFUL in 14s`, `git diff --check`는 출력 없음이었다.
- P7-R6 결과: Phase 2~6 후속 기능과 시리즈 상세 정합화 뒤 36개 관리자 operation의 계약·route·문서 상태를
통합 재판정했고, `Task 7.8` / `P7-R6-GATE`를 완료했다.
- P7-R6 검증: targeted `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`는
`BUILD SUCCESSFUL in 2m 24s`, 전체 `./gradlew test`는 `BUILD SUCCESSFUL in 8m 9s`, `./gradlew ktlintCheck`는
`BUILD SUCCESSFUL in 1s`였다. OpenAPI 36개 operation/36개 `implemented` assertion은 `true`, controller mapping은
36개, 캐릭터 직접 댓글 route 검색은 0개, dependency/DDL 추가 검색과 `git diff --check`는 출력 없음이었다.
- 다음 Goal: 없음.
### UTC 날짜 계약 변경 확정 — 2026-07-29
- 상태: 구현 완료
- 무엇을: 신규 관리자 오디오 생성 request의 `timezone` body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 GET의
`timezone` query를 제거했다. 생성 `releaseDate`는 클라이언트가 보내는 nullable ISO-8601 UTC(`Z`), 상세
`releaseDate`와 댓글 `date`는 기존 필드명·null/노출 조건을 유지한 ISO-8601 UTC(`Z`)로 문서 계약을 확정했다.
- 왜: 서버가 클라이언트별 timezone을 받아 표시 문자열을 만들 필요 없이 절대 시각은 UTC로 교환하고 표시 변환은
클라이언트가 담당하도록 단일 계약을 유지하기 위해서다.
- 어떻게: production/test는 변경하거나 실행하지 않고 PRD, OpenAPI 2.2.0, 계약 설명, 구현 계획과 Phase 3·5·7 리뷰에
신규 `P3-R14`, `P5-R6`, `P7-R7` Task/Gate를 누적했다.
- 결과: 전체 route 36개는 유지된다. 최신 OpenAPI 상태는 `implemented` 36개,
`alignment-required` 0개, `planned` 0개다.
- 다음 Goal: 없음.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-07-27 | `DEC-GOAL-001` | 확정 | 기존 Phase 2·3 완료 Task는 이력으로 보존하고 심층 리뷰·세부 보완·Gate Goal을 추가한다. | 기존 검증 이후 후속 보완이 반복됐고 Phase 완료 Gate가 없었다. | `P2-R1`~`P3-GATE` |
| 2026-07-28 | `DEC-REVIEW-007` | 확정 | Phase 1~7 정적 리뷰의 확정 finding 9건은 기존 완료 Task를 다시 열지 않고 각 소유 Phase의 신규 Task/Gate로 직렬 처리한다. | 완료 이력을 보존하면서 OpenAPI 단일 원본과 실제 HTTP 경계의 불일치를 최소 범위로 수정해야 한다. | `P2-R6`~`P7-R1-GATE`, Phase별 review 문서 |
| 2026-07-28 | `DEC-REVIEW-008` | 확정 | 후속 정적 리뷰의 `REV-030`~`REV-033`은 기존 완료 Task를 다시 열지 않고 Phase 3~5 신규 Task/Gate로 처리한 뒤 Phase 7에서 통합 재판정한다. | 정상·순차 경로의 테스트 통과와 별개로 의미 오류 500, soft-delete link 상태 전이, 필수 multipart binding, count-then-update 경쟁 조건이 코드·문서 근거로 확정됐다. | `P3-R10`~`P7-R2-GATE`, Phase별 review 문서 |
| 2026-07-28 | `DEC-REVIEW-009` | 확정 | 3차 정적 리뷰의 `REV-034`~`REV-037`은 기존 완료 Task를 다시 열지 않고 Phase 2~4 신규 Task/Gate로 처리한 뒤 Phase 7에서 통합 재판정한다. | required/optional multipart는 part 존재만으로 파일 유효성을 보장하지 않고, optional boolean과 예약일 표시의 레거시 의미가 현재 mapper/facade에서 소실되는 코드 경로가 확정됐다. | `P2-R7`~`P7-R3-GATE`, Phase별 review 문서 |
| 2026-07-29 | `DEC-REVIEW-010` | 확정 | 4차 정적 리뷰의 `REV-038`~`REV-039`는 production/OpenAPI를 변경하지 않고 Phase 7 문서 정합성 Task 하나로 처리한다. | 네 후속 Task는 하위 체크리스트·Gate·검증 기록상 완료됐지만 헤더가 미완료이고, Phase 4 DELETE 설명은 OpenAPI/controller와 달라 후속 작업 상태와 route 판단을 오도한다. | `P7-R4`, Phase 7 review 문서 |
| 2026-07-29 | `DEC-REVIEW-011` | 확정 | 5차 정적 리뷰의 `REV-040`~`REV-043`은 전역 Jackson 또는 레거시 DTO를 변경하지 않고 각 v2 request 경계의 신규 Task/Gate로 처리한 뒤 Phase 7에서 통합 재판정한다. | Jackson Kotlin/databind 2.13.5 기본 동작은 Kotlin primitive의 누락·null을 JVM 기본값으로 보정할 수 있고, 현재 strict reader는 미지 필드만 거부해 OpenAPI required/non-null 계약을 강제하지 못한다. | `P2-R8`~`P7-R5-GATE`, Phase별 review 문서 |
| 2026-07-29 | `DEC-REVIEW-012` | 확정 | 6차 정적 리뷰의 `REV-052`~`REV-058`은 OpenAPI를 변경하지 않고 각 v2 HTTP 경계의 신규 Task/Gate로 직렬 처리한 뒤 Phase 7에서 통합 재판정한다. | canonical OpenAPI의 optional pagination, JSON-only/415, multipart `request` part `application/json` 계약이 controller binding·mapping과 직접 불일치하며, domain 의미 변경 없이 소유 Phase의 최소 수정으로 해결할 수 있다. | `P2-R10`~`P7-R8-GATE`, Phase별 review 문서 |
| 2026-07-29 | `DEC-REVIEW-013` | 확정 | 7차 정적 리뷰의 `REV-060`~`REV-063`은 OpenAPI를 변경하지 않고 Phase 2~5 multipart controller 경계에서 operation별 허용 part 이름을 강제하며, `REV-064` 문서 상태와 함께 Phase 7에서 통합 재판정한다. | 8개 canonical multipart schema는 모두 `additionalProperties: false`지만 controller는 선언된 인자만 binding하고 실제 전체 part 이름을 검증하지 않아 미정의 part를 무시한다. OpenAPI는 37개 모두 `implemented`인데 계획 요약과 계약 설명은 36개 구현·1개 planned로 남아 있다. | `P2-R11`~`P7-R9-GATE`, Phase별 review 문서 |
| 2026-07-29 | `DEC-REVIEW-014` | 확정 | 9차 정적 리뷰의 `REV-072`는 preview 규칙을 재구현하지 않고 두 오디오 생성 경로가 공유하는 parsed request overload에서 기존 검증을 정확히 한 번 수행하도록 Phase 3에서 최소 보완한 뒤 Phase 7에서 통합 재판정한다. | v2 facade는 신규 overload를 호출하지만 기존 preview 쌍·형식·최소 15초 검증은 문자열 request overload에만 남아 있어 잘못된 preview 입력이 DB/S3/event 경계로 진행된다. | `P3-R19`~`P7-R11-GATE`, Phase 3·7 review 문서 |
| 2026-07-29 | `DEC-FANTALK-REPLY-UPDATE-001` | 확정 | FanTalk 답변 수정은 레거시 `PUT /explorer/profile/cheers`의 optional/nullable `content`, `isActive`, 빈 객체 no-op, 비활성 reply 재활성화와 `CreatorChannelFanTalkResponse` 성공 `data`를 유지한다. 신규 path ID와 target AI·활성 root·direct reply 검증만 추가한다. | 사용자 요청과 “기존 계약과 동일” 확정, `ExplorerService.modifyCheers`의 상태 전이와 응답 mapper | `P6-R4`, `P6-R4-GATE`, `P7-R8`, PRD, OpenAPI 2.3.0 |
| 2026-07-29 | `DEC-P5-LIST-001` | 확정 | Community 관리자 목록은 `timezone` query를 제거하고 `data`를 `totalCount`, `page`, `size`, `hasNext`, `items`로 반환한다. `totalCount`와 `hasNext`는 target creatorMember의 active 게시글만 기준으로 계산한다. | 목록 응답의 상대 시간은 timezone을 사용하지 않으며 관리자 UI가 전체 개수와 다음 page 추가 로딩 여부를 판단해야 한다는 사용자 확정 요구사항을 반영한다. | PRD, OpenAPI, `Task 5.10`, `P5-R4`~`P5-R4-GATE`, `P7-R5` |
| 2026-07-29 | `DEC-COMMENT-001` | 확정 | 오디오 콘텐츠·커뮤니티 댓글은 target AI 캐릭터 명의로 작성하고 target 작성 댓글만 수정하며 target 소유 자산의 댓글은 작성자와 관계없이 row 단위 soft delete한다. | 사용자 승인과 기존 콘텐츠·게시글 소유자의 댓글 비활성화 동작을 유지한다. | `P3-R13`, `P5-R5`, OpenAPI |
| 2026-07-29 | `DEC-CHAR-COMMENT-001` | 제외 | 사용하지 않는 레거시 캐릭터 직접 댓글 API는 v2로 전환하거나 관리자 삭제 기능을 추가하지 않는다. | 사용자 확인 결과 v2 전환 뒤 사용하지 않는다. | Non-Goals, `P7-R6` |
| 2026-07-29 | `DEC-UTC-DATE-001` | 확정 | 신규 관리자 오디오 생성의 `timezone` body와 오디오 상세·오디오 댓글/답글·커뮤니티 댓글/답글 GET의 `timezone` query를 제거한다. 생성 `releaseDate`는 클라이언트가 UTC로 변환해 보내고, 상세 `releaseDate`와 댓글 `date`는 기존 필드명을 유지한 ISO-8601 UTC(`Z`)로 반환한다. 로컬 시각+timezone 입력은 병행 지원하지 않는다. | 서버가 클라이언트 표시 timezone을 해석하지 않고 단일 절대 시각 계약을 유지한다는 사용자 승인 | `P3-R14`, `P5-R6`, `P7-R7`, PRD, OpenAPI 2.2.0 |
| 2026-07-29 | `DEC-FANTALK-DELETE-001` | 확정 | 팬 작성 FanTalk root 삭제는 원글만 soft delete하고 연결 creator reply row는 변경하지 않는다. | 사용자 승인과 기존 `CreatorCheers.isActive` 상태 전이를 유지한다. | `P6-R2`, OpenAPI |
| 2026-07-29 | `DEC-REGISTRATION-REFERENCE-001` | 확정 | 캐릭터 등록용 원작 검색과 시리즈 등록용 장르 목록을 target 없는 신규 v2 관리자 endpoint로 제공한다. | 캐릭터 관리자 frontend가 동일한 v2 ADMIN/CORS 경계에서 등록 참조 정보를 조회해야 한다. | `P2-R9`, `P4-R5`, OpenAPI |
| 2026-07-29 | `DEC-SERIES-DETAIL-001` | 확정 | 시리즈 상세 `data`를 목록 `items`의 단일 항목과 동일한 schema로 변경하고 기존 상세 전용 `genre`, `keywords`를 제거한다. | 사용자 확정과 관리자 목록·상세 DTO 일관성을 반영한다. | `P4-R6`, OpenAPI |
| 2026-07-28 | `DEC-P4-R2-001` | 확정 | 시리즈 생성 `image` 누락은 PRD 공통 binding 계약과 OpenAPI required part를 따라 exact `MissingServletRequestPartException`, 400 `common.error.invalid_request`로 처리한다. | nullable binding을 통한 legacy `creator.admin.series.cover_image_required`는 필수 part가 MVC를 통과한 결과이며 PRD §8의 명시적 누락 part 계약과 충돌한다. | `P4-R2`, `REV-032` |
| 2026-07-27 | `DEC-REVIEW-001` | 확정 | 코드 리뷰의 8개 확정 finding은 기존 미실행 범주형 Goal에 `REV-001`~`REV-008`로 귀속하고 Phase 안에서 직렬 실행한다. | 새 Goal을 중복 추가하거나 기존 완료 이력을 다시 열지 않으면서 각 finding의 재현·완료 증거를 독립 추적하기 위해. | `P2-R1`~`P2-GATE`, `P3-R1`~`P3-GATE` |
| 2026-07-27 | `DEC-P2-T4-001` | 확정 | POST 생성은 외부 API 필수 입력인 `systemPrompt`를 받고, `externalCharacterId`는 외부 API가 반환하는 response 전용 값이며, `isActive`는 서버가 `true`로 생성하는 response 상태다. `characterType`은 생략 시 `Character`, 잘못된 값은 외부 부작용 전 400이다. 중복 이름은 legacy와 같은 `findByName` 선검증만 적용하며 신규 DDL 없이 동시 요청의 DB unique 보장은 추가하지 않는다. 원작·중복·타입 검증은 외부 생성 전에 수행한다. 외부 API 실패는 DB/S3/event를 남기지 않고, S3 실패는 DB transaction과 event를 롤백하지만 legacy에 삭제 API가 없으므로 이미 생성된 외부 캐릭터는 보상하지 않는다. | Endpoint Contract Summary의 축약 JSON이 외부 API 반환값을 입력처럼 표기하지만, legacy 등록과 v2 external client 모두 `systemPrompt`로 외부 생성을 요청하고 ID를 응답에서 받는다. PRD의 external API 계약·DDL 변경 금지와 legacy failure order를 유지한다. | `P2-T4`, `REV-002`, `REV-003`, `REV-007` |
| 2026-07-27 | `DEC-P2-T5-001` | 확정 | PUT의 `externalCharacterId`는 response 전용으로 명시 거부한다. `isActive=false`는 image와 일반 수정 field를 섞지 않는 단독 soft delete다. 일반 수정은 image를 생략하면 기존 경로를 유지하고, 존재하지 않는 `originalWorkId`는 외부 수정 전에 거부한다. 외부 수정 실패는 S3/DB/event를 남기지 않으며, S3 실패는 DB/event를 롤백하지만 legacy와 같은 external update restore 계약이 없어 성공한 외부 수정은 보상하지 않는다. 응답 `updatedAtUtc`는 DB flush 후 매핑한다. | 기존 service의 soft delete는 다른 field를 무시해 이미지 업로드 고아를 남겼고, `@PreUpdate` timestamp는 flush 전에는 이전 값을 반환했다. 외부 API delete/restore 추가와 DDL은 범위 밖이다. | `P2-T5`, `REV-002`, `REV-003`, `REV-007` |
| 2026-07-27 | `DEC-REVIEW-002` | 확정 | 기존 Phase 2·3 Task/Gate 완료 이력은 보존하고, 2차 리뷰에서 확인한 검증 증거 누락은 `REV-009`~`REV-011`과 새 후속 Task/Gate로 처리한다. | fresh 199건은 모두 통과했지만 기존 완료 체크리스트의 실제 endpoint·failure-order 범위와 test method가 일치하지 않았다. | `P2-R2`~`P3-R2-GATE`, 두 2차 review |
| 2026-07-27 | `DEC-REVIEW-003` | 확정 | 3차 수정에서 유효하게 보강된 범위와 기존 완료 이력은 보존하고, 아직 직접 고정되지 않은 실패·ownership 경계만 `REV-012`~`REV-014`와 새 Task/Gate로 추적한다. | fresh 228건은 모두 통과했지만 `Task 2.8`~`Task 3.10`의 완료 체크리스트와 실제 failure injection·side-effect assertion 범위가 다시 일치하지 않았다. | `P2-R3`~`P3-R3-GATE`, 두 4차 review |
| 2026-07-27 | `DEC-REVIEW-004` | 확정 | 4차 보완의 유효한 테스트와 완료 이력은 보존하고, exact multipart 계약과 actual transaction·ownership/domain 직접 증거 누락은 `REV-015`~`REV-017` 및 새 Task/Gate로 추적한다. | fresh 79건은 통과했지만 nullable file binding, direct facade event test와 unknown target에 한정된 matrix가 기존 완료 조건보다 좁았다. | `P2-R4`~`P3-R4-GATE`, 두 5차 review |
| 2026-07-28 | `DEC-REVIEW-005` | 확정 | 5차 보완과 완료 이력은 보존하고, 추가로 확인한 문서 계약·empty-file 경계·detached publisher 증거 문제를 `REV-018`~`REV-020`과 새 후속 Task/Gate로 추적한다. | fresh focused 216건과 lint는 통과했지만 Endpoint Contract Summary, `MultipartFile.isEmpty` 처리와 실제 service publisher field를 코드·test 단위로 대조해 세 문제가 재현됐다. | `P2-R5`~`P3-R5-GATE`, 두 6차 review |
| 2026-07-28 | `DEC-P3-R7-001` | 확정 | 생성의 빈 `coverImage`·`audioFile`은 400 `common.error.invalid_request`로 거부한다. 수정의 빈 `coverImage`는 생략으로 정규화하고, 수정 `audioFile`은 미지원이므로 part가 존재하면 크기와 관계없이 400으로 거부한다. | non-null binding은 part 누락만 차단하며 빈 파일은 0-byte upload와 cover 교체를 유발할 수 있다. optional cover의 빈 part는 일반 multipart client의 생략 표현으로 안전하게 처리할 수 있지만 미지원 audio part는 존재 자체가 계약 위반이다. | `P3-R7`, `REV-019`, Endpoint Contract Summary |
| 2026-07-28 | `DEC-REVIEW-006` | 확정 | Phase 2·3 6차 보완의 코드와 완료 이력은 유지하고 새 production Goal은 추가하지 않는다. 종결 Gate가 있는 `REV-001`~`REV-009`의 종합 표 상태만 `처리 완료`로 동기화한다. | fresh focused 130건이 모두 통과했고 6차 보완 범위의 추가 production 결함은 재현되지 않았지만, 하단 종합 표 상태가 각 Gate·Progress의 처리 완료 판정과 모순됐다. | `P2-GATE`, `P2-R2-GATE`, `P3-GATE`, 하단 발견된 문제 표 |
| 2026-07-28 | `DEC-API-CONTRACT-001` | 확정 | 신규 관리자 endpoint는 레거시 request/response의 필드명·타입·optional/nullable·기본값·성공 `data` 형태를 유지하고 path로 이동한 ID만 body에서 제거한다. FanTalk 답변은 축약 응답을 유지한다. | 신규 endpoint의 목적이 로그인 불가능한 AI 캐릭터를 관리자 경계로 대리 관리하는 것이며, 클라이언트 계약까지 임의로 재설계하는 범위가 아니기 때문이다. | PRD, `api-contract.openapi.json`, `P23-CONTRACT-1`~`P23-CONTRACT-GATE` |
| 2026-07-28 | `DEC-API-CONTRACT-002` | 확정 | FanTalk 목록은 공개 v2 field 형태를 유지하는 관리자 전용 endpoint로 추가하고, 시리즈 연결 목록과 미연결 검색은 응답 형태가 달라 별도 endpoint로 분리한다. | 공개 v2 직접 호출은 `creatorId`, viewer/block filter와 CORS 경계가 관리자 요구와 다르고, 시리즈 두 legacy API의 응답은 wrapper와 direct array로 서로 다르다. | Phase 4, Phase 6, `api-contract.openapi.json` |
| 2026-07-28 | `DEC-API-CONTRACT-003` | 확정 | `DEC-P2-T5-001`의 캐릭터 `isActive=false` 단독 입력 제한을 최신 JSON 계약에서 폐기한다. `ChatCharacterUpdateRequest`처럼 다른 optional field와 동시 입력을 허용하고, `isActive=false`이면 레거시 service와 같이 비활성화만 반영한다. | 사용자 확정 원칙은 path ID만 제거하고 레거시 request를 그대로 이관하는 것이다. 레거시 controller는 혼합 request를 받으며 service는 비활성화 분기에서 나머지 JSON field를 적용하지 않는다. | `api-contract.openapi.json`, `P23-CONTRACT-2` |
| 2026-07-28 | `DEC-API-CONTRACT-004` | 확정 | 생성된 TypeScript의 `tsc` 실행은 클라이언트 build/CI 책임으로 두고 server runtime `P23-CONTRACT-GATE` 완료 조건에는 추가하지 않는다. | 서버 Gate는 실제 HTTP runtime과 OpenAPI validate/client 생성 가능성을 판정하며, 언어별 client compile은 소비 클라이언트의 toolchain에서 검증해야 한다. 계약 작성 Task의 일회성 TypeScript 생성·컴파일 검증은 이미 완료됐다. | `P23-CONTRACT-GATE`, `api-contract.md` |
## 발견된 문제
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|---|---|---|---|---|---|
| `REV-001` | Blocker | 처리 완료 | 문자열 미매핑 경로가 `/{characterId}`에 매핑되어 Phase 1의 404 계약 대신 type mismatch 400을 반환한다. | `P2-R1`, `P2-T6` | numeric path mapping과 KO/EN/JA·CORS 회귀를 고정했다. |
| `REV-002` | High | 처리 완료 | 캐릭터 생성·수정 DTO의 `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 soft-delete 혼합 의미가 Endpoint Contract Summary와 다르다. | `P2-R1`, `P2-T4`, `P2-T5` | canonical request를 확정하고 문서·actual endpoint 계약을 동기화했다. |
| `REV-003` | High | 처리 완료 | 외부 API·S3·DB 실패 사이에 보상 경계가 없고 동시 중복 이름 생성의 원자성 증거가 없다. | `P2-R1`, `P2-T4`, `P2-T5` | failure-order·선검증·비보상 경계와 신규 DDL 없는 동시성 정책을 고정했다. |
| `REV-004` | High | 처리 완료 | 동일한 `seriesIds` 수정도 기존 연결을 삭제·재생성해 row ID·`orders`·`createdAt`을 소실한다. | `P3-R1`, `P3-T6` | 교집합 row를 보존하고 차집합만 변경하도록 수정·검증했다. |
| `REV-005` | High | 처리 완료 | 콘텐츠 상세의 `releaseDate`, `isOnlyRental`, `purchaseOption`이 legacy 상세 파생 규칙과 다르다. | `P3-R1`, `P3-T4` | legacy 조합 matrix와 v2 `releaseDateUtc` 분리 mapping을 고정했다. |
| `REV-006` | High | 처리 완료 | 콘텐츠 생성의 필수 `coverImage`·`audioFile`이 nullable binding이라 정확한 missing-part 오류 계약을 우회한다. | `P3-R1`, `P3-T5`, `P3-T7` | non-null binding과 세 part별 exact exception·KO/EN/JA endpoint 회귀를 추가했다. |
| `REV-007` | Medium | 처리 완료 | 목록 초과 field, pre-flush timestamp, `characterType`, legacy/public 중첩 DTO, `tags`·생성 `isActive`에서 추가 계약/parity 차이가 있다. | `P2-T3`~`P2-T6`, `P3-T3`~`P3-T5`, `P3-T7` | 소유 Goal별 exact schema·field 의미·DTO 경계와 legacy parity를 분리 검증했다. |
| `REV-008` | High | 처리 완료 | endpoint별 인가/i18n, failure-order characterization과 계획에 명시된 test 파일·검증 수의 완료 증거가 부족하다. | `P2-R1`, `P2-T6`, `P3-R1`, `P3-T5`, `P3-T7` | 실제 endpoint matrix와 characterization을 보강하고 정정 기록을 누적했다. |
| `REV-009` | Medium | 처리 완료 | Phase 2 실제 네 endpoint의 stale claim·허용/거부 CORS·오류와 mutation 실패 경계 직접 증거가 Gate 기록보다 좁다. | `P2-R2`, `P2-R2-GATE` | actual endpoint matrix와 실패 후 DB/S3/external/event 결과를 보강했다. |
| `REV-010` | Medium | 처리 완료 | Phase 3 생성의 invalid theme, 실제 multipart KO/EN/JA와 S3/processing/event 실패 순서 직접 증거가 완료 기록보다 좁다. | `P3-R2`, `P3-R2-GATE` | 생성 actual endpoint와 legacy failure characterization을 보강했다. |
| `REV-011` | Medium | 처리 완료 | Phase 3 수정의 교집합+추가+제거 metadata, cover 성공/실패와 ownership no-side-effect 직접 증거가 완료 기록보다 좁다. | `P3-R3`, `P3-R2-GATE` | update/ownership actual endpoint 회귀를 보강했다. |
| `REV-012` | Medium | 처리 완료 | Phase 2의 DB save/flush·event publish 실패 뒤 내부 rollback과 external/S3 잔존 결과를 직접 검증하지 않았다. | `P2-R3`, `P2-R3-GATE` | 실제 transaction failure injection과 내부·외부 결과 단언을 보강했다. |
| `REV-013` | Medium | 처리 완료 | Phase 3 생성은 첫 cover upload 실패만 검증하고 두 번째 audio upload와 event 실패 결과를 직접 고정하지 않았다. | `P3-R4`, `P3-R3-GATE` | 생성 후반 failure order와 비트랜잭션 S3 결과를 특성화했다. |
| `REV-014` | Medium | 처리 완료 | Phase 3 실제 네 경로의 ownership/domain KO/EN/JA와 DB/S3/event no-side-effect matrix가 부분적이다. | `P3-R4`, `P3-R3-GATE` | actual endpoint 오류와 요청 전후 부작용 count를 보강했다. |
| `REV-015` | Medium | 처리 완료 | Phase 2 event 실패 테스트가 actual endpoint/Spring transaction을 통과하지 않고 DB save/flush·내부 rollback을 직접 단언하지 않는다. | `P2-R4`, `P2-R4-GATE` | actual POST/PUT persistence·event 실패와 transaction 종료 뒤 내부·외부 결과를 고정했다. |
| `REV-016` | High | 처리 완료 | Phase 3 생성 필수 `coverImage`·`audioFile`이 nullable이라 exact `MissingServletRequestPartException` 계약을 우회했다. | `P3-R5`, `P3-R4-GATE` | non-null binding과 세 part별 exact exception·KO/EN/JA·no-side-effect를 복구했다. |
| `REV-017` | Medium | 처리 완료 | Phase 3 4차 보강이 unknown target에 한정돼 cross-owner/domain KO/EN/JA와 DB/S3/event matrix가 완료 기록보다 좁다. | `P3-R6`, `P3-R4-GATE` | 대표 cross-owner/series/date actual endpoint의 전체 no-side-effect matrix를 보강했다. |
| `REV-018` | High | 처리 완료 | 캐릭터 생성 Endpoint Contract Summary가 필수 `systemPrompt`를 누락하고 request 금지 `externalCharacterId`, `isActive`를 포함해 확정 계약과 반대다. | `P2-R5`, `P2-R5-GATE` | 생성 JSON을 `DEC-P2-T4-001`과 동기화하고 기존 actual endpoint 계약 회귀로 확인했다. |
| `REV-019` | Medium | 처리 완료 | 생성·수정의 빈 multipart 파일이 null/non-empty 검사 사이를 통과해 0-byte upload, cover 교체 또는 수정 `audioFile` 미지원 계약을 우회한다. | `P3-R7`, `P3-R5-GATE` | v2 facade에서 생성 empty-file 거부, 수정 empty cover 정규화와 모든 audio part 거부를 RED/GREEN으로 고정했다. |
| `REV-020` | Low | 처리 완료 | Phase 3 ownership/domain test의 event no-interaction mock이 실제 `AudioContentService`·`CreatorAdminContentService` publisher field에 연결되지 않았다. | `P3-R8`, `P3-R5-GATE` | 실제 두 service proxy target의 publisher를 mock으로 교체·복원하고 identity/no-interaction을 단언했다. |
| `REV-021` | Low | 처리 완료 | Character POST/PUT이 `data: null`만 반환하는데 facade가 전체 response DTO를 매핑해 controller가 버린다. | `P2-R6`, `P2-R6-GATE` | facade 반환형을 `Unit`으로 축소하고 미사용 mapping을 제거한 뒤 mutation 계약을 회귀했다. |
| `REV-022` | Low | 처리 완료 | AudioContent 관리자 repository에 실제 호출되지 않는 조회·series 교체 helper와 그 전용 status enum이 남아 있다. | `P3-R9`, `P3-R9-GATE` | 사용 중인 owner-scoped 상세 조회만 남기고 호출 0건 코드를 제거한 뒤 content 회귀를 실행했다. |
| `REV-023` | High | 처리 완료 | OpenAPI Series 9개 operation에 없는 `DELETE /series/{seriesId}`가 구현·테스트되어 공개 API 표면이 계약보다 넓다. | `P4-R1`, `P4-R1-GATE` | 계약 밖 route/facade를 제거하고 DELETE 성공 기대를 405와 `PUT isActive=false` 계약으로 교정했다. |
| `REV-024` | Medium | 처리 완료 | Series 콘텐츠 추가·순서 변경 JSON body가 permissive ObjectMapper binding으로 `additionalProperties: false`를 강제하지 않는다. | `P4-R1`, `P4-R1-GATE` | 두 body를 strict parse하고 미지 필드 400/no-side-effect를 actual endpoint로 고정했다. |
| `REV-025` | High | 처리 완료 | Community create/update의 수동 JSON parse 오류가 공통 400으로 변환되지 않아 malformed payload가 500이 될 수 있고 미지 필드도 허용된다. | `P5-R1`, `P5-R1-GATE` | legacy 호출 전 strict parse와 mapping 예외 변환을 적용하고 400/no-side-effect를 고정했다. |
| `REV-026` | Medium | 처리 완료 | Community 목록이 OpenAPI에 없는 `size <= 50` 상한을 적용해 계약상 유효한 `size=51`을 400으로 거부한다. | `P5-R1`, `P5-R1-GATE` | 문서에 없는 상한 guard를 제거하고 `size=51` actual endpoint 계약을 고정했다. |
| `REV-027` | High | 처리 완료 | FanTalk 목록이 공개 v2 계약의 page/size 보정 대신 범위 밖 값을 400으로 거부하고 `size=1`도 허용한다. | `P6-R1`, `P6-R1-GATE` | 공개 v2 query policy를 재사용하고 경계값 actual test를 교정했다. |
| `REV-028` | Medium | 처리 완료 | FanTalk reply JSON body가 permissive binding으로 OpenAPI의 `additionalProperties: false`를 강제하지 않는다. | `P6-R1`, `P6-R1-GATE` | reply body를 strict parse하고 미지 필드 400/no insert/no event를 고정했다. |
| `REV-029` | Medium | 처리 완료 | plan/API 계약 설명/OpenAPI `x-implementation-status`가 완료된 구현을 여전히 정합화 필요·예정으로 표시한다. | `P7-R1`, `P7-R1-GATE` | 23개 operation metadata를 모두 `implemented`로 동기화하고 controller mapping 23개와 validator/client 생성을 확인했다. |
| `REV-030` | Medium | 처리 완료 | AudioContent 생성의 잘못된 `releaseDate` 형식·`timezone`이 legacy Java time 예외로 빠져 500이 된다. | `P3-R10`, `P3-R10-GATE` | strict parse 결과의 날짜·시간대를 legacy 호출 전에 검증하고 400/no-side-effect를 고정했다. |
| `REV-031` | Medium | 처리 완료 | 시리즈에 연결된 콘텐츠를 soft delete하면 추가 적격성 guard 때문에 연결을 해제할 수 없다. | `P4-R2`, `P4-R2-GATE` | 해제는 실제 owner link만 검증하고 add/search 전용 active/release/duration 적격성은 적용하지 않는다. |
| `REV-032` | Medium | 처리 완료 | OpenAPI 필수 시리즈 생성 `image`가 nullable binding이라 공통 missing-part 오류 계약을 우회한다. | `P4-R2`, `P4-R2-GATE` | 생성 image를 non-null binding으로 바꾸고 exact exception·KO/EN/JA·no-side-effect를 고정했다. |
| `REV-033` | High | 처리 완료 | Community 최대 고정 수가 lock 없는 count-then-update라 실제 병렬 요청에서 3개를 초과할 수 있다. | `P5-R2`, `P5-R2-GATE` | 기존 owner row pessimistic lock으로 고정/해제를 직렬화하고 결정적 병렬 회귀 테스트를 추가했다. |
| `REV-034` | Medium | 처리 완료 | OpenAPI 필수 캐릭터 생성 `image`를 빈 part로 보내면 외부 생성과 DB 저장이 진행되고 이미지 없는 캐릭터가 생성된다. | `P2-R7`, `P2-R7-GATE` | facade에서 empty file을 외부 API·DB·S3·event 전에 400으로 거부하고 no-side-effect를 고정했다. |
| `REV-035` | Medium | 처리 완료 | 캐릭터 수정의 `isActive=true` 단독 요청이 OpenAPI·레거시에서는 유효하지만 v2 no-change guard에서 400으로 거부된다. | `P2-R7`, `P2-R7-GATE` | non-null `isActive`를 변경 요청으로 인정해 레거시 200 `data: null` no-op parity를 복구했다. |
| `REV-036` | High | 처리 완료 | 미래 예약 오디오 콘텐츠 상세의 `releaseDate`가 mapper에서 항상 null로 고정되어 레거시 locale별 공개 예정 시각을 숨긴다. | `P3-R11`, `P3-R11-GATE` | 미래/과거와 KO/EN/JA 레거시 표시 규칙을 mapper에 최소 이관하고 상세 회귀를 고정했다. |
| `REV-037` | Medium | 처리 완료 | 시리즈 생성·수정의 빈 image가 legacy service로 전달되어 0-byte S3 upload와 cover 생성·교체를 유발한다. | `P4-R3`, `P4-R3-GATE` | 생성 empty file은 거부하고 수정 empty file은 생략으로 정규화해 S3/DB/event 경계를 고정했다. |
| `REV-038` | Low | 처리 완료 | 완료 증거와 Gate가 존재하는 `Task 2.13`, `Task 3.21`, `Task 4.9`, `Task 7.5` 헤더가 `[ ]`로 남아 상단 `구현 완료`·완료 수와 모순된다. | `P7-R4` | 기존 완료 기록을 보존하며 네 Task 헤더와 상태표·Progress만 동기화했다. |
| `REV-039` | Low | 처리 완료 | Phase 4 endpoint 설명은 콘텐츠 해제를 `DELETE .../contents` + request body로 적었지만 OpenAPI와 controller는 `DELETE .../contents/{contentId}` + body 없음이다. | `P7-R4` | Phase 4 설명만 기계 검증 가능한 OpenAPI와 실제 route에 맞췄다. |
| `REV-040` | Medium | 처리 완료 | 캐릭터 생성 관계의 OpenAPI 필수 정수 `importance`가 누락·null이어도 Kotlin/Jackson primitive 기본값 `0`으로 역직렬화되어 mutation이 진행될 수 있다. | `P2-R8`, `P2-R8-GATE` | v2 생성 경계에서 primitive null/누락을 400으로 거부하고 400/no-side-effect 회귀를 추가했다. |
| `REV-041` | High | 처리 완료 | 오디오 생성의 필수 `price` 누락·null과 non-null primitive의 explicit null이 JVM 기본값으로 보정되어 파일 업로드·DB mutation이 진행될 수 있고 문서 기본값 의미도 바뀔 수 있다. | `P3-R12`, `P3-R12-GATE` | v2 생성 경계에서 primitive null/누락을 400으로 거부하고 optional 생략 기본값과 no-side-effect 회귀를 추가했다. |
| `REV-042` | Medium | 처리 완료 | 시리즈 생성의 non-null `genreId`, `isAdult`에 explicit null이 들어와도 primitive 기본값 `0`/`false`로 보정될 수 있으며, 특히 `isAdult: null`은 정상 생성으로 이어질 수 있다. | `P4-R4`, `P4-R4-GATE` | v2 생성 경계에서 explicit null을 400으로 거부하고 필드 생략 기본값은 유지했다. |
| `REV-043` | High | 처리 완료 | 커뮤니티 생성 필수 boolean 누락·null과 `price: null`이 false·0으로 보정되고, 수정 `isFixed: null`은 생략과 구분되지 않아 유효 요청처럼 처리될 수 있다. | `P5-R3`, `P5-R3-GATE` | v2 create/update 경계에서 required/non-null primitive를 검증하고 생략 의미와 no-side-effect를 고정했다. |
| `REV-044` | High | 처리 완료 | 캐릭터 등록 화면에서 사용할 신규 v2 원작 검색 operation이 없다. | `P2-R9`, `P2-R9-GATE` | 기존 무페이징 검색과 `OriginalWorkResponse`를 재사용하는 관리자 endpoint를 구현했다. |
| `REV-045` | High | 처리 완료 | target AI 소유 오디오 콘텐츠의 댓글·답글 조회/작성/수정/삭제 operation이 없다. | `P3-R13`, `P3-R13-GATE` | actor·owner·parent 검증과 row-only soft delete를 포함한 5개 operation을 구현했다. |
| `REV-046` | Medium | 처리 완료 | 시리즈 등록 화면에서 사용할 신규 v2 활성 장르 목록 operation이 없다. | `P4-R5`, `P4-R5-GATE` | 기존 활성/`orders` 조회와 장르 response를 재사용하는 관리자 endpoint를 구현했다. |
| `REV-047` | Medium | 처리 완료 | 시리즈 상세 `data`가 목록 item과 다른 레거시 상세 schema를 반환한다. | `P4-R6`, `P4-R6-GATE` | 상세를 목록 item의 단일 객체와 동일한 11개 필드·타입으로 정합화했다. |
| `REV-048` | High | 처리 완료 | target AI 소유 커뮤니티 게시글의 댓글·답글 조회/작성/수정/삭제 operation이 없다. | `P5-R5`, `P5-R5-GATE` | actor·owner·parent 검증과 row-only soft delete를 포함한 5개 operation을 구현했다. |
| `REV-049` | High | 처리 완료 | target AI 채널에서 팬이 작성한 FanTalk root를 삭제할 관리자 operation이 없다. | `P6-R2`, `P6-R2-GATE` | 팬 작성 root만 soft delete하고 creator reply row를 보존하는 DELETE를 구현했다. |
| `REV-050` | High | 처리 완료 | 오디오 생성은 `timezone` body와 로컬 날짜를 받고 상세·댓글·답글 GET은 `timezone` query 및 locale/legacy 날짜 문자열을 사용해 최신 UTC 계약과 다르다. | `P3-R14`, `P3-R14-GATE` | v2 생성 DTO·내부 UTC instant 경계와 세 GET의 UTC mapping을 최소 구현하고 legacy/public 회귀를 고정했다. |
| `REV-051` | High | 처리 완료 | 커뮤니티 댓글·답글 GET이 `timezone` query와 timezone별 `date` 표시 문자열을 사용해 최신 UTC 계약과 다르다. | `P5-R6`, `P5-R6-GATE` | v2 GET에서 timezone을 제거하고 기존 `date` 필드를 UTC로 mapping하며 legacy/public 회귀를 고정했다. |
| `REV-052` | High | 처리 완료 | 오디오 댓글·답글 목록의 `page`, `size`가 OpenAPI에서는 optional 기본값 0/20이지만 controller와 facade는 둘 다 명시한 요청만 허용한다. | `P3-R15`, `P3-R15-GATE` | 두 GET의 전체·부분 생략 기본값을 복구하고 미지 query·범위 오류·UTC/ownership 회귀를 고정했다. |
| `REV-053` | High | 처리 완료 | 커뮤니티 댓글 작성·수정 mapping이 `application/json`을 강제하지 않아 OpenAPI의 JSON-only request와 미지원 media type 415 계약을 보장하지 못한다. | `P5-R7`, `P5-R7-GATE` | POST·PUT에 JSON `consumes`를 추가하고 415 header/envelope/no-side-effect를 actual endpoint로 고정했다. |
| `REV-054` | High | 처리 완료 | FanTalk 답변 작성 mapping이 `application/json`을 강제하지 않아 OpenAPI의 JSON-only request와 미지원 media type 415 계약을 보장하지 못한다. | `P6-R3`, `P6-R3-GATE` | reply POST에 JSON `consumes`를 추가하고 415 header/envelope/no-side-effect와 정상 축약 응답을 회귀했다. |
| `REV-055` | High | 처리 완료 | Character 생성·수정 multipart의 `request` part는 OpenAPI상 `application/json`이지만 `@RequestPart String` binding이 part media type을 강제하지 않아 415 계약을 보장하지 못한다. | `P2-R10`, `P2-R10-GATE` | controller part header에서 JSON 호환 여부를 확인해 POST·PUT의 415 `Accept`/envelope와 external/S3/DB/event no-side-effect를 고정했다. |
| `REV-056` | High | 처리 완료 | AudioContent 생성·수정 multipart의 `request` part가 같은 이유로 `text/plain`도 수용하며 실제 수정 테스트가 이를 200으로 기대한다. | `P3-R16`, `P3-R16-GATE` | POST·PUT controller part header에서 JSON 호환 여부를 강제하고 text/plain·Content-Type 누락 KO/EN/JA 415 `Accept`/envelope와 S3·DB·event no-side-effect를 고정했다. |
| `REV-057` | High | 처리 완료 | Series 생성·수정 multipart의 `request` part는 OpenAPI상 `application/json`이지만 `@RequestPart String` binding이 part media type을 강제하지 않아 415 계약을 보장하지 못한다. | `P4-R7`, `P4-R7-GATE` | POST·PUT controller part header에서 JSON 호환 여부를 강제하고 text/plain·Content-Type 누락 KO/EN/JA 415 `Accept`/envelope와 S3/DB/event no-side-effect를 고정했다. |
| `REV-058` | High | 처리 완료 | Community 게시글 생성·수정 multipart의 `request` part는 OpenAPI상 `application/json`이지만 `@RequestPart String` binding이 part media type을 강제하지 않아 415 계약을 보장하지 못한다. | `P5-R8`, `P5-R8-GATE` | POST·PUT controller part header에서 JSON 호환 여부를 강제하고 text/plain·Content-Type 누락 KO/EN/JA 415 `Accept`/envelope와 S3/DB no-side-effect를 고정했다. |
| `REV-059` | High | 처리 완료 | 선택한 AI 캐릭터가 작성한 FanTalk 답변의 내용·활성 상태를 수정할 V2 관리자 operation이 없다. | `P6-R4`, `P6-R4-GATE` | 레거시 `PUT /explorer/profile/cheers` 계약을 유지하고 target AI·활성 root·direct reply로 한정한 PUT을 구현했다. |
| `REV-060` | Medium | 처리 완료 | Character 생성·수정 multipart schema는 `additionalProperties: false`지만 controller가 전체 part 이름을 검증하지 않아 `image`, `request` 외 part를 무시하고 mutation을 진행한다. | `P2-R11`, `P2-R11-GATE` | operation별 허용 part 집합을 검사하고 미정의 part 400/no-side-effect를 고정했다. |
| `REV-061` | Medium | 처리 완료 | AudioContent 생성·수정도 미정의 multipart part를 무시한다. 수정은 `audioFile`, `contentFile`만 명시적으로 거부해 다른 이름의 추가 part가 정상 mutation을 통과한다. | `P3-R17`, `P3-R17-GATE` | 생성·수정의 서로 다른 허용 part 집합을 검사하고 기존 수정 파일 교체 거부를 같은 경계로 통합했다. |
| `REV-062` | Medium | 처리 완료 | Series 생성·수정 multipart schema는 `image`, `request` 외 part를 금지하지만 controller는 추가 part를 검사하지 않는다. | `P4-R8`, `P4-R8-GATE` | operation별 허용 part 집합과 미정의 part 400/no-side-effect 회귀를 추가했다. |
| `REV-063` | Medium | 처리 완료 | Community 생성·수정은 허용 part 집합이 다른데 전체 part 이름을 검사하지 않아 특히 수정의 `audioFile` 등 미정의 part를 무시하고 mutation을 진행한다. | `P5-R9`, `P5-R9-GATE` | 생성·수정의 허용 part 집합을 각각 검사하고 미정의 part 400/no-side-effect를 고정했다. |
| `REV-064` | Low | 처리 완료 | OpenAPI와 controller는 37개 구현 완료인데 `plan-task.md` Endpoint Contract Summary와 `api-contract.md`는 36개 구현·FanTalk 답변 수정 1개 planned 상태로 남아 있다. | `P7-R9`, `P7-R9-GATE` | 두 규범 설명의 집계·endpoint 상태·client 설명을 37개 구현 완료로 동기화했다. |
| `REV-065` | Medium | 처리 완료 | Character 생성·수정의 미정의 multipart 검사가 `fileMap.keys`만 보므로 filename 없는 일반 form-field part가 `{image, request}` allow-list를 우회한다. | `P2-R12`, `P2-R12-GATE` | 파일 map과 servlet 전체 part 이름을 모두 검사하고 일반 form-field 미정의 part 400/no-side-effect를 고정했다. |
| `REV-066` | Medium | 처리 완료 | AudioContent 생성·수정도 `fileMap.keys`만 검사해 filename 없는 일반 form-field 미정의 part를 허용한다. | `P3-R18`, `P3-R18-GATE` | operation별 파일 map과 servlet 전체 part 이름 집합, 일반 form-field 회귀를 추가했다. |
| `REV-067` | Medium | 처리 완료 | Series 생성·수정도 `fileMap.keys`만 검사해 filename 없는 일반 form-field 미정의 part를 허용한다. | `P4-R9`, `P4-R9-GATE` | 파일 map과 servlet 전체 part 이름을 `{image, request}`와 대조하고 400/no-side-effect를 고정했다. |
| `REV-068` | Medium | 처리 완료 | Series 생성·수정의 active genre 검사가 양수에만 존재 여부를 확인해 `genreId <= 0`을 legacy non-null repository 경계로 전달하고 예상 밖 500을 유발할 수 있다. | `P4-R10`, `P4-R10-GATE` | 0 이하 또는 비활성·미존재 장르를 legacy 호출 전에 공통 400으로 거부했다. |
| `REV-069` | Medium | 처리 완료 | Community 생성·수정도 `fileMap.keys`만 검사해 filename 없는 일반 form-field 미정의 part를 허용한다. | `P5-R10`, `P5-R10-GATE` | operation별 파일 map과 servlet 전체 part 이름 집합, 일반 form-field 회귀를 추가했다. |
| `REV-070` | Low | 처리 완료 | FanTalk 삭제에서 OpenAPI·구현·테스트는 동일 target의 이미 비활성인 팬 root를 성공 no-op으로 처리하지만 PRD는 400으로 기술하고 계약 설명도 상충한다. | `P6-R5`, `P6-R5-GATE` | OpenAPI/runtime의 no-op 계약에 맞춰 PRD와 `api-contract.md` 설명만 동기화했다. |
| `REV-071` | Low | 처리 완료 | 완료 Gate가 존재하는 `REV-060`~`REV-064` 일부가 finding 표에서 여전히 `확정`으로 남아 상단 구현 완료 상태와 모순된다. | `P7-R10`, `P7-R10-GATE` | 후속 Gate 완료 뒤 기존·신규 finding 상태와 Phase 집계를 실제 결과에 맞췄다. |
| `REV-072` | High | 처리 완료 | v2 오디오 생성은 parsed request overload를 호출하지만 기존 preview 시간 쌍·형식·최소 15초 검증은 문자열 request overload에만 있어 잘못된 입력이 DB 저장과 S3 upload·event publish 경계로 진행된다. | `P3-R19`, `P3-R19-GATE`, `P7-R11`, `P7-R11-GATE` | 기존 `validatePreviewTime` 호출을 두 생성 경로가 공유하는 overload로 이동하고 actual endpoint·legacy 회귀와 no-side-effect를 고정한 뒤 37개 operation 통합 재판정을 완료했다. |
## 검증 기록
- Phase 1~7 9차 정적 리뷰(2026-07-29): PRD, plan, OpenAPI 37개 operation과 현재
controller/facade/service/repository/test 소스를 사용자 지시에 따라 컴파일·테스트 실행 없이 대조했다.
`AiCharacterAdminAudioContentFacade.create`는 parsed request overload를 호출하지만 기존 creator 생성의
`validatePreviewTime`은 문자열 request overload에서만 호출되는 `REV-072`를 확정했다. 한쪽만 있는 preview,
잘못된 형식, 15초 미만 입력을 거부하는 v2 회귀 테스트는 없고 정상 값만 존재했다. Phase 1·2·4·5·6에는 신규
finding이 없으며 Phase 7 완료 판정은 Phase 3 보완 뒤 재수행한다. OpenAPI JSON 문법, operation 37개·고유
operationId 37개·`implemented` 37개를 `jq`로 확인했다. production/test/PRD/OpenAPI는 변경하지 않았고
`Task 3.29`, `Task 7.13`과 Phase별 리뷰 기록만 추가했다.
- Phase 1~7 8차 정적 리뷰(2026-07-29): PRD, plan, OpenAPI 37개 operation과 현재
controller/facade/repository/test 소스를 사용자 지시에 따라 컴파일·테스트 실행 없이 대조했다. Character,
AudioContent, Series, Community controller의 미정의 multipart 검사가 `fileMap.keys`에 한정되어 filename 없는 일반
form-field part를 확인하지 못하는 `REV-065`~`REV-067`, `REV-069`를 확정했다. Series의 `genreId <= 0`이 active
genre 사전 검증을 우회하는 `REV-068`, FanTalk 비활성 root 삭제 설명이 OpenAPI·runtime과 상충하는 `REV-070`,
완료 Gate와 finding 상태가 어긋나는 `REV-071`도 확정했다. Phase 1은 신규 finding이 없다. 테스트 명령은 실행하지
않았고, `rg`·`jq`·`nl`과 `javap`만 사용했다. 로컬 Spring Web 5.3.29 bytecode에서 filename 없는 part가 file map이
아니라 parameter 이름 집합으로 분류됨을, 기존 compile output에서는 legacy genre repository의 Kotlin non-null
check를 정적으로 확인했다. production/test/OpenAPI는 변경하지 않았으며 신규 Task와 Phase별 리뷰 문서만 추가했다. OpenAPI는
operation 37개·고유 operationId 37개·`implemented` 37개였고, code fence 짝과 신규 미완료 Task/Gate를 확인했다.
`./gradlew tasks --all`은 컴파일·테스트 없이 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력 없이 성공했다.
- Phase 1~7 7차 정적 리뷰(2026-07-29): PRD, plan, OpenAPI 37개 operation과 현재 controller/facade/test를
사용자 지시에 따라 컴파일·테스트 실행 없이 대조했다. OpenAPI의 8개 multipart schema는 모두
`additionalProperties: false`이고 operation별 허용 part가 명시돼 있지만 네 domain controller는 전체 part 이름
집합을 검증하지 않아 `REV-060`~`REV-063`을 확정했다. OpenAPI 37개 `implemented`와 controller 37개 mapping에
비해 계획 요약·계약 설명이 36개 구현/1개 planned인 `REV-064`도 확정했다. Phase 1·6은 신규 finding이 없으며,
production/test/OpenAPI는 변경하지 않았다. 정적 assertion은 37개 operationId·37개 `implemented`·내부 `$ref`,
8개 multipart `additionalProperties: false`, controller mapping 37개를 확인했고, 신규 Goal/Task ID는 각각 1개로
유일했다. `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`와 trailing whitespace 검사는
출력 없이 성공했으며 문서 code fence도 모두 짝이 맞았다.
- `P7-R8` / `P7-R8-GATE` 검증(2026-07-29): `REV-052`~`REV-059` 처리 뒤 OpenAPI 37개
operationId/고유 operationId/status `implemented` 37개와 controller mapping 37개를 대조했다. 선행 Gate 미체크와
`REV-052`~`REV-059` 미처리 항목은 없었고, dependency·DDL 변경 파일도 없었다. 영향 14개 operation과 공통
error/authorization focused 회귀는 `BUILD SUCCESSFUL in 1m 26s`, 전체 `./gradlew test`는
`BUILD SUCCESSFUL in 8m 8s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`, `git diff --check`는
출력이 없었다. Phase 7 Gate 기준에 따라 문서 상태를 `구현 완료`로 갱신했다.
- `P6-R4` / `P6-R4-GATE` 검증(2026-07-29): `AiCharacterAdminFanTalkReplyUpdateTest`와
`AiCharacterAdminFanTalkReplyUpdateContractTest`에 답변 수정 success/no-op/재활성화, target/root/direct-reply
ownership, malformed/unknown-field/415 `Accept`/DB·event no-side-effect 회귀를 추가했다. RED는 신규 focused
명령에서 15건이 미구현 route 404로 `BUILD FAILED in 49s`였다. PUT route, strict request DTO, active root와
target AI writer/creator direct reply를 검증하는 repository query, non-null field만 반영하는 facade를 추가한 뒤
분리 focused 재실행은 `BUILD SUCCESSFUL in 42s`, FanTalk/common/legacy 영향 범위 회귀는
`BUILD SUCCESSFUL in 1m 2s`였다. OpenAPI status 집계는 37개 operation 모두 `implemented`, `ktlintCheck`는
import/format 수정 뒤 `BUILD SUCCESSFUL in 25s`, `git diff --check`는 출력이 없었다. 신규 dependency·DDL과
legacy/public API 변경은 없다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk reply update와 FanTalk/common/legacy
영향 범위에 포함되므로 생략했다.
- `P6-R3` / `P6-R3-GATE` 검증(2026-07-29): `AiCharacterAdminFanTalkReplyContractTest`에 KO/EN/JA
`text/plain` reply POST 415 envelope, `Accept`, reply insert/event no-side-effect 회귀를 추가했다. RED는 focused
명령에서 3개 invocation이 `status().isUnsupportedMediaType` 기대 실패로 `BUILD FAILED in 33s`였다. reply POST
mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가한 뒤 같은 focused 재실행은
`BUILD SUCCESSFUL in 41s`, FanTalk/common 영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는
`BUILD SUCCESSFUL in 47s`였다. 전체 `./gradlew test`는 변경 범위가 v2 FanTalk reply POST media type 경계와
공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 생략했다.
- `P5-R8` / `P5-R8-GATE` 검증(2026-07-29): `AiCharacterAdminCommunityPostCreateTest`와
`AiCharacterAdminCommunityPostUpdateTest`에 KO/EN/JA `text/plain` 및 Content-Type 누락 `request` part 415 envelope,
`Accept`, DB/S3 no-side-effect 회귀를 추가했다. RED는 focused 명령에서 12개 invocation이
`status().isUnsupportedMediaType` 기대 실패로 `BUILD FAILED in 56s`였다. POST·PUT controller 경계에
JSON 호환 part media type 검사를 추가한 뒤 같은 focused 재실행은 `BUILD SUCCESSFUL in 59s`, community/common
영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 55s`였다. 전체 `./gradlew test`는
변경 범위가 v2 community 게시글 multipart request part 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가
이를 포함하므로 생략했다.
- `P5-R7` / `P5-R7-GATE` 검증(2026-07-29): `AiCharacterAdminCommunityPostCommentTest`에 KO/EN/JA `text/plain`
POST·PUT 415 envelope/`Accept`/작성 insert·수정 row·event no-side-effect 회귀를 추가했다. RED는 같은 focused 명령에서
3개 locale invocation이 `status().isUnsupportedMediaType` 기대 실패로 `BUILD FAILED in 30s`였다. 두 댓글 mapping에
`consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가한 뒤 focused 재실행은 `BUILD SUCCESSFUL in 30s`, community/common
영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 57s`, `./gradlew ktlintCheck`는
`BUILD SUCCESSFUL in 12s`였다. `git diff --check`는 출력 없음을 확인했다. 전체 `./gradlew test`는 변경 범위가 v2
community 댓글 JSON media type 경계와 공통 오류 계약에 한정되고 focused/영향 범위 회귀가 이를 포함하므로 생략했다.
- FanTalk 답변 수정 계약·계획 문서 검증(2026-07-29): production/test 파일은 변경하지 않고 PRD, OpenAPI 2.3.0,
계약 설명, plan과 Phase 6·7 리뷰에 `REV-059`, `Task 6.9` / `P6-R4`·Gate를 추가했다. OpenAPI는 전체 37개
operation, 기존 `implemented` 36개와 신규 `planned` 1개로 구성하며 신규 PUT의 request는 optional/nullable
`content`, `isActive`, 성공 `data`는 `CreatorChannelFanTalkResponse` 필드 형태다. JSON 문법·내부 `$ref
operationId·200 response·상태 집계, Markdown 구조와 `git diff --check`를 정적으로 확인했다.
`./gradlew tasks --all`은 최초 sandbox cache lock 권한 실패 후 허용된 재실행에서 `BUILD SUCCESSFUL in 742ms`였다.
사용자 지시에 따라 컴파일·테스트·lint는 실행하지 않았다.
- Phase 1~7 6차 정적 리뷰 검증(2026-07-29): 현재 working tree를 기준으로 PRD·plan·OpenAPI와
Phase 1~7 production/test 코드를 대조했다. `jq`로 OpenAPI JSON 문법, operation 36개, 고유 operationId 36개를
확인했고 controller mapping도 36개다. OpenAPI 공통 `Page`/`Size`는 optional 기본값 0/20이나 오디오 댓글·답글
GET 두 곳은 기본값이 없고 facade가 query 이름의 정확한 집합을 요구한다. OpenAPI가 `application/json` request와
415를 선언한 mutation 중 커뮤니티 댓글 POST·PUT과 FanTalk reply POST 세 곳은 mapping에 JSON `consumes`가 없다.
OpenAPI와 계약 설명이 Character·AudioContent·Series·Community 생성·수정 8개 multipart의 `request` part를
`application/json`으로 고정하지만 모든 controller는 `@RequestPart String`으로 받아 part media type을 강제하지 않는다.
AudioContent 수정 테스트에는 `text/plain` request part를 보내 200을 기대하는 실제 증거도 존재한다.
변경 파일 기준 신규 dependency·migration·DDL 경로는 없었다. 문서 정적 검증만 수행했으며 사용자 지시에 따라
Gradle, 컴파일, 테스트는 실행하지 않았다.
- UTC 날짜 계약 문서 반영 검증(2026-07-29): production/test 파일은 변경하지 않고 PRD, OpenAPI 2.2.0,
계약 설명, plan과 Phase 3·5·7 리뷰만 갱신했다. `jq`로 JSON 문법, 내부 `$ref` 누락 0건, operationId 중복 0건,
200 response 누락 0건, 전체 36개 operation과 `implemented` 30개/`alignment-required` 6개/`planned` 0개,
정확한 영향 operation 6개를 확인했다. query `timezone` parameter와 `components.parameters.Timezone`, 생성
`AudioContentCreateRequest.timezone`은 모두 0건이며 생성·상세 `releaseDate`와 오디오·커뮤니티 댓글 `date`는
`date-time` + `Z$`로 확인했다. plan Task 집계는 Phase 1~7 순서대로 7/7, 15/15, 23/24, 12/12, 11/12,
7/7, 8/9이며 미완료 Task는 `Task 3.24`, `Task 5.12`, `Task 7.9` 세 건이다. `git diff --check`는 출력이
없었고 `./gradlew tasks --all`은 첫 sandbox 실행에서 Gradle cache lock 권한으로 실패한 뒤 허용된 재실행에서
`BUILD SUCCESSFUL in 896ms`였다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다.
- UTC 날짜 계약 구현 완료 검증(2026-07-29): `P3-R14`, `P5-R6`, `P7-R7`을 완료했다. 오디오 focused 재실행은
`BUILD SUCCESSFUL in 52s`, 커뮤니티 댓글 focused `--rerun-tasks` 재실행은 `BUILD SUCCESSFUL in 4m 33s`였다.
OpenAPI 집계는 36개 operation, 36개 `implemented`, 0개 `alignment-required`, query `timezone` parameter 0개,
`components.parameters.Timezone` 0개였고, controller mapping은 36개였다. `P3-R14-GATE`와 `P5-R6-GATE`의
영향 범위 회귀와 `ktlintCheck`, `git diff --check` 성공 기록을 대조해 Phase 7 상태를 `구현 완료`로 동기화했다.
- `P6-R2` 검증(2026-07-29): 팬 작성 FanTalk root DELETE endpoint를 actual endpoint로 구현했다. RED는
`AiCharacterAdminFanTalkDeleteTest` 신규 6건이 미구현 route로 실패했다. 구현 후 누락 ID 계약을 포함한 focused
재실행은 `BUILD SUCCESSFUL in 29s`였다. Oracle 리뷰에서 DELETE endpoint의 ADMIN 이중 인가 matrix 누락을 지적받아
`AiCharacterAdminAuthorizationTest`에 FanTalk delete request를 추가했고, authorization 단독 재실행은
`BUILD SUCCESSFUL in 29s`, FanTalk/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 58s`였다.
최종 `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 14s`, `git diff --check`는 출력 없음이었다. 전체 `./gradlew test`는
변경 범위가 v2 FanTalk 삭제 endpoint와 FanTalk/common 회귀에 포함되어 생략했다.
- `P5-R5` / `P5-R5-GATE` 검증(2026-07-29): 커뮤니티 댓글 CRUD 5개 operation을 actual endpoint로 구현했다.
RED는 `AiCharacterAdminCommunityPostCommentTest` 신규 7건이 미구현 route로 실패했다. 구현 후 focused 재실행은
`BUILD SUCCESSFUL in 2m`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 26s`였다.
`./gradlew ktlintCheck`는 facade import 순서 1건 실패 후 정리해 `BUILD SUCCESSFUL in 22s`였고,
`git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 community 댓글 endpoint와 community/common
회귀에 포함되어 생략했다.
- `P4-R6` / `P4-R6-GATE` 검증(2026-07-29): 시리즈 상세 `data`를 목록 item과 동일한 11개 field/type으로
정합화했다. RED는 `AiCharacterAdminSeriesQueryTest`의 상세 schema 테스트가 기존 레거시 상세 필드와 달라 실패했다.
구현 후 focused 단일 테스트는 `BUILD SUCCESSFUL in 43s`, query/contract focused 회귀는 `BUILD SUCCESSFUL in 38s`,
series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 19s`였다. `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 23s`,
`git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 series 상세 response mapper와 series/common
회귀에 포함되어 생략했다.
- `P4-R5` / `P4-R5-GATE` 검증(2026-07-29): 시리즈 등록용 장르 목록 endpoint를 actual endpoint로 구현했다.
RED는 `AiCharacterAdminSeriesGenreTest` 신규 2건이 미구현 route로 실패했다. 구현 후 focused 재실행은
`BUILD SUCCESSFUL in 1m 24s`, series/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 38s`였다.
`./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 29s`, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는
변경 범위가 v2 series reference endpoint와 series/common 회귀에 포함되어 생략했다.
- `P3-R13` / `P3-R13-GATE` 검증(2026-07-29): 오디오 콘텐츠 댓글 CRUD 5개 operation을 actual endpoint로 구현했다.
RED는 `AiCharacterAdminAudioContentCommentTest` 신규 7건이 미구현 route의 404/405로 실패했다. 구현 후 focused 재실행은
`BUILD SUCCESSFUL in 3m 16s`, content/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 3m 47s`였다.
`./gradlew ktlintCheck`는 신규 테스트 import 순서 1건 실패 후 정리해 `BUILD SUCCESSFUL in 32s`였고,
`git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 content 댓글 endpoint와 content/common
회귀에 포함되어 생략했다.
- 후속 기능 문서 보완 검증(2026-07-29): 사용자 지시에 따라 Gradle·컴파일·테스트는 실행하지 않았다.
`jq empty`로 OpenAPI JSON 문법을 확인했고 operation/status assertion은 전체 36개,
`implemented` 22개, `planned` 13개, `alignment-required` 1개와 고유 operationId 36개를 확인해 `true`였다.
내부 `$ref` 누락은 0건이고 시리즈 상세 `data`는 `SeriesListItem`을 참조하며 구 `SeriesDetailResponse` schema가
없음을 확인했다. Phase별 Task 집계는 1~7 순서로 7/15/23/12/11/7/8이며 상단 완료/전체 수와 일치했고,
계약 표 집계도 22/13/1과 일치했다. 문서 경로의 trailing whitespace 검사와 추적 문서
`git diff --check`는 출력이 없었다.
- `P5-R4` / `P5-R4-GATE` 검증(2026-07-29): Community 목록의 `timezone` query 제거와
`totalCount/page/size/hasNext/items` wrapper 계약을 actual endpoint로 고정했다. RED는
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostQueryTest'`에서
신규 3건이 400/직접 배열 응답 차이로 `BUILD FAILED`였다. controller/facade의 `timezone` parameter와 검증을 제거하고,
repository에 active owner count query를 추가해 wrapper를 구성한 뒤 같은 query focused는 `BUILD SUCCESSFUL in 1m 28s`였다.
최종 focused query+contract는 `BUILD SUCCESSFUL in 37s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 6s`,
OpenAPI jq assertion은 `true`, `./gradlew ktlintCheck`는 들여쓰기 1건 수정 후 `BUILD SUCCESSFUL in 12s`였다.
`git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 community 목록과 공통 authorization/error
회귀에 포함되어 생략했다.
- `P5-R3` / `P5-R3-GATE` 검증(2026-07-29): 커뮤니티 생성 `isCommentAvailable`/`isAdult` 누락·null, `price:null`, 수정 `isFixed:null` actual endpoint RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostUpdateTest --tests "*shouldRejectMissingOrNullPrimitiveCreateFieldsWithoutSideEffects" --tests "*shouldRejectNullIsFixedUpdateWithoutSideEffects"`에서 신규 6건이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. v2 community strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`, `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가하고 `isFixed:null` explicit null guard를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL in 41s`였다. 리뷰 보완으로 `price` 생략 기본값 0과 `isFixed` 생략 시 고정 상태 보존 assertion을 추가한 뒤 create/update focused는 `BUILD SUCCESSFUL in 34s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 2s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 13s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 community request reader와 community create/update tests에 한정되고 community/common 회귀가 영향 범위를 포함하므로 생략했다.
- `P4-R4` / `P4-R4-GATE` 검증(2026-07-29): 시리즈 생성 `genreId:null`, `isAdult:null` actual POST RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest --tests "*shouldRejectNullPrimitiveCreateFieldsBeforeSideEffects"`에서 신규 2개 invocation이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. v2 series strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesMutationTest`는 `BUILD SUCCESSFUL`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*" --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 12s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 20s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 series request reader와 series mutation test에 한정되고 series/common 회귀가 영향 범위를 포함하므로 생략했다.
- `P2-R8` / `P2-R8-GATE` 검증(2026-07-29): 관계 `importance` 누락·null actual POST RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests "*shouldRejectMissingRelationshipImportanceBeforeSideEffects" --tests "*shouldRejectNullRelationshipImportanceBeforeSideEffects"`에서 신규 2건이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. v2 character strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL in 48s`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`는 `BUILD SUCCESSFUL in 53s`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*" --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 38s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 22s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 character request reader와 character mutation test에 한정되고 character/common 회귀가 영향 범위를 포함하므로 생략했다.
- `P3-R12` / `P3-R12-GATE` 검증(2026-07-29): 오디오 생성 `price` 누락·null과 primitive field explicit null actual POST RED는 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests "*shouldRejectMissingOrNullPriceBeforeUpload" --tests "*shouldRejectNullPrimitiveFieldsBeforeUpload"`에서 8개 invocation이 `status().isBadRequest` 기대 실패로 `BUILD FAILED`였다. `themeId:null`은 기존 missing-theme guard로 이미 400이었다. v2 content strict reader에 `FAIL_ON_NULL_FOR_PRIMITIVES`와 `FAIL_ON_MISSING_CREATOR_PROPERTIES`를 추가한 뒤 같은 명령은 `BUILD SUCCESSFUL in 42s`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`는 `BUILD SUCCESSFUL in 36s`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*" --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 28s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 18s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 v2 content create request reader와 content create test에 한정되고 content/common 회귀가 영향 범위를 포함하므로 생략했다.
- Community 목록 계약·계획 동기화(2026-07-29): 사용자 확정에 따라 `timezone` query를 제거하고
`totalCount/page/size/hasNext/items` schema와 `Task 5.10` / `P5-R4`, `P5-R4-GATE`를 문서에 반영했다.
사용자 요청 범위가 계획 보강이므로 Gradle, 컴파일, 테스트는 실행하지 않았다. `jq empty`, 내부 `$ref` 해석,
23개 operation·고유 operationId를 확인했고 현재 구현 상태는 `implemented` 22개,
`implemented-contract-alignment-required` 1개(Community GET)였다. timezone parameter 부재와 response required 필드
5개를 assertion으로 확인했고 Task/의존 순서, Markdown code fence 균형을 점검했다. `git diff --check`는 출력이 없었다.
- Phase 1~7 5차 정적 리뷰(2026-07-29): multipart JSON DTO의 primitive required/nullability를
OpenAPI와 production strict reader, 로컬 Jackson Kotlin/databind 2.13.5 source까지 대조해 `REV-040`~`REV-043`
4건을 확정했다. Phase 1·6은 신규 finding 없음, Phase 7은 후속 통합 재판정 요청으로 판정했다. 사용자 요청에 따라
Gradle, 컴파일, 테스트는 실행하지 않았다. `jq`로 OpenAPI JSON과 23개 operation/status를 확인하고, controller
mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2, 합계 23개를 확인했다.
신규 Task 5개와 Gate 연결, 문서 code fence 균형을 확인했고 `git diff --check`는 출력이 없었다.
- `P7-R4` 문서 정합화 검증(2026-07-29): 완료 증거가 존재하는 네 Task 헤더와 상단 상태표를 완료로 동기화하고, 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 `rg`는 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2, 합계 23개를 확인했다. production/test/OpenAPI 변경은 없었다.
- Phase 1~7 4차 정적 리뷰(2026-07-29): PRD, plan-task, OpenAPI, production/test를 정적 대조해 기능상 신규 finding은
없었고 문서 정합성 `REV-038`~`REV-039`를 확정했다. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다.
`jq empty`와 23개 operation·고유 operationId·`implemented`·200 response assertion은 성공했고 controller mapping은
Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2로 23개였다. `git diff --check`는 출력이 없었고
`build.gradle.kts`, migration/DDL 경로 변경도 없었다.
- `P2-R7`~`P7-R3-GATE` 후속 보완 검증(2026-07-29): `REV-034`~`REV-037`을 각각 RED로 재현한 뒤 최소 production 수정으로 GREEN을 확인했다. focused 명령은 character mutation, audio query, series mutation 순서로 모두 최종 `BUILD SUCCESSFUL`이었다. 통합 검증으로 `./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 23개 operation/status `jq` assertion은 `true`, controller mapping count는 23, `git diff --check`는 출력이 없었고 신규 dependency/DDL 파일 변경은 없었다.
- Phase 1~7 3차 정적 리뷰(2026-07-28): empty multipart, optional boolean, 미래 예약일 표시를
production/legacy/OpenAPI/test까지 역추적해 `REV-034`~`REV-037` 4건을 확정했다. Phase 1·5·6은 신규 finding 없음,
Phase 7은 23개 operation/mapping/status 유지와 통합 재판정 요청으로 판정했다. 사용자 요청에 따라 Gradle,
컴파일, 테스트는 실행하지 않았으며 `rg`, `sed`, `jq`, `git diff` 기반 정적 대조만 수행했다. OpenAPI
operation/status assertion은 `true`, controller mapping 집계는 23개였고, 변경 문서의 code fence 균형과
`git diff --check`는 출력 없이 통과했다.
- Phase 1~7 후속 정적 리뷰(2026-07-28): 정상·순차 경로 밖의 의미 입력, soft-delete link, required multipart,
fixed-count concurrency를 production/legacy/test까지 역추적해 `REV-030`~`REV-033` 4건을 확정했다. Phase 1·2·6은
신규 finding 없음, Phase 7은 23개 operation/mapping/status 유지와 통합 재판정 요청으로 판정했다. 사용자가 현재
compile/test 통과 사실을 제공하고 재실행하지 말 것을 요청했으므로 `./gradlew test`와 compile 명령은 실행하지 않았다.
문서 가이드 확인용 `./gradlew tasks --all`만 실행해 `BUILD SUCCESSFUL`을 확인했고, `jq` 집계로 OpenAPI 23개
operation/status `implemented`, `rg` 집계로 관리자 controller mapping 23개를 확인했다. 변경 문서의
`git diff --check`, trailing whitespace와 code fence 균형 점검도 출력·오류 없이 종료했다.
- Phase 1~7 정적 리뷰(2026-07-28): PRD, plan-task, OpenAPI 23개 operation과 현재 controller/facade/repository/DTO/test를
정적 대조해 `REV-021`~`REV-029` 9건을 확정하고 Phase별 신규 Task/Gate 및 review 문서로 추적했다. `rg`, `jq`,
`git diff` 계열의 읽기·정적 점검만 사용했으며, 사용자가 현재 compile/test 통과 사실을 제공하고 재실행을 금지했으므로
`./gradlew`, 컴파일, 테스트는 실행하지 않았다.
- 실행 계획 동기화 검증(2026-07-28): OpenAPI 23개 고유 operation과 Character 4, AudioContent 5, Series 9,
Community 3, FanTalk 2 분류를 현재 Endpoint Contract Summary 및 `P23-CONTRACT-2`~Phase 7과 대조했다. Goal ID 중복은
없고 PRD Open Questions는 `없음`이며 Task 3.17·3.18에 추가한 production/test/characterization 파일의 존재를 확인했다.
- 실행 계획 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고
`BUILD SUCCESSFUL in 922ms`였다. `git diff --check`, `git diff --cached --check`는 출력이 없었다.
- `P23-CONTRACT-1` OpenAPI 검증(2026-07-28): `jq empty`, 23개 고유 operation과
`implemented-contract-alignment-required` 9개/`planned` 14개 assertion, 내부 `$ref` 해석을 실행해 모두 성공했다.
`npx --yes @redocly/cli lint --skip-rule info-license ...`는 `valid`, OpenAPI Generator `validate`는
`No validation issues detected`였다.
- `P23-CONTRACT-1` 클라이언트 검증(2026-07-28): OpenAPI Generator 7.24.0 `typescript-fetch` 생성을 완료하고,
생성된 23개 Raw operation(Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2)을 확인했다.
`tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM .../index.ts`는 TypeScript 5.9.3에서 성공했다.
- `P23-CONTRACT-1` 문서·리뷰 Gate(2026-07-28): `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 898ms`였고,
독립 레거시 DTO/controller/service 대조 및 생성물 재검증 결과 Critical 0, Important 0이었다.
- Phase 2·3 6차 보완 재점검 focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 19s`였다. 다섯 XML 합계 130건의 failure/error/skipped는 모두 0이었다.
- Phase 2·3 6차 보완 재점검 문서 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 858ms`였다.
- `P6-R1` RED/GREEN 검증(2026-07-28): `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkQueryTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.AiCharacterAdminFanTalkReplyContractTest"` 최초 실행은 pagination 400과 reply unknown-field 허용으로 4건 실패했다. 공개 v2 query policy 재사용과 reply strict reader 적용 후 동일 명령 재실행은 `BUILD SUCCESSFUL in 3m 14s`였다.
- `P6-R1-GATE` fresh 검증(2026-07-28): `./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`는 `BUILD SUCCESSFUL in 1m 36s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 35s`였고, `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 변경 범위가 FanTalk facade/controller/test와 문서에 한정되고 FanTalk/common 회귀가 영향 범위를 포함하므로 생략했다.
- `P7-R1` 문서/metadata 검증(2026-07-28): OpenAPI operation/status `jq` assertion은 `true`, 신규 prefix controller method mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 = 23개였다. OpenAPI Generator validate는 `No validation issues detected`, TypeScript client 생성은 성공했고 `npx --yes tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM /tmp/ai-character-admin-typescript-client/index.ts`는 출력 없이 exit 0이었다. `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다.
- `P7-R1-GATE` 최종 검증(2026-07-28): OpenAPI 23개 operation과 23개 `implemented` assertion은 `true`, 신규 prefix controller method mapping은 Character 4 + AudioContent 5 + Series 9 + Community 3 + FanTalk 2 = 23개였고, 미처리 `확정` review finding 검색은 출력이 없었다. OpenAPI Generator validate는 `No validation issues detected`, TypeScript client 생성과 compile은 exit 0, `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`, `git diff --check`는 출력이 없었다.
- Phase 2·3 6차 보완 재점검 판정(2026-07-28): `REV-018`~`REV-020`의 production/test 보완과 Gate 완료 증거가 일치했고 추가 production finding은 확정되지 않았다. 하단 종합 표에서만 미처리로 남은 `REV-001`~`REV-009`를 각 소유 Gate·Progress 판정에 맞춰 `처리 완료`로 동기화했다.
- `P3-R5-GATE` content/common 회귀(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 reviewer gate 보완 후 최종 `BUILD SUCCESSFUL in 2m 21s`였다.
- `P3-R5-GATE` lint/diff 검증(2026-07-28): `./gradlew ktlintCheck` 실행 결과 reviewer gate 보완 후 최종 `BUILD SUCCESSFUL in 44s`였고, `git diff --check`는 출력이 없었다.
- `P3-R5-GATE` 전체 회귀 생략(2026-07-28): 변경 범위가 Phase 2 문서와 Phase 3 content v2 facade/test에 한정되고 content/common 회귀가 실제 영향 범위를 포함하므로 전체 `./gradlew test`는 실행하지 않았다.
- `P4-T6` focused 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.AiCharacterAdminSeriesContractTest --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 41s`였고, `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --rerun-tasks`는 순차 재실행에서 `BUILD SUCCESSFUL in 3m 3s`였다. 병렬 실행 중 authorization run 1회는 unrelated `DefaultHomeRecommendationQueryRepository` QueryDSL 참조 compile 오류로 실패했으나 같은 명령 순차 재실행으로 영향 범위를 확인했다.
- `P4-T6` series 회귀와 lint/diff 검증(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 23s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 25s`였고, `git diff --check`는 출력이 없었다.
- `P4-GATE` fresh 검증(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*' --rerun-tasks`는 `BUILD SUCCESSFUL in 3m 21s`, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 24s`였고, `git diff --check`는 출력이 없었다.
- `P5-T1` community baseline 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.LegacyCommunityPostCharacterizationTest --rerun-tasks`와 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*' --rerun-tasks`는 각각 `BUILD SUCCESSFUL in 2m 22s`였다. `./gradlew ktlintCheck --rerun-tasks`는 unused import 2건 정리 후 `BUILD SUCCESSFUL in 21s`였고, `git diff --check`는 출력이 없었다.
- Phase 2·3 6차 리뷰 fresh focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 16s`였다. 다섯 XML 합계 216건의 failure/error/skipped는 모두 0이었다.
- Phase 2·3 6차 리뷰 lint/diff 검증(2026-07-28): `./gradlew ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 17s`였고, staged/unstaged `git diff --check`는 출력이 없었다.
- Phase 2·3 6차 리뷰 전체 회귀 생략(2026-07-28): production code를 변경하지 않은 read-only review와 문서 후속 Task 등록이며, 5차 보완의 핵심 actual endpoint 216건과 lint로 직접 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다. 실제 `P3-R7` production 수정 후 content/common 영향 범위 회귀를 실행한다.
- Phase 2·3 5차 Gate focused 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 52s`였다.
- Phase 2·3 5차 Gate diff 검증(2026-07-28): `git diff --check`는 출력이 없었다.
- `P3-R5` RED(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest` 실행 결과 14건 중 `coverImage`·`audioFile` 누락 KO/EN/JA 6건이 실패했다. 예를 들어 audio 누락 KO는 기대 `잘못된 요청입니다.` 대신 legacy `콘텐츠를 선택해 주세요.`를 반환해 nullable binding이 MVC 경계를 우회함을 확인했다.
- `P3-R5` focused 검증(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`였다. 세 필수 part 누락 KO/EN/JA는 400 generic envelope, exact `MissingServletRequestPartException`, DB/S3/event 0회를 실제 endpoint에서 확인했다.
- `P3-R5` content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 46s`였다.
- `P3-R5` lint(2026-07-27): `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`이었다.
- `P3-R5` 최종 fresh 검증(2026-07-27): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 모든 Gradle task를 재실행해 `BUILD SUCCESSFUL in 3m 33s`였고, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 25s`였다.
- Phase 2·3 5차 리뷰 fresh focused 검증(2026-07-27): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 7m 50s`였다. 세 XML 합계 79건의 failure/error/skipped는 모두 0이었다.
- Phase 2·3 5차 리뷰 lint/diff 검증(2026-07-27): `./gradlew ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 28s`였고, `git diff --check`는 출력이 없었다.
- Phase 2·3 5차 리뷰 전체 회귀 생략(2026-07-27): production code를 수정하지 않은 review/계획 문서 Task 등록이며 변경된 세 핵심 test를 fresh 실행해 finding을 판정했으므로 전체 `./gradlew test`는 실행하지 않았다. 실제 수정 Goal Gate에서 character/content/common 영향 범위 회귀를 각각 실행한다.
- Phase 2·3 5차 리뷰 문서 명령 유효성(2026-07-27): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 1s`였다.
- Phase 2·3 4차 보완 focused 검증(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`였다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 1m 8s`였다.
- Phase 2·3 4차 보완 content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 20s`였다.
- Phase 2·3 4차 보완 lint(2026-07-27): `./gradlew ktlintCheck`는 신규 test import ordering/unused import 3건 실패 후 import만 정리해 재실행했고, `BUILD SUCCESSFUL in 17s`였다.
- Phase 2·3 4차 리뷰 fresh targeted 검증(2026-07-27): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 9m 44s`였다. 관련 XML 14개 합계 228건의 failure/error/skipped는 모두 0이었다.
- Phase 2·3 4차 리뷰 lint(2026-07-27): `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL in 41s`였다.
- Phase 2·3 4차 리뷰 전체 회귀 생략(2026-07-27): production code를 변경하지 않은 리뷰·문서 Task 등록이며 character/content actual endpoint와 공통 authorization/error를 포함한 fresh 228건으로 직접 영향 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다.
- Phase 2·3 2차 리뷰 fresh targeted 검증(2026-07-27): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 9m 23s`였다. 생성된 관련 XML 14개 합계 199건의 failure/error/skipped는 모두 0이었다.
- Phase 2·3 2차 리뷰 lint(2026-07-27): `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL in 27s`였다.
- Phase 2·3 2차 리뷰 전체 회귀 생략(2026-07-27): production code를 수정하지 않은 read-only review와 문서 후속 Task 등록이며, character/content actual endpoint와 공통 authorization/error를 포함한 fresh 199건으로 직접 영향 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다.
- Phase 3 후속 보완 content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 41s`였다.
- Phase 3 후속 보완 lint(2026-07-27): `./gradlew ktlintCheck`는 import ordering 1건 실패 후 `AiCharacterAdminAudioContentUpdateTest.kt` import 순서만 정리해 재실행했고, `BUILD SUCCESSFUL in 16s`였다.
- Phase 2·3 코드 리뷰 Task 보완(2026-07-27): 확정 finding 8개를 `REV-001`~`REV-008`로 등록하고 기존 미실행
`P2-T3`~`P2-T6`, `P3-T3`~`P3-T7`에 소유권, 정확한 RED/RED 확인/GREEN/GREEN 확인/REFACTOR, 파일 경로,
focused 명령과 Gate 완료 증거를 보강했다. 중복 Goal은 만들지 않고 각 Phase 안에서 직렬 실행하도록 의존성 표를 갱신했다.
- Task TDD 샘플 동기화(2026-07-27): `docs/sample/sample-plan-task.md`에 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR`
작성 규칙과 read-only Task의 `TDD 예외 사유`·`대체 검증 방법` 형식을 추가하고 기존 샘플 Task 체크박스를 같은 형식으로 통일했다.
- 문서 보완 검증(2026-07-27): `REV-001`~`REV-008` 추적 횟수, Phase 2·3 Task/Gate·직렬 Goal 순서, 중복 Goal ID와
미교체 placeholder를 `rg`로 확인했고 `git diff --check`, `git diff --cached --check`가 통과했다.
`./gradlew tasks --all`은 exit code 0, `BUILD SUCCESSFUL in 660ms`를 확인했다. 문서 전용 변경이며 사용자가 명시적으로
제외했으므로 전체 `./gradlew test`는 실행하지 않았다.
- 리뷰 문서 경로 보완(2026-07-27): 여러 리뷰를 범위별 파일로 누적할 수 있도록 공통 저장 경로를
`docs/[날짜]_구현할내용한글/reviews/`로 정하고, Phase 2·3 리뷰 산출물과 샘플·가이드 경로를 동기화했다.
- 전체 회귀 실행 정책 보완(2026-07-27): focused/영향 범위 회귀를 우선하고 공통 경계·여러 Phase 영향 또는 targeted 결과만으로
영향 범위를 판단할 수 없는 경우에만 전체 회귀를 실행하도록 공통 가이드·샘플·`P7-T1`을 동기화했다. 생략 시 근거와 대체
검증 명령을 기록하도록 했다.
- Goal 실행형 계획 보완(2026-07-27): `docs/sample/sample-plan-task.md`와 PRD Feature B~F를 대조해 Phase 2·3에는 기존 완료
이력을 보존한 review/세부 보완/Gate Goal을 추가하고, Phase 4~6은 독립 검토 가능한 기능 단위 Task와 Gate로 분할했다.
Phase 7에는 targeted·조건부 전체 회귀, 계약·변경 범위와 최종 판정 Goal을 분리했다.
- 샘플 참조 가이드 보완(2026-07-27): `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`에
`docs/sample/{sample-prd.md,sample-plan-task.md,sample-review.md}`의 용도, Goal 완료·Gate·리뷰 누적 규칙을 반영했다.
- 문서 검증(2026-07-27): Goal/Task/Gate heading과 ID, placeholder·구 샘플 경로, 중복 Goal ID를 `rg`로 확인했고 중복·미교체
placeholder·구 샘플 경로 참조가 없음을 확인했다. `git diff --check`가 통과했다.
- 명령 유효성 검증(2026-07-27): sandbox 실행은 Gradle wrapper lock 권한으로 실패해 승인 범위에서
`./gradlew tasks --all`을 재실행했고 `BUILD SUCCESSFUL`을 확인했다.
- 계획 작성 단계: 코드 변경 없음.
- 문서 규칙 확인: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md`, `docs/sample/sample-prd.md`를 확인하고 `docs/20260724_AI캐릭터_관리자_API/{prd.md,plan-task.md}` 형식으로 작성했다.
- 금지어 확인: 계획 문서 금지어 검색 명령 실행 결과 없음.
- Phase 항목 확인: `rg -n "^### Phase|#### 목표|#### 범위와 비범위|#### 선행 Phase|#### API endpoint|#### entity, repository, service 변경|#### DB migration|#### transaction과 concurrency|#### 보안 및 개인정보|#### acceptance criteria|#### targeted test|#### 전체 회귀 테스트 영향|#### rollback 전략|#### 권장 commit 경계|Final Integration" "docs/20260724_AI캐릭터_관리자_API/plan-task.md"`로 Phase 1~7 전체에 필수 항목이 있음을 확인했다.
- 명령 유효성 확인: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`.
- Markdown diagnostics: `.md` 확장자용 LSP 서버가 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가.
- Endpoint Contract 보강: frontend HTTP 호출 기준으로 query parameter, request body, multipart form fields, response `data` JSON 예시를 추가했다.
- Endpoint Contract 보강 검증: 계획 문서 금지어 검색 결과 없음. `.md` 확장자용 LSP 서버가 없어 diagnostics는 실행 불가.
- 문서 동기화 보강: source spec 기준 `characterId` 예외, 시리즈 콘텐츠 검색, endpoint별 ADMIN 권한 테스트, parity RED 범위, 의존 방향 검증, PRD 가드레일, pagination 경계값, RED/GREEN/REFACTOR task 규칙을 반영했다.
- 문서 동기화 검증: `Read`로 `prd.md`와 `plan-task.md`의 반영 라인을 확인했다. `git status --short` 결과 `docs/20260724_AI캐릭터_관리자_API/`는 현재 untracked 디렉터리로 표시된다. `./gradlew tasks --all`은 이번 세션에서 120초, 300초 제한 모두 초과해 종료 결과를 확인하지 못했다.
- 문서 동기화 보강(2차): 기존 legacy/creator-admin 구현을 먼저 통과하는 특성화 baseline과 신규 v2 RED를 분리했고, character/content/series/community/FanTalk 재사용·parity 경계에 반영했다. 시리즈 CRUD·연결·해제·조회·순서 behavior parity와 signed URL 만료 계산식·edge case 특성화를 PRD와 Task에 명시했다.
- 문서 동기화 재검증(4번 제외): source spec acceptance criteria 25개 추적 검사 25/25, actor/scope/non-goal/architecture 가드레일 11/11을 확인했고 `git diff --check` 결과 문제가 없었다. plan 상단의 실행 skill 지침은 사용자 요청에 따라 변경·판정 범위에서 제외했다.
- 기존 기록은 보존한다. 각 Task의 실행 명령과 결과 요약은 해당 Task 아래에 누적하고, phase/전체 회귀·전체 빌드·포맷·문서 범위 확인만 이 섹션에 누적한다.
- Phase 1 RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest` 실행 결과 `AiCharacterAdminTargetResolver`와 `ChatCharacterRepository.findByIdWithCreatorMember` 미구현으로 `compileTestKotlin` 실패를 확인했다.
- Phase 1 GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest` 실행 결과 `BUILD SUCCESSFUL`.
- Phase 1 ADMIN 권한 RED/GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`에서 sample route 권한 실패를 확인한 뒤 `/api/v2/admin/ai-characters/**` ADMIN rule을 적용해 `BUILD SUCCESSFUL`을 확인했다.
- Phase 1 targeted 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` 실행 결과 `BUILD SUCCESSFUL`.
- Phase 1 lint 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`.
- Phase 1 LSP diagnostics: 현재 도구 목록에 LSP diagnostics tool이 노출되어 있지 않아 실행하지 못했고, 대신 Kotlin compile/test와 `ktlintCheck`로 대체 검증했다.
- Phase 1 reviewer gate: 1차 리뷰에서 invalid target 4xx 미충족, production SecurityConfig 미검증, cross-owner fixture 부족을 지적받아 수정했고, 재리뷰 결과 남은 blocking finding 없음으로 승인받았다.
- Phase 1 추가 리뷰 반영: mock 기반 resolver 테스트만으로 실제 repository/ownership 동작을 검증하지 못한다는 지적에 따라 `AiCharacterAdminTargetResolverIntegrationTest`를 추가했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest` 실행 결과 `BUILD SUCCESSFUL`.
- Phase 1 no-side-effect 근거 보정: 현재 검증은 resolver 자체가 DB row를 변경하지 않는다는 통합 테스트와 resolver가 S3/외부 API/event 의존성을 갖지 않는다는 구조에 한정한다. Phase 2~6 write vertical slice의 S3, 외부 API, 이벤트 no-side-effect는 각 slice 테스트에서 별도로 검증한다.
- Phase 1 후속 정책 반영 전 한계: 기존 ADMIN smoke는 JWT authority만 검증했고 현재 DB role/stale claim 및 신규 prefix 오류
envelope/i18n을 검증하지 않았다. 2026-07-24 후속 확정 정책은 Task 1.4~1.5에서 RED/GREEN으로 보완한다.
- Phase 1 후속 정책 문서 갱신: JWT ADMIN + 현재 DB ADMIN 이중 인가, stale claim 403, 신규 prefix의 비2xx
`ApiResponse.error`/KO·EN·JA 계약, legacy 오류 응답 불변 조건을 PRD와 계획에 반영하고 미완료 Task 1.4~1.5를 추가했다.
- Phase 1 후속 정책 문서 자체 검토: 금지어/미확정 문구 검색 결과 없음, 요구사항 추적 검색으로 stale claim·오류 envelope·i18n·
legacy fallback·Task 1.4~1.5 반영을 확인했고 `git diff --check` 결과 문제가 없었다.
- Phase 1 후속 정책 명령 유효성: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`.
- Task 1.4 RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest`
실행 결과 6개 중 stale ADMIN claim과 비`MemberAdapter` principal이 403 기대 대비 200으로 통과해 2개 실패함을 확인했다.
- Task 1.4 GREEN: 신규 prefix의 단일 access 식에서 JWT ADMIN, `MemberAdapter` principal, 현재 DB ADMIN을 AND로 검증한 뒤
동일 테스트 실행 결과 `BUILD SUCCESSFUL`.
- Task 1.4 reviewer gate: 별도 read-only 리뷰에서 스펙 준수와 코드 품질 모두 승인됐고 Critical/Important/Minor finding이
없음을 확인했다.
- Task 1.5 최초 RED: legacy 오류 baseline 2개는 통과했고 신규 401/403/400/500 계약은 15개 중 13개가 status/content type/
message 불일치로 실패함을 확인했다. target resolver의 기존 `ResponseStatusException`을 신규 API 예외로 교체하는 테스트도
새 예외 미구현 상태의 `compileTestKotlin` 실패로 RED를 확인했다.
- Task 1.5 확장 RED: handler 선택 전 오류와 filter 내부 장애까지 포함해 오류 계약 39개를 실행한 결과 신규 prefix의 404/405/
415 및 예상하지 못한 JWT filter 오류 500에 해당하는 12개만 실패했고 legacy baseline은 통과했다. 미등록 message key fallback은
44개 중 해당 KO/EN/JA 3개 실패로 별도 RED를 확인했다.
- Task 1.5 GREEN: URI prefix 기반 exception resolver, 낮은 우선순위 404 fallback mapping, prefix 전용 security handler,
known 인증 실패 401/그 외 filter 예외 500 분리, 미등록 message key의 localized unknown fallback을 구현했다.
`./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 44개 invocation
모두 통과해 `BUILD SUCCESSFUL`을 확인했다.
- Task 1.5 기존 slice 회귀: production `SecurityConfig`를 import하는 기존 WebMvc test 5개와 신규 authorization/error contract
test를 함께 실행한 결과 `BUILD SUCCESSFUL`을 확인했다. 신규 writer/security handler/exception resolver는 `SecurityConfig`의
명시적 bean으로 등록해 slice와 실제 application 구성을 동일하게 유지했다.
- Task 1.5 reviewer gate: 최초 read-only 리뷰의 405/415 handler-less 경로, 비인증 filter 예외의 401 오분류, 기존 WebMvc slice
빈 누락, 미매핑 404 지적을 모두 보완했다. 재리뷰 결과 Critical/Important/Minor finding 없이 승인됐다.
- Phase 1 후속 정책 최종 targeted/legacy 회귀: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
--tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 `BUILD SUCCESSFUL`(28초)을 확인했다.
- Phase 1 후속 정책 전체 회귀: 최신 작업 트리에서 `./gradlew test` 실행 결과 `BUILD SUCCESSFUL`(4분 22초)을 확인했다.
- Phase 1 후속 정책 lint: 최신 작업 트리에서 `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`(17초)을 확인했다.
- Phase 1 후속 정책 diff 무결성: `git diff --check` 통과, conflict marker 없음, build 설정·dependency·DDL 변경 없음을 확인했다.
- Task 1.5 최종 리뷰 보완 RED/GREEN: JWT parse 사이 만료 `JwtException` 401과 인증 저장소 장애 500을 추가했을 때 50개 중
해당 6개 실패를 확인한 뒤 known credential failure만 401로 분류해 50개 모두 통과했다. 필수 request header 누락 400은
53개 중 해당 KO/EN/JA 3개 실패를 확인한 뒤 `ServletRequestBindingException`을 400으로 분류해 모두 통과했다.
- Task 1.5 CORS RED/GREEN: 허용된 관리자 Origin의 404/405/415와 실제 `authorization,content-type` header를 요청하는 미매핑
prefix preflight를 추가했을 때 57개 중 해당 4개 실패를 확인했다. fallback mapping에 기존 전역 설정과 동일한 CORS 설정을
적용한 뒤 57개 모두 통과했다.
- Phase 1 no-side-effect 검증 강화: `AiCharacterAdminTargetResolverIntegrationTest`에서 Hibernate statistics를 초기화한 뒤
invalid target resolver 호출과 flush 후 entity insert/update/delete가 각각 0건임을 직접 검증했다. resolver 단위·통합 및 오류
계약 테스트를 함께 실행한 결과 `BUILD SUCCESSFUL`(27초)을 확인했다.
- Phase 1 최종 reviewer gate: 인증 예외 분류, request binding, fallback CORS, DB no-write, 문서/rollback을 독립 read-only로
재검토한 결과 Critical/Important/Minor finding이 없음을 확인했다.
- Phase 1 최신 targeted/legacy 회귀: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
--tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 `BUILD SUCCESSFUL`(28초)을 확인했다.
- Phase 1 최신 전체 회귀: 최종 코드 작업 트리에서 `./gradlew test` 실행 결과 `BUILD SUCCESSFUL`(4분 17초)을 확인했다.
- Phase 1 최신 lint: import 순서 1건을 수정한 뒤 `./gradlew ktlintCheck` 재실행 결과 `BUILD SUCCESSFUL`(10초)을 확인했다.
- Phase 1 최신 diff 무결성: `git diff --check`와 `git diff --cached --check`가 모두 통과했고 conflict marker가 없으며 build
설정, dependency, DDL 변경이 없음을 확인했다.
- Phase 1 코드 리뷰 차단 이슈 보완: 리뷰에서 지적된 staged/untracked 누락을 재확인한 결과 최신 작업 트리는 Phase 1 신규 테스트
2개(`AiCharacterAdminAccessDeniedErrorContractTest`, `AiCharacterAdminLoginJwtIntegrationTest`)와 `AccessDeniedException` 403 수정이
모두 변경 세트에 포함되어 있음을 확인했다. `TokenProvider` subject parsing 보정과 관련 테스트, rollback 범위를 Task 1.4/1.5와
Phase 1 rollback 전략에 반영했다.
- Phase 1 코드 리뷰 차단 이슈 재검증: `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
--tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest'` 단독 실행 결과 `BUILD SUCCESSFUL`을 확인했다. 이전 QA의 Gradle
`TestOutputStore` EOF는 동시/강제 실행 환경에서 발생한 결과 저장소 문제로 보며, 현재 단독 fresh rerun에서는 재현되지 않았다.
- Phase 1 코드 리뷰 차단 이슈 최종 검증: 문서 보정 후 `git status --short --untracked-files=all`에서 untracked 파일이 없고
`git diff --name-only` 결과가 비어 있음을 확인했다. `git diff --cached --check`, `git diff --check`,
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest'`,
`./gradlew test --tests 'kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest' --tests 'kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest'`,
`./gradlew ktlintCheck` 실행 결과 모두 `BUILD SUCCESSFUL`을 확인했다.
- Task 1.6 CORS 계약 재확인: 현재 `WebConfig` 기준 전용 Origin
`http://localhost:8888`, `https://test-character-admin.sodalive.net`, `https://character-admin.sodalive.net` 세 개를
PRD/plan에 명시했다. preflight 테스트는 세 Origin 모두 허용하고 기존 범용 관리자/creator Origin 네 개를
거부하는지 고정했다.
- Task 1.6 JWT RED/GREEN: `TokenProviderTest` 9개 중 추가한 누락·빈 값·빈 분할 항목·비문자열 `auth` claim
7개 invocation이 실패하는 RED를 확인했다. claim을 authority로 변환하기 전 검증해
`common.error.bad_credentials`로 변환한 뒤 9개 모두 통과했다.
- Task 1.6 테스트 fixture 격리: 전용 controller 6개를 각 테스트의 nested `@TestComponent`로 이동하고
`@Import`로만 등록했다. 초기 nested 이동 후 명시 등록이 누락된 WebMvc 요청 37개가 404로 실패한 것을
확인한 뒤 보정했고, 독립 재리뷰의 component scan 지적을 `@TestComponent`로 해소했다.
- Task 1.6 최종 targeted/레거시 회귀: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` 실행 결과 103개 모두 통과했다. 기존
`AdminAgentReadControllerSecurityTest`, `AdminContentControllerSecurityTest` 보안 회귀도 `BUILD SUCCESSFUL`을 확인했다.
- Task 1.6 최종 전체 회귀/lint: 최종 소스 상태에서 `./gradlew test`는 `BUILD SUCCESSFUL`(4분 55초),
`./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(19초)를 확인했다.
- Task 1.6 최종 reviewer/diff gate: 독립 read-only 재리뷰 결과 CORS, malformed `auth`, fixture scan 격리에
잔여 finding이 없음을 확인했다. `git diff --check`, `git diff --cached --check`가 통과했고 conflict marker와
untracked 파일이 없음을 확인했다.
- Task 1.6 2차 리뷰 CORS RED/GREEN: 실제 mapped endpoint, 공유 `/admin/member/login`, `/member/logout`의 캐릭터 관리자
Origin 요청/preflight를 추가했을 때 `AiCharacterAdminLoginJwtIntegrationTest` 6개 중 3개 실패를 확인했다. 두 공유 인증
exact path에만 기존 전역 Origin과 캐릭터 관리자 Origin 합집합을 적용한 뒤, 미등록 Origin 거부까지 포함한 7개가 모두
통과했다. 신규 prefix의 기존 범용 관리자 Origin 거부와 실제 로그인·로그아웃도 함께 검증했다.
- Task 1.6 2차 리뷰 HTTP 오류 RED/GREEN: 406, 405 `Allow`, 415 `Accept`/`Accept-Patch`,
`MissingPathVariableException` 500 계약을 추가해 신규 13개 실패를 확인했다. `AiCharacterAdminExceptionHandler`에 Spring 기본
HTTP 의미를 보존하는 최소 분기와 header 처리를 추가한 뒤 `AiCharacterAdminErrorContractTest` 80개가 모두 통과했다.
- Task 1.6 2차 리뷰 최종 targeted/레거시 회귀: `TokenProviderTest`와 `aicharacter.*` 114개, 기존
`AdminAgentReadControllerSecurityTest`와 `AdminContentControllerSecurityTest` 11개를 함께 실행해 총 125개 모두 통과했고
`BUILD SUCCESSFUL`(42초)을 확인했다.
- Task 1.6 2차 리뷰 전체 회귀/lint: 최종 소스 상태에서 `./gradlew test`는 `BUILD SUCCESSFUL`(5분 4초),
`./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(20초)을 확인했다.
- Task 1.6 2차 리뷰 최종 gate: 공유 인증 exact path가 전역 fallback보다 먼저 적용되고 신규 prefix/fallback은 전용 Origin을
유지하는지 독립 read-only로 재검토했다. `git diff --check`, `git diff --cached --check`가 통과했고 conflict marker와 untracked
파일이 없음을 확인했다.
- Task 1.6 후속 리뷰 mapped write preflight 보완: 기존 신규 prefix write preflight 검증이 fallback
`/api/v2/admin/ai-characters/unmapped-path`만 타는 한계를 확인했다. 테스트 controller에 실제 mapped
`/api/v2/admin/ai-characters/error-contract/write-preflight`의 `POST`/`PUT`/`PATCH`/`DELETE` 매핑을 추가하고,
해당 경로 preflight에서 `Access-Control-Allow-Origin`과 `Access-Control-Allow-Methods`를 함께 검증하도록 보완했다.
- Task 1.6 후속 리뷰 fallback write preflight 보완: actual mapping과 fallback이 서로 다른 CORS 설정을 사용하므로,
mapped endpoint 검증과 별도로 fallback `/api/v2/admin/ai-characters/unmapped-path`에서도 `POST`/`PUT`/`PATCH`/`DELETE`
preflight의 `Access-Control-Allow-Origin`과 `Access-Control-Allow-Methods`를 검증하도록 보완했다.
- Task 1.6 후속 리뷰 추가 계약 보완: wrong-role `USER` + `AI_CHARACTER` target은 resolver가 400으로 거부하고
DB insert/update/delete 없이 기존 role/memberKind를 유지하는지 고정했다. 캐릭터 관리자 Origin이 공유 인증 외
legacy/public 경로로 확산되지 않는지 확인했고, 406 Not Acceptable 응답도 허용 Origin에서는 localized `ApiResponse`와
`Access-Control-Allow-Origin`을 함께 반환하는지 검증했다.
- Task 1.6 후속 리뷰 계약 테스트 현황: mapped write preflight 보완 후 `AiCharacterAdminErrorContractTest` 85개,
`AiCharacterAdminLoginJwtIntegrationTest` 8개, `AiCharacterAdminTargetResolverIntegrationTest` 4개 기준으로
후속 리뷰 항목을 회귀했다.
- Task 1.6 후속 리뷰 최종 targeted/레거시 회귀: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests
kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 targeted+legacy 132개가 모두 통과했고
`BUILD SUCCESSFUL`(40초)을 확인했다.
- Task 1.6 후속 리뷰 전체 회귀/lint: mapped write preflight 보완 후 소스 상태에서 `./gradlew test`는 전체 1,259개 기준
`BUILD SUCCESSFUL`(4분 53초), `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(37초)을 확인했다.
- Task 1.6 후속 리뷰 fallback 보완 후 계약 테스트 현황: fallback write preflight 보완 후 `AiCharacterAdminErrorContractTest` 89개,
`AiCharacterAdminLoginJwtIntegrationTest` 8개, `AiCharacterAdminTargetResolverIntegrationTest` 4개 기준으로 후속 리뷰 항목을 회귀했다.
- Task 1.6 후속 리뷰 fallback 보완 후 최종 targeted/레거시 회귀: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests
kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 targeted+legacy 136개가 모두 통과했고
`BUILD SUCCESSFUL`(1분 38초)을 확인했다.
- Task 1.6 후속 리뷰 fallback 보완 후 전체 회귀/lint: fallback write preflight 보완 후 최종 소스 상태에서 `./gradlew test`는
전체 1,263개 기준 `BUILD SUCCESSFUL`(5분 58초), `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL`(35초)을 확인했다.
- Task 1.6 추가 코드 리뷰 보완: 실제 logout 후 동일 JWT로 신규 prefix 보호 경로를 호출하면 localized 401을 반환하는지
`AiCharacterAdminLoginJwtIntegrationTest`에 통합 테스트로 고정했다. 공유 `/admin/member/login`, `/member/logout` CORS preflight는
`WebConfig`의 기존 전역 Origin과 캐릭터 관리자 Origin 합집합 전체를 허용하는 parameterized test로 확장했다. Phase 1 targeted
Run 명령에는 `TokenProviderTest`를 포함하도록 보정했다.
- Task 1.6 추가 코드 리뷰 보완 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest`
실행 결과 `BUILD SUCCESSFUL`을 확인했다.
- Task 1.6 추가 코드 리뷰 보완 최종 회귀/lint: `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'` 실행 결과 `BUILD SUCCESSFUL`(44초), `./gradlew ktlintCheck` 실행 결과
`BUILD SUCCESSFUL`(25초)을 확인했다.
- Phase 1 최신 canonical fresh targeted/legacy 회귀: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 145/145, failure/error 0,
`BUILD SUCCESSFUL`(3분 12초)을 확인했다.
- Phase 1 최신 전체 fresh 회귀: `./gradlew test --rerun-tasks` 실행 결과 1,272/1,272, failure/error 0,
`BUILD SUCCESSFUL`(6분 49초)을 확인했다.
- Phase 1 최신 lint: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`(17초)을 확인했다.
- Phase 1 최신 명령 유효성: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`이며 `test`, `ktlintCheck` task가 존재함을
확인했다.
- Phase 1 최종 보강 후 canonical fresh targeted/legacy 회귀: `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests
kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest` 실행 결과 9개 XML class, 154/154,
failure/error/skipped 0, `BUILD SUCCESSFUL`(5분)을 확인했다.
- Phase 1 최종 보강 후 전체 fresh 회귀: `./gradlew test --rerun-tasks` 실행 결과 243개 XML class, 1,281/1,281,
failure/error/skipped 0, `BUILD SUCCESSFUL`(10분 2초)을 확인했다.
- Phase 1 최종 보강 후 lint: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고
`BUILD SUCCESSFUL`(29초)을 확인했다.
- `P5-T3` RED (2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.AiCharacterAdminCommunityPostCreateTest --rerun-tasks` 실행 결과 테스트 컴파일 후 6개 테스트가 모두 `POST` 미매핑으로 기대 400/200 대비 실제 405를 반환해 실패했다. 테스트 setup 오류가 아닌 생성 endpoint 미구현을 확인했다.
- `P5-T3` GREEN (2026-07-28): 동일 focused 명령을 재실행해 정상 무료 생성의 target owner/data null, image/audio legacy media upload, 유료·오디오 이미지 누락 legacy 오류, inactive target 선검증, request part 누락 envelope을 포함한 6개 테스트가 `BUILD SUCCESSFUL in 4m 7s`로 통과했다. 리뷰 보완으로 private publisher reflection과 after-commit mock 검증을 제거하고 실패 케이스를 요청 전후 DB count 불변으로 검증하도록 낮춘 뒤 focused 명령을 다시 실행해 `BUILD SUCCESSFUL in 3m 53s`를 확인했다.
- `P5-T3` legacy/community 회귀 (2026-07-28): 병렬 실행 중 legacy 단독 명령 1회가 QueryDSL Q-class 미해결 compile 오류로 실패했으나, 같은 시점의 community package 회귀는 `BUILD SUCCESSFUL in 5m 53s`였다. 이후 legacy 단독 명령을 순차 재실행해 `BUILD SUCCESSFUL in 5m 5s`를 확인했다.
- `P5-T3` lint/diff 검증 (2026-07-28): `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 46s`였고, `git diff --check`는 출력이 없었다. 구현 범위는 community controller/facade의 생성 endpoint와 생성 통합 테스트이며 P5-T4/P5-T5 endpoint는 추가하지 않았다.
### `P6-T1` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: `LegacyFanTalkReplyCharacterizationTest`로 legacy `ExplorerService.writeCheers`의 root parent 연결,
writer/creator 저장, response mapper, blank 언어 감지 event, missing parent root 전환, nested/inactive parent 허용,
중복 creator 답변과 creator/blocked 오류를 고정했다.
- 왜: Phase 6 v2가 legacy 저장·응답·이벤트 의미는 재사용하되 root/active/owner 검증 없는 legacy 허용 범위는 복제하지 않기 때문이다.
- 어떻게: production 변경 없이 focused test를 먼저 실행하고, import 정렬 1건을 수정한 뒤 focused test와 `ktlintCheck`를 재실행했다.
- 결과: 첫 focused 실행은 `BUILD SUCCESSFUL in 9s`, import 정렬 후 focused 재실행은 `BUILD SUCCESSFUL in 28s`,
`ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였다. `git diff --check`는 출력이 없었다. 전체 `./gradlew test`는 production
변경이 없고 focused test가 직접 legacy service 경계를 실행하므로 생략했다.
- 남은 항목: `P6-T2` FanTalk 관리자 목록 조회 구현.
### `P6-T5` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 실제 FanTalk list/reply endpoint의 JWT 비ADMIN과 stale ADMIN claim 거부 행렬, 빈/공백 content와
malformed/missing body·content binding의 KO/EN/JA 400 `ApiResponse.error` envelope를 추가했다.
- 왜: 공통 prefix 인가만으로는 실제 FanTalk endpoint의 이중 ADMIN 증거가 없었고 빈 content가 저장되는 결함이 있었기 때문이다.
- 어떻게: contract RED에서 빈/공백 3개 locale assertion이 200으로 실패함을 확인한 뒤 `createReply` 시작부에
`request.content.isBlank()` 공통 guard만 추가했다. existing `AiCharacterAdminFanTalkReplyCreateTest`의 target creator writer/creator
assertion을 재사용해 admin/principal impersonation 없음도 확인했다.
- 결과: `AiCharacterAdminFanTalkReplyContractTest`는 `BUILD SUCCESSFUL in 53s`,
`AiCharacterAdminAuthorizationTest`는 `BUILD SUCCESSFUL in 46s`, FanTalk package는 `BUILD SUCCESSFUL in 50s`,
`ktlintCheck`는 unused import 1건 제거 뒤 `BUILD SUCCESSFUL in 31s`였다.
- 남은 항목: `P6-GATE`.
### `P6-GATE` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: Phase 6 FanTalk 목록, root reply 저장, target/root/ownership 거부와 보안·오류 회귀를 최종 판정했다.
- 왜: Phase 7로 진행하기 전 P6-T1~P6-T5의 endpoint 계약과 package 회귀 성공 증거를 확정해야 하기 때문이다.
- 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와
`./gradlew ktlintCheck`를 실행했다.
- 결과: FanTalk package는 `BUILD SUCCESSFUL in 50s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 31s`였다. 전체
`./gradlew test`는 Phase 7 범위이므로 실행하지 않았다.
- 문서/변경 범위 검증: `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 1s`였고 `test`, `ktlintCheck`, `tasks` task가
존재했다. `git diff --check`는 출력 없이 통과했다.
- 남은 항목: `P7-T1` 대기.
### `P7-T1` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: Phase 1~6 targeted test와 전체 회귀 필요성을 판정했다.
- 왜: 최종 통합 Gate 전에 신규 AI 캐릭터 관리자 API 전체와 공통 JWT/인가 경계의 최신 회귀 결과가 필요하기 때문이다.
- 어떻게: 계획의 targeted 명령인 `./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests
'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'`를 실행했다.
- 결과: targeted test는 `BUILD SUCCESSFUL in 3m 50s`였고, 10개 task 중 1 executed, 9 up-to-date였다. 직전 fresh 검증으로
FanTalk package는 `BUILD SUCCESSFUL in 27s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 2s`, `git diff --check`는 출력 없이
통과했다.
- 전체 회귀 판정: 이번 Goal은 production 코드를 추가 변경하지 않았고 targeted 범위가 `TokenProviderTest`, 신규 prefix 전체,
`AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, Phase 1~6 신규 slice 회귀를 포함한다. 공통 경계나
legacy/public runtime 변경을 새로 만들지 않았으므로 전체 `./gradlew test`는 생략한다.
- 남은 항목: `P7-T2` API contract와 변경 범위 점검.
### `P7-T2` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: API contract, 변경 범위, dependency/DDL, 신규 v2 import 방향, source spec acceptance criteria를 read-only로 점검했다.
- 왜: 최종 Gate 전에 신규 기능 추가 없이 계획·PRD·OpenAPI·diff가 같은 상태를 가리키는지 확인하기 위해서다.
- 어떻게: `git diff --name-only`, `git diff --check`, `build.gradle.kts` 확인, DDL/migration 경로 검색, 신규
`v2/api/admin/aicharacter` import 검색, `api-contract.openapi.json` operationId 검색, PRD §11 acceptance criteria 대조,
`./gradlew ktlintCheck`, `./gradlew tasks --all`을 실행했다.
- 결과: `git diff --check`는 출력 없이 통과했다. `build.gradle.kts` diff는 없고, 변경 파일 중 `.sql`/migration/DDL 경로는 0건이다.
`api-contract.openapi.json`에는 23개 operationId가 있으며 Phase 2~6 구현 범위와 일치한다. 신규 package import 검색에서 기존
controller 역참조는 없었다. FanTalk facade의 공개 v2 `CreatorChannelFanTalk*Response` import는 계획과 PRD가 명시한 FanTalk 목록
응답 형태 재사용 예외라 유지한다.
- Acceptance 대조: PRD §11의 ADMIN 이중 인가, 401/403, API 오류/i18n, 405/406/415/500, CORS/firewall, full-context security,
target resolver, binding exception, no-side-effect, cross-owner, character/content/series/community/FanTalk parity, legacy/public
회귀, 신규 dependency/DDL 없음은 Phase 1~6 Gate와 `P7-T1` targeted 결과로 모두 추적됐다.
- 검증: `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 1s`, `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 11s`였고 `test`,
`ktlintCheck`, `tasks` task가 존재했다.
- 남은 항목: `P7-GATE` 최종 완료 판정.
### `P7-GATE` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: Phase 1~7 완료 증거, review finding 종결 상태, 최신 targeted/lint/tasks/diff 결과를 대조해 최종 완료로 판정했다.
- 왜: 신규 AI 캐릭터 관리자 API 구현을 더 진행하지 않고 인수 가능한 상태인지 확인하기 위해서다.
- 어떻게: `REV-001`~`REV-020` 종합 표가 모두 `처리 완료`인지 확인하고, 독립 Oracle read-only 리뷰를 받아 Gate 차단 finding이
없음을 확인했다. 이어 `git status --short --untracked-files=all`, `git diff --check`, `./gradlew ktlintCheck`를 fresh 실행했다.
- 결과: Oracle 리뷰는 PASS였고 Blocker/Important finding은 없었다. `git status --short --untracked-files=all`은 Phase 2~6의
의도된 tracked/untracked 변경 범위를 보여줬으며 신규 dependency/DDL 경로는 없었다. `git diff --check`는 출력 없이 통과했고,
`./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 2s`였다.
- 최종 판정: `P7-T1` targeted test `BUILD SUCCESSFUL in 3m 50s`, `P7-T2` `ktlintCheck`/`tasks --all`/diff 점검, 전체 회귀
생략 근거, review finding 처리 완료 기록이 모두 충족되어 문서 상태를 `구현 완료`로 갱신했다.
- 남은 항목: 없음. 커밋은 수행하지 않았다.
### `P7-GATE` 후속 OpenAPI 불일치 수정 — 2026-07-28
- 상태: 완료
- 무엇을: `removeAiCharacterSeriesContent` 구현을 OpenAPI 원본과 같은
`DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}`로 정합화하고 request body DTO를 제거했다.
- 왜: OpenAPI는 path `contentId`와 body 없음이 기준인데 기존 구현은 `/contents` + JSON body `{contentId}`를 받아 계약과 달랐기 때문이다.
- 어떻게: series content, contract, authorization 테스트를 body 없는 path `contentId` 호출로 먼저 바꿔 RED를 확인한 뒤 controller mapping과
DTO만 최소 변경했다. 신규 API라 dual route나 하위호환 shim은 추가하지 않았다.
- 결과: RED focused 실행은 기존 `/contents/{contentId}` 미매핑으로 5건이 실패했다. GREEN focused 실행 후 최신 series package 회귀
`./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`는 `BUILD SUCCESSFUL in 24s`,
`./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 2s`, `git diff --check`는 출력 없이 통과했다.