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

1539 lines
94 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.
---
## 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",
"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,
"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": "File | optional",
"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`
- [ ] **Task 2.1: 기존 character parity 특성화 baseline 고정**
- 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`
- [ ] **Task 2.2: character controller/facade/DTO 구현**
- 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.*'`
---
### 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/{characterId}/audio-contents`
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
- `POST /api/v2/admin/ai-characters/{characterId}/audio-contents`
- `PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
- response item/detail에는 `contentId`, `title`, `status`, `isActive`, `releaseDate`, `coverImageUrl`, `audioSignedUrl?`, 가격/성인/공개 상태 등 기존 관리자 화면 필드를 포함한다.
#### 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`
- [ ] **Task 3.1: 기존 콘텐츠와 signed URL 특성화 baseline 고정**
- 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'`
- [ ] **Task 3.2: content controller/facade/DTO 구현**
- 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.*'`
---
### 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 고정**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/LegacyCreatorAdminSeriesCharacterizationTest.kt`
- CHARACTERIZE: 기존 creator series 구현을 대상으로 목록/상세/생성/수정/soft delete, inactive 조회 정책, 콘텐츠 연결/해제, 조회/검색, 순서 변경의 결과·검증·side effect를 고정한다.
- BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- REFACTOR: fixture와 assertion naming만 정리하고 series parity baseline은 변경하지 않는다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.LegacyCreatorAdminSeriesCharacterizationTest`
- [ ] **Task 4.2: series controller/facade/DTO 구현**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/*`
- RED: Task 4.1 baseline에서 옮긴 series parity, cross-owner detail/update/delete/order, cross-owner content attach, 시리즈 콘텐츠 검색, pagination 경계값 테스트와 controller 권한 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
- GREEN: owner-scoped series endpoint와 시리즈 콘텐츠 `search/page/size` 조회를 구현한다.
- REFACTOR: 기존 owner-less order update를 참조하지 않는지 확인하고 회귀 테스트를 재실행한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`
---
### 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 고정**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/LegacyCommunityPostCharacterizationTest.kt`
- CHARACTERIZE: 기존 community 구현을 대상으로 max fixed count, image/audio/paid post validation, notification/recent-news side effect, fixed post soft delete clearing의 결과와 실패 계약을 고정한다.
- BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- REFACTOR: media fixture와 event spy 중복만 정리하고 side-effect parity baseline은 변경하지 않는다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.LegacyCommunityPostCharacterizationTest`
- [ ] **Task 5.2: community controller/facade/DTO 구현**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/*`
- RED: Task 5.1 baseline에서 옮긴 community parity, cross-owner update, invalid target no-side-effect 테스트와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
- GREEN: owner-scoped community endpoint를 구현하고 목록 `page/size` 기본값·최소·최대 보정을 적용한다.
- REFACTOR: public community 조회 DTO/로직을 무비판적으로 복제하지 않았는지 확인하고 회귀 테스트를 재실행한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`
---
### 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 고정**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/LegacyFanTalkReplyCharacterizationTest.kt`
- CHARACTERIZE: 기존 FanTalk 구현을 대상으로 valid root reply의 언어 감지, 응답 DTO 의미, writer/creator 저장 결과를 고정한다.
- BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
- REFACTOR: FanTalk root/reply fixture만 정리하고 언어 감지·응답·writer/creator baseline은 변경하지 않는다.
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest`
- [ ] **Task 6.2: FanTalk reply controller/facade/DTO 구현**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/*`
- RED: Task 6.1 baseline에서 옮긴 FanTalk reply parity, cross-character/nested/inactive/missing reject, writer/creator 일관성, no-side-effect 테스트와 controller 권한 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
- GREEN: root/active/owner 검증 후 reply 저장 endpoint를 구현한다.
- REFACTOR: nested reply 금지와 writer/creator mapping을 정리하고 회귀 테스트를 재실행한다.
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`
---
### 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`가 통과한다.
- 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 test
./gradlew ktlintCheck
```
#### 전체 회귀 테스트 영향
- 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/regression test 실행**
- TDD 예외 사유: 구현 완료 후 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다.
- 대체 검증 방법: Phase 1~6 targeted test, `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, 기존 admin
security test와 전체 회귀 테스트를 실행한다.
- REFACTOR: 실패가 있으면 관련 Phase Task로 되돌려 최소 수정 후 다시 실행한다.
- Verify: 위 targeted command와 `./gradlew test`를 실행하고 결과를 이 문서 하단 검증 기록에 남긴다.
- [ ] **Task 7.2: API contract와 변경 범위 점검**
- 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`
---
## 검증 기록
- 계획 작성 단계: 코드 변경 없음.
- 문서 규칙 확인: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md`, `docs/prd/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초)을 확인했다.