# AI 캐릭터 관리자 API Contract ## 1. 문서 목적 클라이언트 개발과 서버 계약 테스트가 같은 스키마를 사용하도록 AI 캐릭터 관리자 API의 전체 request/response를 OpenAPI 3.1 JSON으로 고정한다. - 정식 계약: `api-contract.openapi.json` - endpoint: 23개 - 현재 route 구현: 9개 - 현재 구현 중 계약 정합화 필요: 9개 - 구현 예정: 14개 - 공통 envelope: `ApiResponse` - 인증: 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` | | 정합화 필요 | 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` | | 계획 | 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` | | 계획 | 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: "..." })`로 반드시 지정한다.