12 KiB
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과 현재 DBMember.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. 계약 결정
- 신규 endpoint와
characterId기반 관리자 target 경계는 유지한다. - JSON 필드명, 타입, optional/nullable, 기본값과 성공
data형태는 레거시 API를 유지한다. - 신규 path에 포함된
characterId,contentId,seriesId,postId,fanTalkId만 request body에서 중복 제거한다. - 레거시 mutation이
ApiResponse.ok(null)이면 신규 endpoint도data: null을 반환한다. - 오디오 콘텐츠 생성은 레거시
CreateAudioContentResponse(contentId)를 반환한다. - FanTalk 답변 작성만 신규 계획의 축약 응답
fanTalkId,replyId,creatorMemberId,content,createdAtUtc를 사용한다. - FanTalk 목록은 공개 v2
CreatorChannelFanTalkTabResponse의 필드 형태를 유지하는 관리자 전용 endpoint로 추가한다. - 시리즈 연결 콘텐츠 목록과 미연결 콘텐츠 검색은 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 수정:
imageoptional,request필수 - AudioContent 생성:
contentFile,coverImage,request필수 - AudioContent 수정:
coverImageoptional,request필수 - Series 생성:
image,request필수 - Series 수정:
imageoptional,request필수 - Community 생성:
audioFile,postImageoptional,request필수 - Community 수정/고정:
postImageoptional,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 publishedDaysOfWeekenum은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. 공통 응답과 오류
일반 조회 성공:
{
"success": true,
"message": null,
"data": {},
"errorProperty": null
}
레거시 mutation 성공:
{
"success": true,
"message": null,
"data": null,
"errorProperty": null
}
오류:
{
"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. 기계 검증과 클라이언트 생성
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: "..." })로 반드시
지정한다.