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

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과 현재 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 createdAtUtcdate-time이다.
  • enum은 대소문자를 구분하며 Kotlin enum 이름을 그대로 사용한다.
  • Accept-Languageko, 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. 공통 응답과 오류

일반 조회 성공:

{
  "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: "..." })로 반드시 지정한다.