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

3115 lines
262 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 기준 | 이 문서의 `Endpoint Contract Summary` |
| 현재 Phase | Phase 3 6차 리뷰 완료 |
| 현재 활성 Goal | `P4-T1` 대기 |
## 현재 상태
| Phase | 상태 | 기존 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | 7/7 | 완료 | 없음 |
| 2 | 완료 | 11/11 | 완료 | 없음 |
| 3 | 완료 | 15/15 | 완료 | 없음 |
| 4 | 대기 | 0/6 | `P4-T1` | `P3-R5-GATE`, 사용자 진행 지시 |
| 5 | 대기 | 0/6 | `P5-T1` | `P4-GATE` |
| 6 | 대기 | 0/4 | `P6-T1` | `P5-GATE` |
| 7 | 대기 | 0/2 | `P7-T1` | Phase 1~6 Gate 완료 |
- 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}`
## 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는 `page` 기본값 0, `size` 기본값 20, 최소 20, 최대 50 보정을 적용하고 경계값 테스트를 둔다.
```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
}
```
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
{
"contentIds": [501, 502]
}
```
Response `data`: 시리즈 상세와 동일하다.
#### 시리즈 콘텐츠 연결 해제
`DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}`
Request body 없음.
Response `data`: 시리즈 상세와 동일하다.
#### 시리즈 순서 변경
`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 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
- `GET /api/v2/admin/ai-characters?search=&page=&size=` -> `AiCharacterAdminListResponse(totalCount, items, page, size, hasNext)`
- `GET /api/v2/admin/ai-characters/{characterId}` -> `AiCharacterAdminDetailResponse`
- `POST /api/v2/admin/ai-characters` multipart `image?`, `request: CreateAiCharacterAdminRequest` -> detail
- `PUT /api/v2/admin/ai-characters/{characterId}` multipart `image?`, `request: UpdateAiCharacterAdminRequest` -> detail
- `UpdateAiCharacterAdminRequest.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 3: 오디오 콘텐츠 관리와 signed URL vertical slice
#### 목표
선택한 AI 캐릭터 소유 오디오 콘텐츠 목록/검색/상세/생성/수정/soft delete와 관리자 재생용 signed URL을 제공한다.
#### 범위와 비범위
- 포함: 콘텐츠 owner 검증, 기존 파일 처리/가격/공개/예약/번역/알림 parity, `AudioContentCloudFront` 재사용, private path 비노출.
- 제외: 콘텐츠 구매/좋아요/댓글, 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 `data`:
```json
[
{
"themeId": 11,
"themeName": "ASMR",
"imageUrl": "https://cdn.example.com/audio-content-theme/asmr.png"
}
]
```
- 기존 크리에이터 관리자 콘텐츠 등록 화면의 콘텐츠 테마(카테고리) 조회와 같은 기능이다.
- 기존 내부/legacy DTO의 `id`, `theme`, `image` 필드명은 frontend 계약으로 노출하지 않고, 신규 v2 DTO의 `themeId`, `themeName`, `imageUrl`만 사용한다.
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents`
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
- `POST /api/v2/admin/ai-characters/{characterId}/audio-contents`
- multipart `coverImage`, `audioFile`, `request` JSON string part를 사용한다.
- `request` JSON은 `title`, `description`, `tags`, `price`, `purchaseOption`, `limited`, `isAdult`, `isActive`, `themeId`, `releaseDateUtc?`, `seriesIds`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 포함한다.
- legacy `CreateAudioContentRequest`의 `detail`은 v2 `description`, `releaseDate`는 UTC ISO-8601 `releaseDateUtc`로 받으며 facade에서 기존 pipeline 입력으로 변환한다.
- `PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
- `audioFile` 교체는 기존 creator/admin 수정 pipeline에 없는 동작이므로 Phase 3 범위에서는 제공하지 않는다. 오디오 파일 교체가 필요하면 별도 upload/processing parity 설계 후 추가한다.
- response item은 현 v2 목록 계약을 유지한다. response detail에는 기존 `GetAudioContentDetailResponse`의 필드 전체를 포함하고, `description`, `audioSignedUrl`, `releaseDateUtc`, `seriesIds`, `createdAtUtc`, `updatedAtUtc` 같은 v2 관리자 필드도 유지한다. 구매·좋아요·핀·추천·댓글 목록처럼 viewer 상태가 필요한 legacy 상세 필드는 관리자 상세에서 안전한 기본값을 반환한다.
#### 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 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
- `GET/POST/PUT /api/v2/admin/ai-characters/{characterId}/series...`
- `GET /series/{seriesId}/contents` query: `search?`, `page`, `size`
- `POST /series/{seriesId}/contents` request: `AddAiCharacterAdminSeriesContentsRequest(contentIds: List<Long>)`
- `DELETE /series/{seriesId}/contents/{contentId}`
- `PUT /series/orders` request: `UpdateAiCharacterAdminSeriesOrdersRequest(seriesIds: 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`
- [ ] **Task 4.1: 기존 series parity 특성화 baseline 고정**
**Goal 실행 `P4-T1`:** 기존 creator series의 CRUD·연결·검색·순서 동작을 신규 v2 구현의 비교 기준으로 고정한다.
- **시작 조건:** 최신 Phase 3 후속 Gate인 `P3-R5-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`
- [ ] 목록·상세·생성·수정·soft delete와 inactive 조회 baseline test를 작성한다.
- [ ] 콘텐츠 연결·해제·검색과 순서 변경의 결과·검증·side effect를 고정한다.
- [ ] Phase 4 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
- [ ] production code 변경 없이 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- [ ] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 4.2: 시리즈 목록·상세 조회 구현**
**Goal 실행 `P4-T2`:** target owner의 활성 시리즈 목록·상세를 pagination 계약으로 제공한다.
- **시작 조건:** `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`
- [ ] 미구현 목록·상세, inactive 제외, cross-owner 거부와 pagination 경계 실패 test를 작성한다.
- [ ] owner-scoped 목록·상세 최소 구현으로 focused test를 통과시킨다.
- [ ] `page` 기본 0, `size` 기본·최소 20·최대 50과 legacy DTO 비노출을 검증한다.
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] 생성·수정·soft delete와 cross-owner mutation 실패 test를 작성한다.
- [ ] owner 검증 후 최소 CRUD 구현으로 test를 통과시킨다.
- [ ] `isActive=false`와 활성 조회 제외, invalid target의 DB/event no-side-effect를 검증한다.
- [ ] focused/legacy test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] 콘텐츠 조회·검색·연결·해제와 cross-owner ID 실패 test를 작성한다.
- [ ] 모든 series/content ID를 mutation 전에 검증하는 최소 구현을 통과시킨다.
- [ ] 일부 연결 성공이 남지 않는 원자성과 pagination 경계를 검증한다.
- [ ] focused/Phase 3 owner query 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] 정상 순서와 cross-owner ID-only 취약 경로를 재현하는 실패 test를 작성한다.
- [ ] 동일 owner 전체 검증 후 한 transaction에서 갱신하는 최소 구현을 통과시킨다.
- [ ] 검증 실패 시 update 0건과 동시 요청의 기존 last-transaction 정책을 확인한다.
- [ ] focused/legacy order test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다.
- [ ] target/series/content/pagination 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
- [ ] invalid ownership의 DB/event side effect 0건과 legacy creator series 계약을 검증한다.
- [ ] Phase 4 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
#### Phase 4 Gate
**Goal 실행 `P4-GATE`:** Phase 4 series 사용자 흐름과 ownership·회귀 품질을 최종 판정한다.
- [ ] **`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 5: 커뮤니티 게시글 관리 vertical slice
#### 목표
선택한 AI 캐릭터 소유 커뮤니티 게시글 등록, 수정, 고정/해제, soft delete와 관리자 조회를 제공한다.
#### 범위와 비범위
- 포함: owner-scoped community query/write, 최대 고정 3개, soft delete 시 fixed 상태 제거, 이미지/오디오/유료 게시글 검증, 기존 알림/최근 소식 side effect parity.
- 제외: 구매/좋아요/댓글 관리, public community 조회 정책 변경.
#### 선행 Phase 및 의존성
- Phase 1 target resolver.
- 기존 community write behavior 특성화 테스트.
#### API endpoint와 request/response contract
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts`
- `POST /api/v2/admin/ai-characters/{characterId}/community-posts`
- `PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`
- update request는 `isFixed`, `isActive`, 본문/이미지/오디오/가격 필드를 포함한다.
#### 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`
- [ ] **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`
- [ ] image/audio/paid post validation과 notification/recent-news side effect baseline을 작성한다.
- [ ] 최대 고정 3개와 fixed post soft delete clearing baseline을 작성한다.
- [ ] Phase 5 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
- [ ] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
- [ ] fixture/event spy만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] 미구현 목록, owner 격리, pagination 경계와 관리자 DTO 실패 test를 작성한다.
- [ ] 최소 owner-scoped query와 `page/size` 보정으로 focused test를 통과시킨다.
- [ ] 유료/media private 정보와 public viewer 상태를 부적절하게 노출하지 않는지 검증한다.
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] 정상/image/audio/paid validation과 invalid target 실패 test를 작성한다.
- [ ] 해석된 creatorMember를 writer/owner로 사용하는 최소 생성 구현을 통과시킨다.
- [ ] S3, notification, recent-news 호출 순서와 실패 시 부분 저장/no-side-effect를 검증한다.
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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/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`
- [ ] 수정·고정/해제·soft delete와 cross-owner 실패 test를 작성한다.
- [ ] owner 검증 후 최소 mutation 구현으로 test를 통과시킨다.
- [ ] soft delete가 한 transaction에서 `isActive=false`, `isFixed=false`, `fixedAt=null`을 적용하는지 검증한다.
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] 3개 허용·4번째 거부와 동시 고정 요청 실패 test를 작성한다.
- [ ] 기존 repository count/update 순서를 유지하는 최소 구현으로 test를 통과시킨다.
- [ ] invalid target/ownership 실패 시 DB/S3/event 0건을 검증한다.
- [ ] concurrency/focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **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`
- [ ] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다.
- [ ] target/post/media/fixed-count 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
- [ ] HUMAN/cross-character 게시글 mutation 거부와 legacy/public community 계약을 검증한다.
- [ ] Phase 5 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
#### Phase 5 Gate
**Goal 실행 `P5-GATE`:** Phase 5 community 사용자 흐름과 고정·side-effect·회귀 품질을 최종 판정한다.
- [ ] **`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 6: FanTalk 답변 vertical slice
#### 목표
선택한 AI 캐릭터가 자신의 활성 root FanTalk에만 creator reply를 작성하는 v2 관리자 API를 제공한다.
#### 범위와 비범위
- 포함: root FanTalk 존재/활성/owner 검증, creator reply 저장, 언어 감지와 기존 응답 의미 parity.
- 제외: FanTalk 원글 작성, nested reply, 구매/댓글형 기능, AI 캐릭터 일반 사용자 활동.
#### 선행 Phase 및 의존성
- Phase 1 target resolver.
- 기존 FanTalk 저장 엔티티와 응답 DTO 의미 특성화.
#### API endpoint와 request/response contract
- `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`
- Request: `CreateAiCharacterAdminFanTalkReplyRequest(content: String)`
- Response: `AiCharacterAdminFanTalkReplyResponse(fanTalkId, replyId, creatorMemberId, content, createdAtUtc)`
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: `CreatorCheers` 또는 FanTalk repository에 root/active/creator owner 조회 adapter 추가 가능.
- Service: 신규 FanTalk reply application service 구현. 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에만 답변할 수 있다.
- cross-character, nested parent, inactive/missing FanTalk는 4xx이며 reply 저장과 이벤트 발행이 없다.
- 저장된 답변의 writer/creator는 해석된 creatorMember와 일관된다.
#### targeted test
- Characterization: `LegacyFanTalkReplyCharacterizationTest`.
- V2 RED/GREEN: `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 reply slice`
- [ ] **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`
- [ ] valid root reply의 언어 감지, response 의미와 writer/creator 저장 baseline을 작성한다.
- [ ] root/nested/active 판별과 기존 중복 답변 정책을 관찰해 기록한다.
- [ ] Phase 6 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
- [ ] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
- [ ] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 6.2: FanTalk root reply 저장 구현**
**Goal 실행 `P6-T2`:** 선택한 AI 캐릭터의 활성 root FanTalk에 creator reply를 저장하고 전용 응답을 반환한다.
- **시작 조건:** `P6-T1` 완료와 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`
- [ ] 미구현 정상 root reply, 언어 감지와 응답 DTO 실패 test를 작성한다.
- [ ] root 조회와 reply 저장을 같은 transaction에서 수행하는 최소 구현을 통과시킨다.
- [ ] writer/creator가 resolver의 creatorMember와 일치하고 principal impersonation이 없음을 검증한다.
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 6.3: FanTalk target·root·ownership 거부 구현**
**Goal 실행 `P6-T3`:** cross-character, nested, inactive, missing FanTalk를 저장·이벤트 없이 거부한다.
- **시작 조건:** `P6-T1`, `P6-T2` 완료.
- **완료 증거:** 네 거부 분기의 정확한 오류 계약, DB/event 0건과 Progress 기록.
- **범위 밖:** 새로운 중복 답변 차단 정책.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyFacade.kt`
- Modify: `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/AiCharacterAdminFanTalkReplyOwnershipTest.kt`
- [ ] cross-character/nested/inactive/missing 각각의 실패 test와 의도한 실패를 확인한다.
- [ ] target·root·active·owner를 저장 전에 검증하는 최소 구현을 통과시킨다.
- [ ] 각 실패의 정확한 status/message key/KO·EN·JA와 reply insert/event 0건을 검증한다.
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 6.4: Phase 6 보안·오류·회귀 검증**
**Goal 실행 `P6-T4`:** FanTalk reply endpoint의 ADMIN·오류 계약과 기존 FanTalk 회귀를 고정한다.
- **시작 조건:** `P6-T2`, `P6-T3` 완료.
- **완료 증거:** 권한 매트릭스, 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`
- [ ] endpoint의 ADMIN 이중 인가와 stale claim을 검증한다.
- [ ] 빈 content/binding/domain 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
- [ ] 기존 FanTalk 조회·작성 계약과 AI 로그인/token/impersonation 부재를 확인한다.
- [ ] Phase 6 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
#### Phase 6 Gate
**Goal 실행 `P6-GATE`:** Phase 6 FanTalk reply의 root·ownership·저장·회귀 품질을 최종 판정한다.
- [ ] **`P6-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P6-T1`~`P6-T4` 완료.
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와
`./gradlew ktlintCheck` 성공, Progress 기록.
- **범위 밖:** 실패와 무관한 신규 기능.
---
### 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
- Endpoint Contract Summary의 모든 endpoint가 구현되어야 한다.
- 모든 신규 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`
- [ ] **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` 결과를, 생략 시 근거와 대체
회귀 범위를 이 문서 하단 검증 기록에 남긴다.
- [ ] Phase 1~6 targeted test를 실행하고 각 결과를 기록한다.
- [ ] 공통 경계·여러 Phase 변경과 targeted 결과를 근거로 전체 회귀 필요성을 판정한다.
- [ ] 필요하면 `./gradlew test`의 exit code·실패 수를 기록하고, 불필요하면 생략 근거와 대체 회귀 범위를 기록한다.
- [ ] 실패가 있으면 소유 Phase에 별도 회귀 수정 Goal을 추가하고 `P7-T1`을 완료 처리하지 않는다.
- [ ] **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`
- [ ] `git diff --name-only`와 `git diff --check`로 변경 범위와 문서/코드 오류를 확인한다.
- [ ] `build.gradle.kts`와 migration/DDL 경로를 확인해 신규 dependency·DDL 0건을 기록한다.
- [ ] legacy/public controller·DTO 외부 계약 diff와 신규 application/domain의 역방향 import 0건을 확인한다.
- [ ] source spec acceptance criteria 25개를 Phase Goal/Gate 완료 증거에 대조한다.
- [ ] `./gradlew ktlintCheck`와 `./gradlew tasks --all` 결과를 기록한다.
#### Phase 7 Gate
**Goal 실행 `P7-GATE`:** 모든 Phase의 완료 증거와 최신 전체 검증을 대조해 AI 캐릭터 관리자 API의 최종 완료 여부를 판정한다.
- [ ] **`P7-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 문서 상태를 `구현 완료`로 갱신한다.
- **시작 조건:** `P7-T1`, `P7-T2` 완료.
- **완료 증거:** 미완료 Goal·미처리 review finding·보류 없는 차단 사항 0건, 아래 완료 조건과 최종 Progress 기록.
- **범위 밖:** Gate에서 직접 production code 수정, test 삭제·skip·완화.
- [ ] Phase 1~6의 Task/Gate 완료 증거와 하단 검증 기록이 일치한다.
- [ ] 모든 확정 review finding이 수정 완료 또는 근거 있는 제외로 종결됐다.
- [ ] 최신 targeted·ktlint·문서 명령이 성공했고, 전체 회귀는 필요성 판정에 따라 성공 결과 또는 생략 근거가 기록됐다.
- [ ] 남은 항목과 최종 상태를 Progress 및 최종 보고 형식으로 기록한다.
---
## 실행 순서와 의존성
| 순서 | 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 | `P4-T1`~`P4-T6` → `P4-GATE` | `P3-R5-GATE`, 사용자 진행 지시 | 아니요 | 실패 소유 Task로 되돌림 |
| 16 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 17 | `P6-T1`~`P6-T4` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 18 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 |
## 변경 금지·중단 규칙
- 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 실행 결과를 차수별로 누적한다.
### `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`이지만 사용자 진행 지시 전까지 시작하지 않는다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-07-27 | `DEC-GOAL-001` | 확정 | 기존 Phase 2·3 완료 Task는 이력으로 보존하고 심층 리뷰·세부 보완·Gate Goal을 추가한다. | 기존 검증 이후 후속 보완이 반복됐고 Phase 완료 Gate가 없었다. | `P2-R1`~`P3-GATE` |
| 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`, 하단 발견된 문제 표 |
## 발견된 문제
| 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을 단언했다. |
## 검증 기록
- 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`였다.
- 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`는 실행하지 않았다.
- 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초)을 확인했다.