docs(ai-character): 관리자 API 계약 문서를 고정한다

This commit is contained in:
2026-07-28 02:19:26 +09:00
parent dc1816aea7
commit 2f93e2c9c3
4 changed files with 1843 additions and 80 deletions

View File

@@ -0,0 +1,229 @@
# AI 캐릭터 관리자 API Contract
## 1. 문서 목적
클라이언트 개발과 서버 계약 테스트가 같은 스키마를 사용하도록 AI 캐릭터 관리자 API의 전체 request/response를
OpenAPI 3.1 JSON으로 고정한다.
- 정식 계약: `api-contract.openapi.json`
- endpoint: 23개
- 현재 route 구현: 9개
- 현재 구현 중 계약 정합화 필요: 9개
- 구현 예정: 14개
- 공통 envelope: `ApiResponse<T>`
- 인증: JWT `ROLE_ADMIN`과 현재 DB `Member.role == ADMIN` 동시 충족
`x-implementation-status`의 의미는 다음과 같다.
- `implemented-contract-alignment-required`: route는 구현되어 있지만 2026-07-28 확정 레거시 JSON 계약과 runtime DTO가
아직 달라 `P23-CONTRACT-2`, `P23-CONTRACT-3`을 완료하기 전 호출 호환을 보장하지 않는다.
- `planned`: 계약만 고정되었고 route는 아직 구현되지 않았다.
## 2. 계약 결정
1. 신규 endpoint와 `characterId` 기반 관리자 target 경계는 유지한다.
2. JSON 필드명, 타입, optional/nullable, 기본값과 성공 `data` 형태는 레거시 API를 유지한다.
3. 신규 path에 포함된 `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`만 request body에서 중복 제거한다.
4. 레거시 mutation이 `ApiResponse.ok(null)`이면 신규 endpoint도 `data: null`을 반환한다.
5. 오디오 콘텐츠 생성은 레거시 `CreateAudioContentResponse(contentId)`를 반환한다.
6. FanTalk 답변 작성만 신규 계획의 축약 응답
`fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다.
7. FanTalk 목록은 공개 v2 `CreatorChannelFanTalkTabResponse`의 필드 형태를 유지하는 관리자 전용 endpoint로 추가한다.
8. 시리즈 연결 콘텐츠 목록과 미연결 콘텐츠 검색은 response shape가 달라 별도 endpoint로 분리한다.
## 3. endpoint와 레거시 근거
| 상태 | Method | Endpoint | request 근거 | response `data` 근거 |
|---|---|---|---|---|
| 정합화 필요 | GET | `/api/v2/admin/ai-characters` | `searchTerm?`, `page`, `size` | `ChatCharacterListPageResponse` / `ChatCharacterSearchListPageResponse` |
| 정합화 필요 | POST | `/api/v2/admin/ai-characters` | `ChatCharacterRegisterRequest`, 필수 `image` | `null` |
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/{characterId}` | path only | `ChatCharacterDetailResponse` |
| 정합화 필요 | PUT | `/api/v2/admin/ai-characters/{characterId}` | `ChatCharacterUpdateRequest`에서 `id` 제외 | `null` |
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | body 없음 | `List<GetAudioContentThemeResponse>` |
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | `search_word?`, `page`, `size` | `GetCreatorAdminContentListResponse` |
| 정합화 필요 | POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | `CreateAudioContentRequest`, `contentFile`, `coverImage` | `CreateAudioContentResponse` |
| 정합화 필요 | GET | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | 필수 `timezone` | `GetAudioContentDetailResponse` |
| 정합화 필요 | PUT | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | `UpdateCreatorAdminContentRequest`에서 `id` 제외 | `null` |
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series` | `page`, `size` | `GetCreatorAdminContentSeriesListResponse` |
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/series` | `CreateSeriesRequest`, 필수 `image` | `null` |
| 계획 | PUT | `/api/v2/admin/ai-characters/{characterId}/series/orders` | `UpdateOrdersRequest` | `null` |
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | path only | `GetCreatorAdminContentSeriesDetailResponse` |
| 계획 | PUT | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | `ModifySeriesRequest`에서 `seriesId` 제외 | `null` |
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` | `page`, `size` | `GetCreatorAdminContentSeriesContentResponse` |
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents` | `AddingContentToTheSeriesRequest`에서 `seriesId` 제외 | `null` |
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/search` | 필수 `search_word` | `List<SearchContentNotInSeriesResponse>` |
| 계획 | DELETE | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}` | body 없음 | `null` |
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/community-posts` | 필수 `timezone`, `page`, `size` | `List<GetCommunityPostListResponse>` |
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/community-posts` | `CreateCommunityPostRequest`, `audioFile?`, `postImage?` | `null` |
| 계획 | PUT | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` | 두 레거시 update request에서 ID 제외 | `null` |
| 계획 | GET | `/api/v2/admin/ai-characters/{characterId}/fan-talks` | 공개 v2 `page?`, `size?` | `CreatorChannelFanTalkTabResponse` |
| 계획 | POST | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies` | `content` | 신규 축약 응답 |
## 4. JSON Schema 해석
- 객체의 `required` 배열에 포함된 필드는 JSON key가 필수다.
- `required`에 없으면 optional이며 key를 생략할 수 있다.
- `type: ["string", "null"]`, 다른 union의 `type: "null"`은 명시적 `null`을 허용한다.
- response DTO의 nullable 생성자 필드는 key는 존재하고 값이 `null`일 수 있으므로 `required`와 nullable을 함께 사용한다.
- request의 optional nullable 필드는 key 생략과 명시적 `null`을 모두 허용한다.
- request는 `additionalProperties: false`이므로 정의되지 않은 이름은 계약 위반이다.
- 모든 DB ID는 JSON integer, OpenAPI `int64`다.
- 레거시 날짜 문자열은 원래 format을 유지한다. UTC ISO-8601이 보장되는 FanTalk `createdAtUtc``date-time`이다.
- enum은 대소문자를 구분하며 Kotlin enum 이름을 그대로 사용한다.
- `Accept-Language``ko`, `en`, `ja` 외 문자열도 전송할 수 있고 서버가 KO로 fallback하므로 enum으로 제한하지 않는다.
### multipart
multipart API의 `request` part는 schema상 JSON 객체다.
- part name: `request`
- part Content-Type: `application/json`
- Character 생성: `image`, `request` 필수
- Character 수정: `image` optional, `request` 필수
- AudioContent 생성: `contentFile`, `coverImage`, `request` 필수
- AudioContent 수정: `coverImage` optional, `request` 필수
- Series 생성: `image`, `request` 필수
- Series 수정: `image` optional, `request` 필수
- Community 생성: `audioFile`, `postImage` optional, `request` 필수
- Community 수정/고정: `postImage` optional, `request` 필수
## 5. domain별 전체 필드 근거
### Character
- request:
`ChatCharacterRegisterRequest`, `ChatCharacterUpdateRequest`,
`ChatCharacterRelationshipRequest`, `ChatCharacterPersonalityRequest`,
`ChatCharacterBackgroundRequest`, `ChatCharacterMemoryRequest`
- response:
`ChatCharacterListResponse`, `ChatCharacterListPageResponse`,
`ChatCharacterSearchListPageResponse`, `ChatCharacterDetailResponse`,
`RelationshipResponse`, `PersonalityResponse`, `BackgroundResponse`,
`MemoryResponse`, `OriginalWorkBriefResponse`
- 목록 response는 `totalCount`, `content`이며 `items/page/size/hasNext`로 바꾸지 않는다.
- 상세에는 `systemPrompt`, 캐릭터 속성 배열과 `originalWork`를 포함한다.
- 생성과 수정의 성공 `data`는 상세가 아니라 `null`이다.
- 수정은 `isActive=false`와 다른 optional field의 동시 입력도 레거시 request처럼 허용한다. 이 경우 레거시 service 의미대로
비활성화만 반영하고 나머지 JSON field는 적용하지 않는다.
### AudioContent
- request:
`CreateAudioContentRequest`, `UpdateCreatorAdminContentRequest`
- response:
`GetAudioContentThemeResponse`, `GetCreatorAdminContentListResponse`,
`GetCreatorAdminContentListItem`, `CreateAudioContentResponse`,
`GetAudioContentDetailResponse`, `OtherContentResponse`,
`AudioContentCreator`, `ContentBuyer`,
`GetAudioContentCommentListItem`, `TranslatedContent`
- `detail`, `releaseDate`, `contentFile`, `id/theme/image`,
`audioContentId`, `contentUrl`, `tags`를 레거시 이름 그대로 사용한다.
- 현재 v2 전용 `description`, `releaseDateUtc`, `audioSignedUrl`, `status`,
`seriesIds`, `themeName`, `imageUrl` 별칭은 정식 계약에 포함하지 않는다.
### Series
- request:
`CreateSeriesRequest`, `ModifySeriesRequest`,
`AddingContentToTheSeriesRequest`, `RemoveContentToTheSeriesRequest`,
`UpdateOrdersRequest`
- response:
`GetCreatorAdminContentSeriesListResponse`,
`GetCreatorAdminContentSeriesDetailResponse`,
`GetCreatorAdminContentSeriesContentResponse`,
`SearchContentNotInSeriesResponse`
- `publishedDaysOfWeek` enum은 `SUN..SAT`, `RANDOM`이며 state는
`PROCEEDING`, `SUSPEND`, `COMPLETE`다.
- 상세의 `publishedDaysOfWeek`, `genre`, `keywords`, `state`는 레거시처럼 문자열이다.
- 상세 `state`의 현재 관찰 값은 `연재중`, `휴재중`, `완결`이지만 DTO 타입이 `String`이므로 enum으로 제한하지 않는다.
- 연결 request는 `contentIdList`, 순서 request는 `ids`다.
### Community
- request:
`CreateCommunityPostRequest`, `ModifyCommunityPostRequest`,
`UpdateCommunityPostFixedRequest`
- response:
`GetCommunityPostListResponse`, `GetCommunityPostCommentListItem`
- 목록 `data`는 pagination wrapper가 아니라 직접 배열이다.
- 생성 part 이름은 `postImage`, `audioFile`이다.
- 수정 endpoint는 기존 본문 수정과 고정/해제를 합치므로 ID를 제외한
`content`, `isCommentAvailable`, `isAdult`, `isActive`, `isFixed`를 받는다.
- 레거시에 없는 수정 `price`, `audioFile`은 포함하지 않는다.
### FanTalk
- 목록 response:
`CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkResponse`,
`CreatorChannelFanTalkReplyResponse`
- 공개 v2 endpoint를 직접 사용하지 않는다. 공개 v2는 `creatorId`, viewer 인증과 block filter, 다른 CORS 경계를 사용하기
때문이다.
- 신규 관리자 목록은 `characterId`로 creator를 해석하고 공개 v2 response field만 유지한다.
- 답변 request는 path로 이동한 `creatorId`, `parentId`를 제외하고 `content`만 받는다.
- 답변 response는 사용자 승인 예외인 신규 축약 형태다.
## 6. 공통 응답과 오류
일반 조회 성공:
```json
{
"success": true,
"message": null,
"data": {},
"errorProperty": null
}
```
레거시 mutation 성공:
```json
{
"success": true,
"message": null,
"data": null,
"errorProperty": null
}
```
오류:
```json
{
"success": false,
"message": "현지화된 오류 메시지",
"data": null,
"errorProperty": null
}
```
주요 status는 400, 401, 403, 404, 405, 406, 415, 500이다. 405는 `Allow`, 415는 `Accept` header를 유지한다.
Spring CORS 계층이 차단한 미허용 Origin의 403 body는 이 envelope 계약 대상이 아니다.
## 7. 기계 검증과 클라이언트 생성
```bash
jq empty docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @redocly/cli lint --skip-rule info-license \
docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @openapitools/openapi-generator-cli validate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @openapitools/openapi-generator-cli generate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json \
-g typescript-fetch \
-o /tmp/ai-character-admin-typescript-client
tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM \
/tmp/ai-character-admin-typescript-client/index.ts
```
`info-license`만 제외하는 이유는 저장소 라이선스 값을 추측해 계약에 추가하지 않기 위해서다.
생성 클라이언트에는 23개 operation이 모두 포함된다. `implemented-contract-alignment-required` operation은 해당 runtime
정합화 Gate가 끝나기 전 production 호출 대상으로 간주하지 않는다.
OpenAPI의 `/` server URL은 현재 host를 의미하지만 OpenAPI Generator 7.24.0의 `typescript-fetch` runtime 기본값은
`http://localhost`다. 실제 클라이언트는 배포 환경의 API origin을 `new Configuration({ basePath: "..." })`로 반드시
지정한다.

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,8 @@
> **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를 구현한다.
**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 컴포넌트를 테스트로 고정해 선택적으로 재사용한다.
@@ -13,20 +14,20 @@
| 상태 | 구현 중 |
| 작성일 | 2026-07-24 |
| 요구사항 기준 | `docs/20260724_AI캐릭터_관리자_API/prd.md` |
| API 기준 | 이 문서의 `Endpoint Contract Summary` |
| 현재 Phase | Phase 3 6차 리뷰 완료 |
| 현재 활성 Goal | `P4-T1` 대기 |
| API 기준 | `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json` |
| 현재 Phase | Phase 2·3 레거시 계약 정합화 |
| 현재 활성 Goal | `P23-CONTRACT-2` 대기 |
## 현재 상태
| Phase | 상태 | 기존 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | 7/7 | 완료 | 없음 |
| 2 | 완료 | 11/11 | 완료 | 없음 |
| 3 | 완료 | 15/15 | 완료 | 없음 |
| 4 | 대기 | 0/6 | `P4-T1` | `P3-R5-GATE`, 사용자 진행 지시 |
| 2 | 후속 보완 대기 | 11/11 | `P23-CONTRACT-2` | 레거시 JSON 계약과 현재 v2 구현 정합화 |
| 3 | 후속 보완 대기 | 15/15 | `P23-CONTRACT-3` | `P23-CONTRACT-2` |
| 4 | 대기 | 0/6 | `P4-T1` | `P23-CONTRACT-GATE` |
| 5 | 대기 | 0/6 | `P5-T1` | `P4-GATE` |
| 6 | 대기 | 0/4 | `P6-T1` | `P5-GATE` |
| 6 | 대기 | 0/5 | `P6-T1` | `P5-GATE` |
| 7 | 대기 | 0/2 | `P7-T1` | Phase 1~6 Gate 완료 |
- Phase는 결과와 의존성을 묶는 문서 단위다. `create_goal`에는 Task 또는 Phase Gate 하나만 등록한다.
@@ -56,6 +57,15 @@
415 `Accept` 표준 header를 유지한다.
- 문서 작성 규칙: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md`
- 기존 AI 캐릭터 연결 문서: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/{prd.md,plan-task.md}`
- 2026-07-28 후속 확정 정책: 신규 endpoint의 JSON request/response는 레거시 필드명·타입·optional/nullable·기본값과
성공 `data` 형태를 그대로 유지한다. path로 이동한 ID만 body에서 제거한다. FanTalk 답변 작성만 계획의 축약 응답을
유지하고, FanTalk 목록은 공개 v2 `CreatorChannelFanTalkTabResponse` 필드 형태의 관리자 전용 endpoint로 제공한다.
따라서 `DEC-P2-T5-001`의 캐릭터 `isActive=false` 단독 입력 제한은 계약 차원에서 폐기하고, 레거시처럼 다른 optional
field와 동시 입력을 허용하되 비활성화만 반영한다.
- 기계 검증 가능한 API 계약 원본:
`docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- 계약 근거와 예외 설명:
`docs/20260724_AI캐릭터_관리자_API/api-contract.md`
## Endpoint Contract Summary
@@ -67,7 +77,37 @@
`characterId`는 target resource endpoint의 외부 대상 식별자다. 캐릭터 목록/검색은 선택된 target이 없어 `characterId`를 받지 않고, 캐릭터 생성은 새 `ChatCharacter`를 만드는 endpoint라 path `characterId`를 받지 않는다.
모든 목록/검색 endpoint`page` 기본값 0, `size` 기본값 20, 최소 20, 최대 50 보정을 적용하고 경계값 테스트를 둔다.
목록/검색 endpoint의 query와 pagination은 `api-contract.openapi.json`에 명시한 각 레거시 API를 따른다. FanTalk 관리자
목록만 공개 v2 query policy의 `page` 기본값 0, `size` 기본값 20, 최소 20, 최대 50 보정을 적용한다.
정식 endpoint, request/response schema, 타입, required/optional/nullable, 기본값과 multipart encoding은
`api-contract.openapi.json`만 기준으로 사용한다. 사람이 읽는 레거시 근거와 생성 방법은 `api-contract.md`를 참고한다.
| Domain | operation | 구현 상태와 소유 Goal |
|---|---:|---|
| Character | 4 | runtime 정합화 필요, `P23-CONTRACT-2` |
| AudioContent | 5 | runtime 정합화 필요, `P23-CONTRACT-3` |
| Series | 9 | 구현 예정, Phase 4 |
| Community | 3 | 구현 예정, Phase 5 |
| FanTalk | 2 | 구현 예정, Phase 6 |
- 신규 path에 포함된 `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`만 레거시 request body에서 제거한다.
- 그 밖의 JSON 필드명·타입·optional/nullable·기본값과 성공 `data` 형태는 레거시 API를 유지한다.
- 레거시 mutation의 성공 `data``null`이고 오디오 콘텐츠 생성만 `CreateAudioContentResponse(contentId)`를 반환한다.
- FanTalk 답변 작성만 승인된 축약 응답 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다.
- 캐릭터 수정의 `isActive=false`는 다른 optional JSON field와 함께 받을 수 있으며 레거시 의미대로 비활성화만 반영한다.
- multipart의 `request` part는 `application/json`이고 각 파일 part의 이름과 required 여부는 OpenAPI encoding을 따른다.
- 공통 오류는 400/401/403/404/405/406/415/500과 `ApiResponse.error`를 사용한다. 405의 `Allow`, 415의 `Accept`,
미지원 `Accept-Language`의 KO fallback과 Spring CORS 정책 거부 403 예외를 유지한다.
- Character·AudioContent 9개 operation은 `P23-CONTRACT-GATE` 완료 전 production 호출 호환을 보장하지 않는다.
---
## 과거 구현 계약 이력 (비규범)
아래 축약 예시는 2026-07-28 레거시 계약 확정 전 Phase 2·3 구현과 계획 변경 이력을 보존하기 위한 자료다.
클라이언트 개발, 신규 구현, 테스트의 계약으로 사용하지 않으며 위 Endpoint Contract Summary와
`api-contract.openapi.json`이 항상 우선한다.
```json
{
@@ -652,6 +692,8 @@ Response `data`:
---
## 구현 Phase
### Phase 1: 공통 ADMIN 인증과 AI 캐릭터 target resolver 기반
#### 공통 Task 실행 규칙
@@ -956,11 +998,14 @@ AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2
- 기존 `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 의미다.
- 정식 schema는 `api-contract.openapi.json`의 Character operation을 따른다.
- `GET /api/v2/admin/ai-characters?searchTerm=&page=&size=` ->
`ChatCharacterListPageResponse(totalCount, content)` 또는 동일 필드의 검색 response.
- `GET /api/v2/admin/ai-characters/{characterId}` -> nested 필드 전체를 포함한 `ChatCharacterDetailResponse`.
- `POST /api/v2/admin/ai-characters` multipart 필수 `image`, 필수 `request: ChatCharacterRegisterRequest` -> `data: null`.
- `PUT /api/v2/admin/ai-characters/{characterId}` multipart optional `image`, 필수
`request: ChatCharacterUpdateRequest`에서 `id` 제외 -> `data: null`.
- update request의 `isActive=false`는 레거시 soft delete 의미다.
#### entity, repository, service 변경
- Entity: 변경 없음.
@@ -1397,29 +1442,21 @@ git diff --check
#### 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}`
- Response: `List<GetAudioContentThemeResponse(id, theme, image)>`.
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search_word=&page=&size=` ->
`GetCreatorAdminContentListResponse` 전체 필드.
- `GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}?timezone=Asia/Seoul` ->
`GetAudioContentDetailResponse` 전체 nested 필드.
- `POST /api/v2/admin/ai-characters/{characterId}/audio-contents`
- multipart `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 입력으로 변환한다.
- multipart 필수 `contentFile`, `coverImage`, `request: CreateAudioContentRequest`.
- Response: `CreateAudioContentResponse(contentId)`.
- `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 상세 필드는 관리자 상세에서 안전한 기본값을 반환한다.
- multipart optional `coverImage`, 필수 `request: UpdateCreatorAdminContentRequest`에서 `id` 제외.
- Response: `data: null`.
- 수정 `audioFile` 교체는 레거시 creator admin 수정 pipeline에 없어 제공하지 않는다.
- 정식 전체 schema와 optional/nullable은 `api-contract.openapi.json`의 AudioContent operation을 따른다. 현재 구현의
`description`, `releaseDateUtc`, `audioSignedUrl`, `status`, `seriesIds`, `themeName`, `imageUrl` 별칭은
`P23-CONTRACT-3`에서 레거시 계약으로 정합화한다.
#### entity, repository, service 변경
- Entity: 변경 없음.
@@ -1964,6 +2001,135 @@ git diff --check
---
### Phase 2·3 후속: 레거시 JSON 계약 정합화
기존 Phase 2·3 완료 이력은 보존한다. 2026-07-28 확정된 레거시 필드명·전체 payload 이관 정책에 따라 문서 계약을 먼저
고정하고, 현재 구현된 캐릭터·오디오 콘텐츠 v2 DTO와 endpoint를 별도 후속 Goal에서 정합화한다.
- [x] **Task 3.16: 전체 API OpenAPI 계약 고정**
**Goal 실행 `P23-CONTRACT-1`:** 23개 endpoint의 request/response를 레거시 DTO 전체 필드와 직접 대조해 OpenAPI 3.1
JSON과 설명 문서로 고정한다.
- **시작 조건:** 사용자 확정 정책과 시리즈 미연결 콘텐츠 검색 endpoint 분리 결정.
- **완료 증거:** JSON 문법·OpenAPI lint/validate·TypeScript client 생성 및 `tsc --noEmit` 성공, 23개 operation과 누락 `$ref` 0건,
`./gradlew tasks --all` 성공 및 검증 기록.
- **범위 밖:** production DTO/controller/test 수정, legacy/public endpoint 변경.
- **TDD 예외 사유:** 실행 코드를 변경하지 않는 계약 문서 작성 Task다.
- **대체 검증 방법:** 레거시 Kotlin DTO의 생성자 필드와 OpenAPI schema를 대조하고 두 validator와 client generator로
기계 검증한다.
**Files:**
- Modify: `docs/20260724_AI캐릭터_관리자_API/prd.md`
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
- Create: `docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json`
- Create: `docs/20260724_AI캐릭터_관리자_API/api-contract.md`
- [x] 캐릭터, 테마·오디오 콘텐츠, 시리즈·연결 콘텐츠 검색, 커뮤니티, FanTalk의 레거시 DTO 전체 필드를 schema로
옮긴다.
- [x] path로 이동한 ID만 body에서 제거하고 나머지 query/body/response 필드명은 레거시와 동일하게 유지한다.
- [x] 레거시 mutation의 `data: null`, 오디오 생성의 `data.contentId`, FanTalk 답변 축약 응답 예외를 operation별로
고정한다.
- [x] FanTalk 관리자 목록과 시리즈 미연결 콘텐츠 검색을 별도 operation으로 포함해 총 23개 endpoint를 검증한다.
- [x] OpenAPI lint/validate, TypeScript Fetch client 생성과 `tsc --noEmit`을 실행하고 결과를 기록한다.
- [ ] **Task 3.17: Phase 2 캐릭터 runtime 계약 정합화**
**Goal 실행 `P23-CONTRACT-2`:** 현재 구현된 캐릭터 4개 endpoint를
`api-contract.openapi.json`의 레거시 필드명·전체 request/response·mutation 응답에 맞춘다.
- **시작 조건:** `P23-CONTRACT-1` 완료.
- **완료 증거:** 4개 actual endpoint의 exact JSON field/required/nullable/multipart/`data: null` RED/GREEN,
Phase 2 focused·legacy 회귀와 Progress 기록.
- **범위 안:** `isActive=false`와 다른 optional JSON field의 동시 입력 허용 및 나머지 JSON field 미반영이라는 레거시
request 의미 복구.
- **범위 밖:** 외부 API·ownership·soft delete persistence 결과 변경, 신규 character business behavior.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
- [ ] **RED:** 목록 `content`, 상세 전체 nested 필드, 생성 전체 request와 필수 `image`, 수정 optional 필드,
`isActive=false`와 다른 optional JSON field의 동시 입력·미반영, 생성·수정 `data: null` exact JSON 테스트를 작성해
현재 v2 축약/변환 DTO와의 불일치를 확인한다.
- [ ] **GREEN:** business pipeline을 바꾸지 않는 최소 DTO/controller/facade/mapper 변경으로 OpenAPI 계약을 통과시킨다.
- [ ] **REFACTOR:** Phase 2 actual endpoint와 legacy characterization, 공통 오류 계약을 회귀하고 결과를 기록한다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
- [ ] **Task 3.18: Phase 3 오디오 콘텐츠 runtime 계약 정합화**
**Goal 실행 `P23-CONTRACT-3`:** 현재 구현된 테마·오디오 콘텐츠 5개 endpoint를
`api-contract.openapi.json`의 레거시 필드명·전체 request/response·성공 응답에 맞춘다.
- **시작 조건:** `P23-CONTRACT-2` 완료.
- **완료 증거:** 5개 actual endpoint의 exact query/multipart/JSON schema RED/GREEN, Phase 3 focused·legacy 회귀와
Progress 기록.
- **범위 밖:** upload/processing pipeline, signed URL 정책, series 연결 behavior 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentThemeControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
- [ ] **RED:** 테마 `id/theme/image`, 목록 전체 item, 상세 전체 nested DTO, 생성 `contentFile`과
`CreateAudioContentRequest`, 수정 `UpdateCreatorAdminContentRequest` 및 각 성공 `data` 형태를 exact JSON으로 고정한다.
- [ ] **GREEN:** 기존 business pipeline과 signed URL 계산을 유지하는 최소 DTO/controller/facade/mapper adapter를 구현한다.
- [ ] **REFACTOR:** Phase 3 actual endpoint·legacy characterization·ownership/error 계약을 회귀하고 결과를 기록한다.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
```
#### Phase 2·3 계약 정합화 Gate
**Goal 실행 `P23-CONTRACT-GATE`:** 문서 계약과 구현된 9개 endpoint의 runtime 응답이 일치하고 Phase 4가 같은 계약을
소비할 수 있는지 판정한다.
- [ ] **`P23-CONTRACT-GATE` 완료:** `P23-CONTRACT-1`~`P23-CONTRACT-3` 완료 후 character/content focused·legacy
회귀, OpenAPI validate/client 생성과 `ktlintCheck`를 fresh 실행한다.
- **범위 밖:** Gate에서 직접 production code 수정, Phase 4 이후 기능 구현.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' \
--tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest \
--tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
./gradlew ktlintCheck
npx --yes @openapitools/openapi-generator-cli validate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json
npx --yes @openapitools/openapi-generator-cli generate \
-i docs/20260724_AI캐릭터_관리자_API/api-contract.openapi.json \
-g typescript-fetch \
-o /tmp/ai-character-admin-typescript-client
```
---
### Phase 4: 시리즈 관리 vertical slice
#### 목표
@@ -1979,11 +2145,15 @@ git diff --check
- 기존 creator series 생성/수정/soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior를 통과하는 특성화 테스트로 먼저 고정해야 한다.
#### API endpoint와 request/response contract
- 정식 전체 schema는 `api-contract.openapi.json`의 Series operation 9개를 따른다.
- `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>)`
- `GET /series/{seriesId}/contents` query: `page`, `size`; response:
`GetCreatorAdminContentSeriesContentResponse(totalCount, items)`
- `GET /series/{seriesId}/contents/search` query: 필수 `search_word`; response:
`List<SearchContentNotInSeriesResponse>`
- `POST /series/{seriesId}/contents` request: `AddingContentToTheSeriesRequest(contentIdList: List<Long>)`
- `DELETE /series/{seriesId}/contents/{contentId}`
- `PUT /series/orders` request: `UpdateAiCharacterAdminSeriesOrdersRequest(seriesIds: List<Long>)`
- `PUT /series/orders` request: `UpdateOrdersRequest(ids: List<Long>)`
#### entity, repository, service 변경
- Entity: 변경 없음.
@@ -2028,7 +2198,7 @@ git diff --check
**Goal 실행 `P4-T1`:** 기존 creator series의 CRUD·연결·검색·순서 동작을 신규 v2 구현의 비교 기준으로 고정한다.
- **시작 조건:** 최신 Phase 3 후속 Gate인 `P3-R5-GATE` 완료와 사용자 진행 지시.
- **시작 조건:** Phase 2·3 runtime 계약 정합화의 `P23-CONTRACT-GATE` 완료와 사용자 진행 지시.
- **완료 증거:** production 변경 전 특성화 테스트 통과, 관찰된 오류·side-effect 정책과 Progress 기록.
- **범위 밖:** 신규 v2 series production code 구현.
@@ -2044,10 +2214,10 @@ git diff --check
- [ ] **Task 4.2: 시리즈 목록·상세 조회 구현**
**Goal 실행 `P4-T2`:** target owner의 활성 시리즈 목록·상세를 pagination 계약으로 제공한다.
**Goal 실행 `P4-T2`:** target owner의 활성 시리즈 목록·상세를 레거시 response 계약으로 제공한다.
- **시작 조건:** `P4-T1` 완료와 Phase 4 오류 계약의 계획 반영.
- **완료 증거:** 목록·상세·inactive·cross-owner·pagination RED/GREEN과 Progress 기록.
- **완료 증거:** 목록·상세 전체 필드, inactive·cross-owner·pagination RED/GREEN과 Progress 기록.
- **범위 밖:** 시리즈 mutation, 콘텐츠 연결, 순서 변경.
**Files:**
@@ -2060,7 +2230,8 @@ git diff --check
- [ ] 미구현 목록·상세, inactive 제외, cross-owner 거부와 pagination 경계 실패 test를 작성한다.
- [ ] owner-scoped 목록·상세 최소 구현으로 focused test를 통과시킨다.
- [ ] `page` 기본 0, `size` 기본·최소 20·최대 50과 legacy DTO 비노출을 검증한다.
- [ ] 목록 `totalCount/items`와 item 전체 필드, 상세의 문자열 `publishedDaysOfWeek/genre/keywords/state`를
`api-contract.openapi.json`과 exact JSON으로 검증한다.
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 4.3: 시리즈 생성·수정·soft delete 구현**
@@ -2088,7 +2259,8 @@ git diff --check
**Goal 실행 `P4-T4`:** 동일 owner의 시리즈와 콘텐츠만 검색·연결·해제할 수 있도록 한다.
- **시작 조건:** `P4-T2`, `P4-T3`와 Phase 3 owner query 계약 완료.
- **완료 증거:** 검색·pagination·전체 ID 사전 검증·원자적 연결/해제 RED/GREEN과 Progress 기록.
- **완료 증거:** 연결 목록·미연결 검색의 분리된 응답, pagination·전체 ID 사전 검증·원자적 연결/해제 RED/GREEN과
Progress 기록.
- **범위 밖:** 콘텐츠 자체 수정, 시리즈 순서 변경.
**Files:**
@@ -2098,9 +2270,10 @@ git diff --check
- 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를 작성한다.
- [ ] `GET .../contents`와 `GET .../contents/search?search_word=...`의 별도 응답, 연결·해제와 cross-owner ID 실패
test를 작성한다.
- [ ] 모든 series/content ID를 mutation 전에 검증하는 최소 구현을 통과시킨다.
- [ ] 일부 연결 성공이 남지 않는 원자성과 pagination 경계를 검증한다.
- [ ] `contentIdList` 필드와 mutation `data: null`, 일부 연결 성공이 남지 않는 원자성과 pagination 경계를 검증한다.
- [ ] focused/Phase 3 owner query 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 4.5: owner-scoped 시리즈 순서 변경 구현**
@@ -2168,10 +2341,14 @@ git diff --check
- 기존 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`, 본문/이미지/오디오/가격 필드를 포함한다.
- 정식 전체 schema는 `api-contract.openapi.json`의 Community operation 3개를 따른다.
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts?timezone=&page=&size=` ->
`List<GetCommunityPostListResponse>`.
- `POST /api/v2/admin/ai-characters/{characterId}/community-posts`는 optional `audioFile`, optional `postImage`, 필수
`request: CreateCommunityPostRequest`를 받고 `data: null`을 반환한다.
- `PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`는 optional `postImage`, 필수 request를 받는다.
- update request는 두 레거시 update DTO에서 ID를 제외한 `content`, `isCommentAvailable`, `isAdult`, `isActive`,
`isFixed`만 포함하고 `data: null`을 반환한다. 수정 `audioFile`, `price`는 레거시 계약에 없어 포함하지 않는다.
#### entity, repository, service 변경
- Entity: 변경 없음.
@@ -2340,13 +2517,15 @@ git diff --check
---
### Phase 6: FanTalk 답변 vertical slice
### Phase 6: FanTalk 목록·답변 vertical slice
#### 목표
선택한 AI 캐릭터 자신의 활성 root FanTalk에만 creator reply를 작성하는 v2 관리자 API를 제공한다.
선택한 AI 캐릭터의 FanTalk 목록을 관리자 전용 경계에서 조회하고 자신의 활성 root FanTalk에만 creator reply를 작성하는
v2 관리자 API를 제공한다.
#### 범위와 비범위
- 포함: root FanTalk 존재/활성/owner 검증, creator reply 저장, 언어 감지와 기존 응답 의미 parity.
- 포함: 관리자용 root FanTalk/creator reply 목록, root FanTalk 존재/활성/owner 검증, creator reply 저장, 언어 감지와
기존 응답 의미 parity.
- 제외: FanTalk 원글 작성, nested reply, 구매/댓글형 기능, AI 캐릭터 일반 사용자 활동.
#### 선행 Phase 및 의존성
@@ -2354,14 +2533,19 @@ git diff --check
- 기존 FanTalk 저장 엔티티와 응답 DTO 의미 특성화.
#### API endpoint와 request/response contract
- 정식 전체 schema는 `api-contract.openapi.json`의 FanTalk operation 2개를 따른다.
- `GET /api/v2/admin/ai-characters/{characterId}/fan-talks?page=0&size=20`
- Response: 공개 v2 `CreatorChannelFanTalkTabResponse`와 동일한
`fanTalkCount`, `fanTalks`, `page`, `size`, `hasNext` 및 nested root/reply 필드
- `POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`
- Request: `CreateAiCharacterAdminFanTalkReplyRequest(content: String)`
- Response: `AiCharacterAdminFanTalkReplyResponse(fanTalkId, replyId, creatorMemberId, content, createdAtUtc)`
#### entity, repository, service 변경
- Entity: 변경 없음.
- Repository: `CreatorCheers` 또는 FanTalk repository에 root/active/creator owner 조회 adapter 추가 가능.
- Service: 신규 FanTalk reply application service 구현. target 검증 후 기존 저장/언어 감지 로직을 필요한 만큼 재사용한다.
- Repository: 관리자 목록용 owner-scoped root/reply 조회와 root/active/creator owner 검증 adapter 추가한다.
- Service: 관리자 목록은 공개 v2 DTO 형태로 조립하되 viewer/block 필터를 적용하지 않는다. reply는 target 검증 후 기존
저장/언어 감지 로직을 필요한 만큼 재사용한다.
#### DB migration
- 없음.
@@ -2376,23 +2560,24 @@ git diff --check
#### acceptance criteria
- target AI character는 자신의 활성 root FanTalk에만 답변할 수 있다.
- 관리자는 공개 v2와 동일한 필드 형태로 target AI character의 root FanTalk와 creator reply를 조회할 수 있다.
- cross-character, nested parent, inactive/missing FanTalk는 4xx이며 reply 저장과 이벤트 발행이 없다.
- 저장된 답변의 writer/creator는 해석된 creatorMember와 일관된다.
#### targeted test
- Characterization: `LegacyFanTalkReplyCharacterizationTest`.
- V2 RED/GREEN: `AiCharacterAdminFanTalkReplyServiceTest`.
- V2 RED/GREEN: `AiCharacterAdminFanTalkQueryTest`, `AiCharacterAdminFanTalkReplyServiceTest`.
- Run: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`
#### 전체 회귀 테스트 영향
- 기존 FanTalk 조회/작성 관련 테스트가 통과해야 한다.
#### rollback 전략
- 신규 FanTalk reply v2 admin route/facade를 제거한다.
- 신규 FanTalk 목록·reply v2 admin route/facade를 제거한다.
- 신규 DDL이 없으므로 schema rollback은 없다.
#### 권장 commit 경계
- `feat: add ai character admin fan talk reply slice`
- `feat: add ai character admin fan talk slice`
- [ ] **Task 6.1: 기존 FanTalk reply 의미 특성화 baseline 고정**
@@ -2412,11 +2597,32 @@ git diff --check
- [ ] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
- [ ] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 6.2: FanTalk root reply 저장 구현**
- [ ] **Task 6.2: FanTalk 관리자 목록 조회 구현**
**Goal 실행 `P6-T2`:** 선택한 AI 캐릭터의 활성 root FanTalk creator reply를 저장하고 전용 응답을 반환한다.
**Goal 실행 `P6-T2`:** 선택한 AI 캐릭터의 root FanTalk creator reply를 공개 v2 응답 필드 형태로 조회한다.
- **시작 조건:** `P6-T1` 완료와 Phase 6 오류 계약의 계획 반영.
- **완료 증거:** owner-scoped 목록·nested reply·pagination exact JSON RED/GREEN과 Progress 기록.
- **범위 밖:** 공개 v2 endpoint 변경, viewer/block 필터 재사용, FanTalk 원글 작성.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkDto.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkQueryTest.kt`
- [ ] **RED:** `fanTalkCount/fanTalks/page/size/hasNext`와 root/reply 전체 필드, target owner, pagination 실패
test를 작성한다.
- [ ] **GREEN:** 공개 v2 DTO 필드 형태를 유지하면서 관리자 target 정책으로 조회하는 최소 구현을 통과시킨다.
- [ ] **REFACTOR:** 공개 v2 endpoint 계약 불변, viewer/block 필터 비적용과 ADMIN 인가를 회귀하고 기록한다.
- [ ] **Task 6.3: FanTalk root reply 저장 구현**
**Goal 실행 `P6-T3`:** 선택한 AI 캐릭터의 활성 root FanTalk에 creator reply를 저장하고 전용 응답을 반환한다.
- **시작 조건:** `P6-T1`, `P6-T2` 완료와 Phase 6 오류 계약의 계획 반영.
- **완료 증거:** 정상 저장·언어 감지·DTO·writer/creator RED/GREEN과 Progress 기록.
- **범위 밖:** FanTalk 원글, nested reply, 일반 사용자 대리 작성.
@@ -2433,11 +2639,11 @@ git diff --check
- [ ] writer/creator가 resolver의 creatorMember와 일치하고 principal impersonation이 없음을 검증한다.
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 6.3: FanTalk target·root·ownership 거부 구현**
- [ ] **Task 6.4: FanTalk target·root·ownership 거부 구현**
**Goal 실행 `P6-T3`:** cross-character, nested, inactive, missing FanTalk를 저장·이벤트 없이 거부한다.
**Goal 실행 `P6-T4`:** cross-character, nested, inactive, missing FanTalk를 저장·이벤트 없이 거부한다.
- **시작 조건:** `P6-T1`, `P6-T2` 완료.
- **시작 조건:** `P6-T1`~`P6-T3` 완료.
- **완료 증거:** 네 거부 분기의 정확한 오류 계약, DB/event 0건과 Progress 기록.
- **범위 밖:** 새로운 중복 답변 차단 정책.
@@ -2452,11 +2658,11 @@ git diff --check
- [ ] 각 실패의 정확한 status/message key/KO·EN·JA와 reply insert/event 0건을 검증한다.
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
- [ ] **Task 6.4: Phase 6 보안·오류·회귀 검증**
- [ ] **Task 6.5: Phase 6 보안·오류·회귀 검증**
**Goal 실행 `P6-T4`:** FanTalk reply endpoint의 ADMIN·오류 계약과 기존 FanTalk 회귀를 고정한다.
**Goal 실행 `P6-T5`:** FanTalk 목록·reply endpoint의 ADMIN·오류 계약과 기존 FanTalk 회귀를 고정한다.
- **시작 조건:** `P6-T2`, `P6-T3` 완료.
- **시작 조건:** `P6-T2`~`P6-T4` 완료.
- **완료 증거:** 권한 매트릭스, request binding/domain 오류, legacy 회귀와 Progress 기록.
- **범위 밖:** Phase 7 외 전체 기능 수정.
@@ -2472,11 +2678,11 @@ git diff --check
#### Phase 6 Gate
**Goal 실행 `P6-GATE`:** Phase 6 FanTalk reply의 root·ownership·저장·회귀 품질을 최종 판정한다.
**Goal 실행 `P6-GATE`:** Phase 6 FanTalk 목록·reply의 조회·root·ownership·저장·회귀 품질을 최종 판정한다.
- [ ] **`P6-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
- **시작 조건:** `P6-T1`~`P6-T4` 완료.
- **시작 조건:** `P6-T1`~`P6-T5` 완료.
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와
`./gradlew ktlintCheck` 성공, Progress 기록.
- **범위 밖:** 실패와 무관한 신규 기능.
@@ -2496,7 +2702,7 @@ git diff --check
- Phase 1~6 완료.
#### API endpoint와 request/response contract
- Endpoint Contract Summary의 모든 endpoint가 구현되어야 한다.
- `api-contract.openapi.json`의 23개 operation이 모두 구현되어야 한다.
- 모든 신규 endpoint가 JWT ADMIN + 현재 DB ADMIN 이중 인가와 공통 오류 envelope/i18n을 공유해야 한다.
- legacy/public endpoint URI와 성공·오류 status/body/message diff가 없어야 한다.
@@ -2614,10 +2820,11 @@ git diff --check
| 12 | `P3-R5` → `P3-R6` → `P3-R4-GATE` | `P2-R4-GATE`, 5차 Phase 3 review | 아니요 | `REV-016`~`REV-017` 소유 Task에서 수정·증거 보강 |
| 13 | `P2-R5` → `P2-R5-GATE` | 6차 Phase 2 review | 아니요 | `REV-018` 문서 계약 동기화 |
| 14 | `P3-R7` → `P3-R8` → `P3-R5-GATE` | `P2-R5-GATE`, 6차 Phase 3 review | 아니요 | `REV-019`~`REV-020` 소유 Task에서 수정·증거 보강 |
| 15 | `P4-T1`~`P4-T6` → `P4-GATE` | `P3-R5-GATE`, 사용자 진행 지시 | 아니요 | 실패 소유 Task로 되돌림 |
| 16 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 17 | `P6-T1`~`P6-T4` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 18 | `P7-T1``P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 |
| 15 | `P23-CONTRACT-1` → `P23-CONTRACT-2` → `P23-CONTRACT-3` → `P23-CONTRACT-GATE` | `P3-R5-GATE`, 사용자 계약 확정 | 아니요 | 문서 또는 runtime 불일치 소유 Goal에서 수정 |
| 16 | `P4-T1`~`P4-T6` → `P4-GATE` | `P23-CONTRACT-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 17 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 18 | `P6-T1`~`P6-T5` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
| 19 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 |
## 변경 금지·중단 규칙
@@ -2842,6 +3049,38 @@ git diff --check
불일치를 확인해 `처리 완료`로 동기화했다.
- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시 전까지 시작하지 않는다.
### `P23-CONTRACT-1` 완료 — 2026-07-28
- 상태: 완료
- 무엇을: 캐릭터, 테마·오디오 콘텐츠, 시리즈, 커뮤니티, FanTalk의 신규 관리자 API 23개 request/response를
OpenAPI 3.1 JSON과 설명 문서로 고정했다.
- 왜: 신규 path와 관리자 target만 적용하고 클라이언트 JSON 계약은 레거시 필드명·타입·optional/nullable·기본값과 성공
`data` 형태를 그대로 이관해야 하기 때문이다.
- 어떻게: 레거시 Kotlin DTO·controller·service와 schema를 대조하고 path ID만 body에서 제거했다. FanTalk 답변 축약 응답,
관리자 FanTalk 목록, 분리된 시리즈 미연결 콘텐츠 검색만 확정 예외로 반영했다. 독립 리뷰에서 확인한
`Accept-Language` fallback, 캐릭터 비활성화 혼합 입력, 시리즈 상세 문자열 `state`, Phase 4 선행 Gate 문제를 교정했다.
- 결과: JSON parse, 내부 `$ref` 누락 0건, 고유 operation 23개와 상태 9/14를 확인했다. Redocly lint와 OpenAPI Generator
validate가 통과했고 `typescript-fetch` 생성 후 TypeScript 5.9.3 `tsc --noEmit`도 성공했다.
`./gradlew tasks --all`은 `BUILD SUCCESSFUL in 898ms`였으며 독립 최종 리뷰는 Critical 0, Important 0이었다.
- 남은 항목: 현재 구현된 캐릭터 4개 endpoint의 runtime DTO를 맞추는 `P23-CONTRACT-2`. production code는 변경하지 않았다.
### `P23-CONTRACT-2`~`P7-GATE` 실행 계획 보완 — 2026-07-28
- 상태: 구현 시작 준비 완료
- 무엇을: `P23-CONTRACT-2`·`P23-CONTRACT-3`의 실제 facade와 전용 query/theme/ownership/legacy test 범위를 보강하고,
Task별 focused 명령을 추가했다. Phase 4~6은 각 OpenAPI tag를 정식 schema로, Phase 7은 OpenAPI 23개 operation을
최종 구현 기준으로 명시했다.
- 왜: 확정 계약이 있어도 실제 request parsing·response 조립을 담당하는 facade와 전용 테스트가 계획에서 누락되면
구현 중 범위가 다시 흔들릴 수 있기 때문이다.
- 어떻게: OpenAPI operation 23개와 Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2 분류를 Phase/Goal과
대조하고 기존 source/test 파일 존재, Goal ID 중복, 선행 Gate와 PRD Open Questions를 확인했다.
- 결과: 과거 축약 예시는 비규범 이력으로 분리했고 현재 Endpoint Contract Summary는 OpenAPI와 같은 9/14 상태 및 예외만
제공한다. Task 3.17부터 `P7-GATE`까지 시작 조건·파일·RED/GREEN/REFACTOR·검증 명령이 연결됐으며
`./gradlew tasks --all`은 `BUILD SUCCESSFUL in 922ms`였다.
- 제외: 생성 TypeScript의 `tsc` 실행은 클라이언트 build/CI 책임으로 두고 server runtime `P23-CONTRACT-GATE`에는 추가하지
않았다. `P23-CONTRACT-1`의 일회성 생성·컴파일 검증 기록은 유지한다.
- 다음 행동: production 변경 없이 문서 보완만 완료했다. 구현 시작 Goal은 `P23-CONTRACT-2`다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
@@ -2856,6 +3095,10 @@ git diff --check
| 2026-07-28 | `DEC-REVIEW-005` | 확정 | 5차 보완과 완료 이력은 보존하고, 추가로 확인한 문서 계약·empty-file 경계·detached publisher 증거 문제를 `REV-018`~`REV-020`과 새 후속 Task/Gate로 추적한다. | fresh focused 216건과 lint는 통과했지만 Endpoint Contract Summary, `MultipartFile.isEmpty` 처리와 실제 service publisher field를 코드·test 단위로 대조해 세 문제가 재현됐다. | `P2-R5`~`P3-R5-GATE`, 두 6차 review |
| 2026-07-28 | `DEC-P3-R7-001` | 확정 | 생성의 빈 `coverImage`·`audioFile`은 400 `common.error.invalid_request`로 거부한다. 수정의 빈 `coverImage`는 생략으로 정규화하고, 수정 `audioFile`은 미지원이므로 part가 존재하면 크기와 관계없이 400으로 거부한다. | non-null binding은 part 누락만 차단하며 빈 파일은 0-byte upload와 cover 교체를 유발할 수 있다. optional cover의 빈 part는 일반 multipart client의 생략 표현으로 안전하게 처리할 수 있지만 미지원 audio part는 존재 자체가 계약 위반이다. | `P3-R7`, `REV-019`, Endpoint Contract Summary |
| 2026-07-28 | `DEC-REVIEW-006` | 확정 | Phase 2·3 6차 보완의 코드와 완료 이력은 유지하고 새 production Goal은 추가하지 않는다. 종결 Gate가 있는 `REV-001`~`REV-009`의 종합 표 상태만 `처리 완료`로 동기화한다. | fresh focused 130건이 모두 통과했고 6차 보완 범위의 추가 production 결함은 재현되지 않았지만, 하단 종합 표 상태가 각 Gate·Progress의 처리 완료 판정과 모순됐다. | `P2-GATE`, `P2-R2-GATE`, `P3-GATE`, 하단 발견된 문제 표 |
| 2026-07-28 | `DEC-API-CONTRACT-001` | 확정 | 신규 관리자 endpoint는 레거시 request/response의 필드명·타입·optional/nullable·기본값·성공 `data` 형태를 유지하고 path로 이동한 ID만 body에서 제거한다. FanTalk 답변은 축약 응답을 유지한다. | 신규 endpoint의 목적이 로그인 불가능한 AI 캐릭터를 관리자 경계로 대리 관리하는 것이며, 클라이언트 계약까지 임의로 재설계하는 범위가 아니기 때문이다. | PRD, `api-contract.openapi.json`, `P23-CONTRACT-1`~`P23-CONTRACT-GATE` |
| 2026-07-28 | `DEC-API-CONTRACT-002` | 확정 | FanTalk 목록은 공개 v2 field 형태를 유지하는 관리자 전용 endpoint로 추가하고, 시리즈 연결 목록과 미연결 검색은 응답 형태가 달라 별도 endpoint로 분리한다. | 공개 v2 직접 호출은 `creatorId`, viewer/block filter와 CORS 경계가 관리자 요구와 다르고, 시리즈 두 legacy API의 응답은 wrapper와 direct array로 서로 다르다. | Phase 4, Phase 6, `api-contract.openapi.json` |
| 2026-07-28 | `DEC-API-CONTRACT-003` | 확정 | `DEC-P2-T5-001`의 캐릭터 `isActive=false` 단독 입력 제한을 최신 JSON 계약에서 폐기한다. `ChatCharacterUpdateRequest`처럼 다른 optional field와 동시 입력을 허용하고, `isActive=false`이면 레거시 service와 같이 비활성화만 반영한다. | 사용자 확정 원칙은 path ID만 제거하고 레거시 request를 그대로 이관하는 것이다. 레거시 controller는 혼합 request를 받으며 service는 비활성화 분기에서 나머지 JSON field를 적용하지 않는다. | `api-contract.openapi.json`, `P23-CONTRACT-2` |
| 2026-07-28 | `DEC-API-CONTRACT-004` | 확정 | 생성된 TypeScript의 `tsc` 실행은 클라이언트 build/CI 책임으로 두고 server runtime `P23-CONTRACT-GATE` 완료 조건에는 추가하지 않는다. | 서버 Gate는 실제 HTTP runtime과 OpenAPI validate/client 생성 가능성을 판정하며, 언어별 client compile은 소비 클라이언트의 toolchain에서 검증해야 한다. 계약 작성 Task의 일회성 TypeScript 생성·컴파일 검증은 이미 완료됐다. | `P23-CONTRACT-GATE`, `api-contract.md` |
## 발견된 문제
@@ -2883,6 +3126,20 @@ git diff --check
| `REV-020` | Low | 처리 완료 | Phase 3 ownership/domain test의 event no-interaction mock이 실제 `AudioContentService`·`CreatorAdminContentService` publisher field에 연결되지 않았다. | `P3-R8`, `P3-R5-GATE` | 실제 두 service proxy target의 publisher를 mock으로 교체·복원하고 identity/no-interaction을 단언했다. |
## 검증 기록
- 실행 계획 동기화 검증(2026-07-28): OpenAPI 23개 고유 operation과 Character 4, AudioContent 5, Series 9,
Community 3, FanTalk 2 분류를 현재 Endpoint Contract Summary 및 `P23-CONTRACT-2`~Phase 7과 대조했다. Goal ID 중복은
없고 PRD Open Questions는 `없음`이며 Task 3.17·3.18에 추가한 production/test/characterization 파일의 존재를 확인했다.
- 실행 계획 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고
`BUILD SUCCESSFUL in 922ms`였다. `git diff --check`, `git diff --cached --check`는 출력이 없었다.
- `P23-CONTRACT-1` OpenAPI 검증(2026-07-28): `jq empty`, 23개 고유 operation과
`implemented-contract-alignment-required` 9개/`planned` 14개 assertion, 내부 `$ref` 해석을 실행해 모두 성공했다.
`npx --yes @redocly/cli lint --skip-rule info-license ...`는 `valid`, OpenAPI Generator `validate`는
`No validation issues detected`였다.
- `P23-CONTRACT-1` 클라이언트 검증(2026-07-28): OpenAPI Generator 7.24.0 `typescript-fetch` 생성을 완료하고,
생성된 23개 Raw operation(Character 4, AudioContent 5, Series 9, Community 3, FanTalk 2)을 확인했다.
`tsc --noEmit --target ES2020 --module commonjs --lib ES2020,DOM .../index.ts`는 TypeScript 5.9.3에서 성공했다.
- `P23-CONTRACT-1` 문서·리뷰 Gate(2026-07-28): `./gradlew tasks --all`은 `BUILD SUCCESSFUL in 898ms`였고,
독립 레거시 DTO/controller/service 대조 및 생성물 재검증 결과 Critical 0, Important 0이었다.
- Phase 2·3 6차 보완 재점검 focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 19s`였다. 다섯 XML 합계 130건의 failure/error/skipped는 모두 0이었다.
- Phase 2·3 6차 보완 재점검 문서 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 858ms`였다.
- Phase 2·3 6차 보완 재점검 판정(2026-07-28): `REV-018`~`REV-020`의 production/test 보완과 Gate 완료 증거가 일치했고 추가 production finding은 확정되지 않았다. 하단 종합 표에서만 미처리로 남은 `REV-001`~`REV-009`를 각 소유 Gate·Progress 판정에 맞춰 `처리 완료`로 동기화했다.

View File

@@ -19,7 +19,7 @@
- 모든 신규 target endpoint는 외부 대상 식별자로 `characterId`를 받고, 서버가 `ChatCharacter.creatorMember`를 내부 행위자로 해석한다. 단, 캐릭터 목록/검색은 아직 선택된 target이 없어 `characterId`를 받지 않고, 캐릭터 생성은 새 `ChatCharacter`를 만드는 endpoint라 path `characterId`를 받지 않는다.
- target 해석 시 `ChatCharacter` 존재, `creatorMember` 존재, `creatorMember.role == CREATOR`, `creatorMember.memberKind == AI_CHARACTER`를 모두 검증한다.
- 검증 실패 시 4xx로 거부하고 DB, S3, 외부 캐릭터 API, 이벤트 발행 등 후속 부작용을 만들지 않는다.
- 캐릭터, 오디오 콘텐츠, 시리즈, 커뮤니티 게시글, FanTalk 답변, 오디오 signed URL을 신규 관리자 API에서 관리한다.
- 캐릭터, 오디오 콘텐츠, 시리즈, 커뮤니티 게시글, FanTalk 목록·답변, 오디오 signed URL을 신규 관리자 API에서 관리한다.
- 기존 legacy/public endpoint의 URI, 성공·오류 HTTP status, response body, message/i18n을 포함한 외부 계약은 변경하지 않는다.
단, 캐릭터 관리자 frontend가 기존 관리자 인증을 재사용할 수 있도록 `/admin/member/login`, `/member/logout`의 CORS 허용
Origin만 path-specific으로 확장한다.
@@ -56,7 +56,7 @@
- 운영자는 선택한 AI 캐릭터 소유 오디오 콘텐츠를 조회, 생성, 수정, soft delete하고 관리자 화면에서 재생 가능한 signed URL을 받고 싶다.
- 운영자는 선택한 AI 캐릭터 소유 시리즈를 조회, 생성, 수정, soft delete하고 콘텐츠 연결/해제/순서를 관리하고 싶다.
- 운영자는 선택한 AI 캐릭터 소유 커뮤니티 게시글을 작성, 수정, 고정/해제, soft delete하고 싶다.
- 운영자는 선택한 AI 캐릭터가 자신의 활성 root FanTalk creator reply를 작성하게 하고 싶다.
- 운영자는 선택한 AI 캐릭터 FanTalk 목록과 기존 creator reply를 확인한 뒤 활성 root FanTalk에 답변하고 싶다.
- 서버는 다른 AI 캐릭터나 HUMAN creator의 resource ID가 전달되면 변경 없이 4xx로 거부해야 한다.
---
@@ -95,7 +95,8 @@
#### Requirements
- 캐릭터 소유 콘텐츠 목록/검색/상세 조회, 생성, 수정, 기존 삭제 동작에 따른 soft delete를 제공한다.
- 콘텐츠 생성 화면에서 사용할 활성 콘텐츠 테마(카테고리) 목록 조회를 제공한다. 기존 크리에이터 관리자 콘텐츠 등록 화면의 테마 조회와 같은 기능이며, 신규 v2 응답 필드는 `themeId`, `themeName`, `imageUrl`로 명확히 구분한다.
- 콘텐츠 생성 화면에서 사용할 활성 콘텐츠 테마(카테고리) 목록 조회를 제공한다. 기존 크리에이터 관리자 콘텐츠 등록
화면의 `GetAudioContentThemeResponse`와 같은 `id`, `theme`, `image` 필드명을 유지한다.
- 기존 크리에이터 콘텐츠 관리의 검증, 파일 처리, content upload/processing pipeline, 가격, 공개/예약, 번역/알림 등 business behavior parity를 유지한다.
- 관리자 화면 재생용 signed URL을 콘텐츠 목록/상세 응답에 제공한다.
- signed URL은 공통 `AudioContentCloudFront`를 재사용하고 기존 크리에이터 관리자와 같은 만료 정책을 따른다.
@@ -135,6 +136,9 @@
### Feature F. FanTalk 답변
#### Requirements
- 선택한 AI 캐릭터의 FanTalk 목록과 각 root 글의 creator reply 목록을 관리자 전용 endpoint로 조회한다.
- 목록 응답 필드와 page 정책은 공개 v2 `CreatorChannelFanTalkTabResponse`를 유지하되, 공개 v2 endpoint를 직접 재사용하지
않고 `characterId` target 해석과 관리자 ownership 정책을 적용한다.
- 선택한 AI 캐릭터가 자신의 활성 root FanTalk에 creator reply를 작성한다.
- 요청은 `characterId`와 대상 root `fanTalkId`를 포함한다.
- 대상 FanTalk가 존재하고 활성 상태이며, 대상 creator가 해석된 `creatorMember`와 일치하는 root 글인지 검증한다.
@@ -181,12 +185,23 @@
- 이후 phase의 domain/client/server 오류는 각 task에서 정확한 HTTP status와 KO/EN/JA message key를 먼저 정의하고 같은
envelope를 적용한다.
- 신규 prefix 전용 오류 처리는 legacy/public endpoint의 기존 성공·오류 응답에 적용하지 않는다.
- page 기반 조회는 기존 v2 탭 API 관례를 따라 `page` 기본값 0, `size` 기본값 20, 최소 20, 최대 50 보정을 기본안으로 하며, 경계값 보정은 구현 task와 테스트에 포함한다.
- request/response의 기계 검증 가능한 단일 기준은 같은 디렉터리의 `api-contract.openapi.json`이다. 설명과 레거시 근거는
`api-contract.md`에 기록한다.
- 신규 endpoint는 레거시 API의 request/response 필드명, 타입, optional/nullable, 기본값과 성공 `data` 형태를 그대로
이관한다. `characterId`, `contentId`, `seriesId`, `postId`, `fanTalkId`처럼 신규 path로 이동한 ID만 request body에서
중복 제거한다.
- 캐릭터 수정은 레거시 `ChatCharacterUpdateRequest`처럼 `isActive=false`와 다른 optional field의 동시 입력을 허용하며,
이 경우 레거시 service 의미대로 비활성화만 반영한다.
- 목록 endpoint의 query와 page 동작은 각 레거시 API를 따른다. FanTalk 관리자 목록만 공개 v2 탭의 `page` 기본값 0,
`size` 기본값 20, 최소 20, 최대 50 보정을 따른다.
- multipart 생성/수정은 기존 admin/creator-admin 관례대로 파일 part와 `request` JSON string part를 사용한다.
- `GET /api/v2/admin/ai-characters/audio-content-themes`는 request body 없이 활성 콘텐츠 테마 목록을 반환한다. 성공 응답 `data``[{ "themeId": 11, "themeName": "ASMR", "imageUrl": "https://cdn.example.com/audio-content-theme/asmr.png" }]` 형태이며, 기존 내부/legacy DTO의 `id`, `theme`, `image` 필드명을 외부 계약으로 노출하지 않는다.
- 오디오 콘텐츠 생성 `request` JSON에는 콘텐츠 테마 선택값인 `themeId`와 기존 `CreateAudioContentRequest`의 생성 필드 전체를 포함한다. v2는 `detail` 대신 `description`, `releaseDate` 대신 UTC ISO-8601 `releaseDateUtc`를 외부 계약으로 사용하고 legacy pipeline 호출 시 변환한다.
- 오디오 콘텐츠 목록 응답은 현 v2 관리자 목록 계약을 유지한다. 상세 응답은 기존 `GetAudioContentDetailResponse`의 필드 전체를 v2 상세 DTO에 포함하되, 구매·좋아요·핀·추천·댓글 목록처럼 viewer 상태가 필요한 필드는 관리자 상세에서 안전한 기본값을 반환한다.
- request/response DTO는 신규 v2 AI character admin API 전용 DTO로 두고 legacy/public DTO를 외부 계약으로 재노출하지 않는다.
- 시리즈 연결 콘텐츠 목록과 미연결 콘텐츠 검색은 응답 형태가 다르므로 각각
`GET .../series/{seriesId}/contents``GET .../series/{seriesId}/contents/search?search_word=...`로 분리한다.
- 레거시 mutation이 `ApiResponse.ok(null)`을 반환하면 신규 endpoint도 `data: null`을 반환한다. 오디오 콘텐츠 생성은
레거시 `CreateAudioContentResponse(contentId)`를 유지한다. FanTalk 답변 작성만 신규 계획의 축약 응답
`fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다.
- request/response 구현 DTO는 신규 v2 AI character admin API 전용으로 둘 수 있지만, JSON 외부 계약은
`api-contract.openapi.json`의 레거시 필드명과 형태를 유지한다.
---
@@ -248,6 +263,8 @@
- 콘텐츠 생성/수정/soft delete와 signed URL 응답은 기존 creator/admin behavior parity를 유지한다.
- 시리즈 CRUD, 콘텐츠 연결/해제/순서 변경은 target character 소유 범위를 벗어나지 않는다.
- 커뮤니티 생성/수정/고정/해제/soft delete는 target creatorMember 소유 게시글에만 적용된다.
- FanTalk 관리자 목록은 target AI character의 root 글과 creator reply를 공개 v2 응답 필드명으로 반환하며, 공개 v2의
viewer/block 필터나 creator 관리자 CORS 경계에 의존하지 않는다.
- FanTalk 답변은 target AI character 자신의 활성 root FanTalk에만 저장된다.
- 기존 legacy/public endpoint 테스트가 통과하고, 두 공유 인증 경로의 CORS 허용 Origin 확장 외 성공·오류
status/body/message를 포함한 request/response contract가 변경되지 않는다.