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

2364 lines
163 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 2~3 재검토 및 보완 |
| 현재 활성 Goal | 없음 (`P2-R1`부터 시작) |
## 현재 상태
| Phase | 상태 | 기존 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | 7/7 | 완료 | 없음 |
| 2 | 진행 중 | 2/2 | `P2-R1` | `REV-001`~`REV-003`, `REV-007`, `REV-008` 보완 후 `P2-GATE` |
| 3 | 진행 중 | 2/2 | `P3-R1` | `REV-004`~`REV-008` 보완 후 `P3-GATE` |
| 4 | 대기 | 0/6 | `P4-T1` | `P3-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": "루나",
"description": "달빛을 좋아하는 AI 캐릭터",
"originalWorkId": 31,
"externalCharacterId": "ext-luna-001",
"isActive": true
}
}
```
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": "ext-luna-001",
"isActive": false
}
}
```
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`을 확인했다.
- [ ] **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`
- [ ] 목록·검색·상세·생성·수정·비활성화 endpoint와 DTO 필드를 PRD/계약에 1:1로 추적한다.
- [ ] 중복 이름, 외부 API, S3, 원작, 언어 이벤트, creatorMember 동기화와 실패 순서를 코드·test에 추적한다.
- [ ] ADMIN 이중 인가, pagination, multipart/binding, KO/EN/JA 오류, private 정보 비노출 계약을 확인한다.
- [ ] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`의 실제 결과를 기록한다.
- [ ] 후보를 확정·오탐·보류로 판정하고 확정 항목을 아래 세부 Goal에 연결하거나 새 회귀 수정 Goal을 계획에 먼저 추가한다.
- [ ] **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`
- [ ] **RED:** 목록 item의 exact JSON key를 고정하고 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`가 노출되는 현재 동작을 실패로 재현한다.
- [ ] **RED 확인:** `page=0`, `size` 기본 20·최소 20·최대 50, 검색·`hasNext`와 상세 target 불변식의 경계 test를 실행해 의도한 assertion 실패를 확인한다.
- [ ] **GREEN:** 목록 전용 DTO와 mapper를 최소 구현해 계약 field만 반환하고 credential·token·private path를 노출하지 않는다.
- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 exact 목록 field set과 pagination·target 계약이 모두 통과하는지 확인한다.
- [ ] **REFACTOR:** 상세 UTC field 계약을 유지하면서 목록/상세 DTO 의존 방향을 점검하고 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`
- [ ] **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`
- [ ] `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 invalid `characterType`의 canonical request 계약을 Endpoint Contract Summary·legacy 특성화 결과로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다.
- [ ] **RED:** 확정된 문서 JSON의 역직렬화·반영, 동시 중복 이름 생성, 외부 API 실패, S3 실패와 존재하지 않는 `originalWorkId` 실패를 각각 재현한다.
- [ ] **RED 확인:** 실패 지점별 DB row·creatorMember·원작 연결·S3 객체·외부 캐릭터·event 결과와 호출 순서를 단언해 현재 부분 저장 또는 고아 부작용을 확인한다.
- [ ] **GREEN:** 모든 DB 참조를 외부 부작용 전에 검증하고, 동시 중복 정책과 legacy parity에 맞는 최소 보상/정리 경계로 정상 생성과 실패 원자성을 통과시킨다.
- [ ] **GREEN 확인:** 같은 생성 focused/characterization test를 다시 실행해 정상 결과와 실패 지점별 잔존 상태가 확정 계약과 일치하는지 확인한다.
- [ ] **REFACTOR:** creatorMember 표시 정보와 언어 이벤트를 포함한 focused/legacy characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다.
- 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`
- [ ] **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`
- [ ] `externalCharacterId`, 일반 수정과 `isActive=false` 혼합 요청, invalid `characterType`의 canonical update 계약을 확정하고 충돌 시 Decision Log를 먼저 갱신한다.
- [ ] **RED:** 문서 PUT JSON의 field 반영, soft delete와 일반 수정 혼합, image 동시 요청, 외부 수정 성공 후 S3/DB 실패, `updatedAtUtc`의 flush 전 mapping을 각각 재현한다.
- [ ] **RED 확인:** soft delete 성공·실패에서 row·Member·콘텐츠 보존, 미참조 S3 객체 0건, 외부/DB 상태 일치와 응답 timestamp가 후속 GET과 같은지 확인한다.
- [ ] **GREEN:** 확정 계약에 맞춰 혼합 요청을 명시적으로 처리하고, 불필요한 upload를 차단하며 외부/S3/DB 보상 경계와 flush 후 response mapping을 최소 구현한다.
- [ ] **GREEN 확인:** 같은 수정 focused/characterization test를 다시 실행해 field 반영, 보상 결과, soft-delete 보존과 timestamp가 모두 통과하는지 확인한다.
- [ ] **REFACTOR:** creatorMember 표시 정보·번역 event와 legacy `characterType` 동작을 포함한 focused/characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다.
- 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`
- [ ] **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`
- [ ] **RED:** `/api/v2/admin/ai-characters/unmapped-path`가 detail `Long` binding에 잡혀 404 대신 400이 되는 KO/EN/JA·허용 Origin CORS 4건을 현재 Phase 1 오류 계약 test로 재현한다.
- [ ] **RED:** 목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim과 binding·multipart·domain/client/server 오류의 exact status/key/KO·EN·JA를 parameterized test로 고정한다.
- [ ] **RED 확인:** 오류 계약과 실제 endpoint matrix를 실행해 404 회귀 4건과 누락된 인가·i18n assertion이 의도대로 실패하는지 확인한다.
- [ ] **GREEN:** numeric `characterId`만 resource handler에 매핑되도록 최소 수정하고, Phase 2 오류 의미를 확정된 message key와 `ApiResponse.error`로 반환한다.
- [ ] **GREEN 확인:** 같은 오류·인가 focused test를 다시 실행해 실제 endpoint의 status/header/envelope와 KO/EN/JA가 모두 통과하는지 확인한다.
- [ ] **REFACTOR:** 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminCharacterServiceTest` 참조 및 과거 test 수 기록은 삭제하지 않고 정정 기록을 누적한다.
- [ ] 기존 admin/public character contract, 신규 DTO 의존 방향과 Phase 2 focused test·`ktlintCheck` 결과를 Progress에 기록한다.
- 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 회귀를 최종 판정한다.
- [ ] **`P2-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P2-R1`, `P2-T3`~`P2-T6` 완료 또는 근거 있는 `해당 없음` 판정.
- **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록.
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, Phase 3 기능 수정.
- [ ] `REV-001`~`REV-003`, `REV-007`, `REV-008`의 계약 결정·failure matrix·수정 test와 실제 결과가 각 소유 Goal의 Progress에 연결됐다.
- [ ] 캐릭터 목록 exact key, 문서 mutation JSON, 동시 중복 결과, 외부/S3/DB 보상, post-flush `updatedAtUtc`와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다.
- [ ] 완료 이력의 누락 test 파일·test 수·failure-order 증거는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다.
```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 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에서 미지원으로 명시했다.
- [ ] **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`
- [ ] 테마·목록·검색·상세·생성·수정·soft delete endpoint와 DTO 필드를 PRD/계약에 추적한다.
- [ ] signed URL TTL/path, private path 비노출, viewer 상태 기본값을 production·test에 추적한다.
- [ ] 생성/update pipeline, 파일, 가격, 공개·예약, 번역·알림, `seriesIds`, 날짜 변환과 실패 순서를 확인한다.
- [ ] owner 검증, no-side-effect, ADMIN 인가, 오류 i18n, pagination/multipart 계약을 확인한다.
- [ ] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` 결과와 후보 판정을 리뷰 문서에 기록한다.
- [ ] **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`
- [ ] **RED:** 테마 endpoint 부재 또는 계약 불일치와 legacy field 노출을 재현하는 가장 작은 실패 test를 작성한다.
- [ ] **RED 확인:** focused test를 실행해 의도한 route·field assertion 실패를 확인한다.
- [ ] **GREEN:** 활성 테마만 `themeId`, `themeName`, `imageUrl`로 반환하는 최소 구현을 작성한다.
- [ ] **GREEN 확인:** request body 없음, exact field set과 ADMIN 이중 인가를 포함한 focused test 성공을 확인한다.
- [ ] **REFACTOR:** v2 DTO 경계만 정리하고 테마 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest`
- [ ] **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`
- [ ] **RED:** `purchaseOption=RENT_ONLY`, entity `isOnlyRental=true`, 미래·과거 `releaseDate` 조합에서 legacy 상세와 다른 `isOnlyRental`, `purchaseOption`, `releaseDate`를 재현한다.
- [ ] **RED:** 응답의 creator·buyer·other content·comment·translation 중첩 타입이 legacy/public DTO package에 직접 의존하는 현재 경계를 검출하고 exact JSON key를 고정한다.
- [ ] **RED 확인:** query/legacy baseline test를 실행해 세 compatibility field와 금지 DTO 의존이 의도대로 실패하는지 확인한다.
- [ ] **GREEN:** legacy 파생 규칙과 현지화된 `releaseDate` 의미를 유지하고 UTC 원본은 `releaseDateUtc`에만 반환하며, 동일 JSON을 v2 전용 중첩 DTO로 최소 매핑한다.
- [ ] **GREEN 확인:** 같은 query/legacy baseline test를 다시 실행해 legacy compatibility field, `releaseDateUtc`와 exact JSON schema가 모두 통과하는지 확인한다.
- [ ] **REFACTOR:** owner·검색·status·pagination, 활성 owner-scoped `seriesIds`, viewer 기본값, signed URL TTL/path와 private 정보 비노출을 함께 회귀한다.
- [ ] 조회/signed URL focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- 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`
- [ ] **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`
- [ ] `tags` 필수 여부와 생성 `isActive=false`의 canonical 계약을 legacy pipeline·Endpoint Contract Summary로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다.
- [ ] **RED:** `coverImage`, `audioFile`, `request` 각 part 누락에서 Kotlin nullable 때문에 `MissingServletRequestPartException`이 발생하지 않는 현재 binding과 KO/EN/JA envelope 차이, facade·DB·S3·event 호출 0건 기대를 재현한다.
- [ ] **RED:** 생성 request 전체 field, `description/releaseDateUtc` 변환, `tags` 누락, `isActive=false`, target·theme·`seriesIds` 오류와 S3/processing/event 실패 순서를 각각 고정한다.
- [ ] **RED 확인:** create/error/legacy characterization test를 실행해 part별 exception·field 계약·failure order가 의도대로 실패하는지 확인한다.
- [ ] **GREEN:** 필수 file part를 non-null binding으로 만들고 확정된 field 계약, 외부 부작용 전 참조 검증과 legacy upload/processing parity를 최소 구현한다.
- [ ] **GREEN 확인:** 같은 test를 다시 실행해 part별 400/i18n, 정상 생성과 실패 후 DB/S3/event 결과가 모두 통과하는지 확인한다.
- [ ] **REFACTOR:** cover/audio upload, 가격·공개·예약·번역·알림 및 실패 후 DB/S3/event 결과를 characterization/focused test로 회귀하고 `ktlintCheck` 결과를 Progress에 기록한다.
- 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`
- [ ] **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`
- [ ] **RED:** 기존 `SeriesContent.orders`, row ID, `createdAt`이 있는 콘텐츠에 동일 `seriesIds`를 PUT했을 때 전부 삭제·재생성되는 현재 동작을 실패 test로 고정한다.
- [ ] **RED 확인:** 동일 ID, 추가 ID, 제거 ID를 각각 요청해 교집합 metadata 보존과 차집합만 insert/delete한다는 기대가 현재 실패하는지 확인한다.
- [ ] **GREEN:** 기존 연결과 요청 ID의 차집합만 변경하고 교집합 row의 ID·`orders`·`createdAt`을 보존하는 최소 구현을 작성한다.
- [ ] **GREEN 확인:** 같은 update test를 다시 실행해 동일 집합 no-op, 교집합 metadata 보존과 차집합 변경만 발생하는지 확인한다.
- [ ] **REFACTOR:** cover 유지/교체, 날짜, `audioFile` 미지원, `isActive=false`와 cross-owner/invalid series의 DB/S3/event no-side-effect를 회귀한다.
- [ ] 수정 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest`
- [ ] **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:**
- Modify: `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`
- 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`
- [ ] **RED:** 테마·목록·상세·생성·수정 endpoint 각각의 JWT role × DB role, stale ADMIN claim과 허용·거부 Origin을 parameterized test로 고정한다.
- [ ] **RED:** target/content/theme/series/date와 세 multipart part 누락의 exact status, exception type, message key와 KO/EN/JA envelope를 실제 endpoint에서 고정한다.
- [ ] **RED 확인:** 실제 endpoint matrix와 legacy characterization을 실행해 누락된 인가·i18n·failure-order assertion이 의도대로 실패하는지 확인한다.
- [ ] **GREEN:** 확정된 domain/client/server 오류만 최소 매핑하고 ownership 실패 시 DB insert/update/delete, S3, event 0건을 보장한다.
- [ ] **GREEN 확인:** 같은 endpoint/error/ownership test를 다시 실행해 status/header/envelope, KO/EN/JA와 no-side-effect가 모두 통과하는지 확인한다.
- [ ] **REFACTOR:** legacy characterization에 validation·파일·가격·공개/예약·번역/알림·failure order를 보강하고 signed URL edge case와 함께 실행한다.
- [ ] 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest` 참조와 과거 test 수는 삭제하지 않고 정정 기록을 누적한다.
- [ ] `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest`, `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentOwnershipTest`의 파일 존재와 각 소유 계약 통과를 확인한다.
- [ ] creator/admin/public content 회귀, 신규 DTO 의존 방향과 focused test·`ktlintCheck` 결과를 Progress에 기록한다.
- 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 회귀를 최종 판정한다.
- [ ] **`P3-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P3-R1`, `P3-T3`~`P3-T7` 완료 또는 근거 있는 `해당 없음` 판정.
- **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록.
- **범위 밖:** Gate 실패와 무관한 Phase 4 기능 구현.
- [ ] `REV-004`~`REV-008`의 response parity matrix, multipart exception, series metadata, DTO 경계와 test 증거가 각 소유 Goal의 Progress에 연결됐다.
- [ ] 동일 `seriesIds`의 row metadata 보존, legacy `releaseDate`·rental 파생값, 세 필수 part와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다.
- [ ] 완료 이력의 누락 test 파일·test 수·characterization 범위는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다.
```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 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 구현의 비교 기준으로 고정한다.
- **시작 조건:** `P3-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 | `P4-T1`~`P4-T6` → `P4-GATE` | `P3-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 8 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 9 | `P6-T1`~`P6-T4` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 10 | `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`에 등록한다.
## 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` |
## 발견된 문제
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|---|---|---|---|---|---|
| `REV-001` | Blocker | 확정 | 문자열 미매핑 경로가 `/{characterId}`에 매핑되어 Phase 1의 404 계약 대신 type mismatch 400을 반환한다. | `P2-R1`, `P2-T6` | numeric path mapping RED/GREEN과 KO/EN/JA·CORS 회귀 4건을 고정한다. |
| `REV-002` | High | 확정 | 캐릭터 생성·수정 DTO의 `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 soft-delete 혼합 의미가 Endpoint Contract Summary와 다르다. | `P2-R1`, `P2-T4`, `P2-T5` | canonical request를 먼저 확정하고 문서 JSON 역직렬화·반영 test로 맞춘다. |
| `REV-003` | High | 확정 | 외부 API·S3·DB 실패 사이에 보상 경계가 없고 동시 중복 이름 생성의 원자성 증거가 없다. | `P2-R1`, `P2-T4`, `P2-T5` | failure-order characterization, 선검증, 동시성 정책과 보상/정리 결과를 고정한다. |
| `REV-004` | High | 확정 | 동일한 `seriesIds` 수정도 기존 연결을 삭제·재생성해 row ID·`orders`·`createdAt`을 소실한다. | `P3-R1`, `P3-T6` | 교집합 row를 보존하고 차집합만 변경하는 RED/GREEN을 추가한다. |
| `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 test를 추가한다. |
| `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을 보강하고 기존 기록을 삭제하지 않은 채 정정 기록을 누적한다. |
## 검증 기록
- 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초)을 확인했다.