Files

20 KiB

AI 캐릭터 관리자 API Contract

1. 문서 목적

클라이언트 개발과 서버 계약 테스트가 같은 스키마를 사용하도록 AI 캐릭터 관리자 API의 전체 request/response를 OpenAPI 3.1 JSON으로 고정한다.

  • 정식 계약: api-contract.openapi.json
  • endpoint: 37개
  • 현재 route 구현: 37개
  • 현재 계약과 일치하는 구현 완료: 37개
  • 계약 정합화 필요: 0개
  • 구현 예정: 0개
  • 공통 envelope: ApiResponse<T>
  • 인증: JWT ROLE_ADMIN과 현재 DB Member.role == ADMIN 동시 충족

x-implementation-status의 의미는 다음과 같다.

  • implemented: route가 구현되어 있고 확정 계약과 후속 리뷰 Gate를 통과했다.
  • alignment-required: route는 구현되어 있으나 승인된 최신 계약에 맞춘 request/response 정합화가 필요하다.
  • planned: 계약과 구현 Task는 확정됐으나 route가 아직 구현되지 않았다.

2. 계약 결정

  1. 신규 endpoint와 characterId 기반 관리자 target 경계는 유지한다.
  2. JSON 필드명, 타입, optional/nullable, 기본값과 성공 data 형태는 레거시 API를 유지한다.
  3. 신규 path에 포함된 characterId, contentId, seriesId, postId, fanTalkId, replyId만 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로 분리한다.
  9. 시리즈 상세 data는 배열 wrapper 없이 시리즈 목록 items의 단일 객체와 동일한 필드·타입을 사용한다.
  10. 오디오 콘텐츠와 커뮤니티 게시글 댓글의 원댓글/답글 조회는 레거시 응답을 유지하고, 작성은 target AI 캐릭터 명의로 수행한다.
  11. 댓글 수정은 target AI 캐릭터가 작성한 댓글/답글만 허용한다. 삭제는 target 소유 리소스에 달린 댓글/답글이면 작성자와 관계없이 해당 row만 soft delete하며, 이미 비활성인 row의 삭제는 성공 no-op이다.
  12. 댓글 생성의 optional parentId가 없으면 원댓글, 있으면 같은 리소스의 활성 원댓글에 대한 답글이다.
  13. 팬이 작성한 FanTalk 원글 삭제는 target 채널의 root만 soft delete하고 이미 비활성이면 성공 no-op이며 연결된 creator reply row는 유지한다.
  14. FanTalk 답변 수정은 레거시 PutWriteCheersRequest에서 path replyId로 이동한 cheersId만 제거하고 optional/nullable content, isActive, 빈 객체 no-op과 CreatorChannelFanTalkResponse 성공 data를 유지한다.
  15. 캐릭터에 직접 달리는 레거시 댓글 삭제 API는 v2 전환 후 사용하지 않으므로 계약과 구현 범위에서 제외한다.
  16. 캐릭터 등록용 원작 검색과 시리즈 장르 목록은 기존 관리자 조회 규칙과 전체 response 필드를 재사용한다.
  17. AI 캐릭터 관리자 오디오·커뮤니티 API는 timezone query/body를 받지 않는다. 오디오 생성의 nullable releaseDate는 클라이언트가 ISO-8601 UTC(Z)로 변환해 보내며, 오디오 상세 releaseDate와 오디오·커뮤니티 댓글 date도 기존 필드명과 null/노출 조건을 유지한 채 ISO-8601 UTC(Z)로 반환한다.

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/original-works/search 필수 searchTerm List<OriginalWorkResponse>
구현 완료 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 timezone을 제외하고 nullable UTC releaseDate를 받는 AudioContentCreateRequest, contentFile, coverImage CreateAudioContentResponse
구현 완료 GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId} path only releaseDate가 nullable UTC인 GetAudioContentDetailResponse
구현 완료 PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId} UpdateCreatorAdminContentRequest에서 id 제외 null
구현 완료 GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments page, size 댓글 date가 UTC인 GetAudioContentCommentListResponse
구현 완료 POST /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments comment, parentId?, isSecret?, languageCode? null
구현 완료 PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId} comment null
구현 완료 DELETE /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId} body 없음 null
구현 완료 GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}/comments/{commentId}/replies page, size 댓글 date가 UTC인 GetAudioContentCommentListResponse
구현 완료 GET /api/v2/admin/ai-characters/series-genres body 없음 List<GetSeriesGenreListResponse>
구현 완료 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 시리즈 목록 items 단일 객체
구현 완료 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 page, size AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)
구현 완료 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}/community-posts/{postId}/comments page, size 댓글 date가 UTC인 GetCommunityPostCommentListResponse
구현 완료 POST /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments comment, parentId?, isSecret? null
구현 완료 PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId} comment null
구현 완료 DELETE /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId} body 없음 null
구현 완료 GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}/comments/{commentId}/replies page, size 댓글 date가 UTC인 GetCommunityPostCommentListResponse
구현 완료 GET /api/v2/admin/ai-characters/{characterId}/fan-talks 공개 v2 page?, size? CreatorChannelFanTalkTabResponse
구현 완료 DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId} body 없음 null
구현 완료 POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies content 신규 축약 응답
구현 완료 PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId} PutWriteCheersRequest에서 cheersId 제외: content?, isActive? CreatorChannelFanTalkResponse

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다.
  • 승인된 UTC 예외인 오디오 생성·상세 releaseDate, 오디오·커뮤니티 댓글 date, FanTalk createdAtUtc는 OpenAPI date-time이다. 그 밖의 레거시 날짜 문자열은 기존 format을 유지한다.
  • 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, OriginalWorkResponse
  • 목록 response는 totalCount, content이며 items/page/size/hasNext로 바꾸지 않는다.
  • 상세에는 systemPrompt, 캐릭터 속성 배열과 originalWork를 포함한다.
  • 생성과 수정의 성공 data는 상세가 아니라 null이다.
  • 캐릭터 등록용 원작 검색은 필수 searchTerm으로 제목·콘텐츠 타입·카테고리를 부분 검색하고, soft delete 원작을 제외한 OriginalWorkResponse 전체 필드의 직접 배열을 반환한다. 별도 pagination은 두지 않는다.
  • 수정은 isActive=false와 다른 optional field의 동시 입력도 레거시 request처럼 허용한다. 이 경우 레거시 service 의미대로 비활성화만 반영하고 나머지 JSON field는 적용하지 않는다.

AudioContent

  • request: AudioContentCreateRequest, UpdateCreatorAdminContentRequest, RegisterCommentRequest, ModifyCommentRequest
  • 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 별칭은 정식 계약에 포함하지 않는다.
  • 생성 request는 timezone을 받지 않으며 nullable releaseDate를 ISO-8601 UTC(Z)로 받는다. 로컬 시각과 timezone을 함께 받는 레거시 생성 형식은 신규 관리자 endpoint에서 지원하지 않는다.
  • 상세 response의 nullable releaseDate는 기존 null/노출 조건을 유지하고, 값이 있으면 ISO-8601 UTC(Z)로 반환한다.
  • 댓글/답글 목록은 timezone 없이 page, size와 레거시 totalCount, items를 유지하고, 각 date를 ISO-8601 UTC(Z)로 반환한다.
  • 댓글 생성은 target AI 캐릭터 명의로 수행한다. parentId는 optional/nullable, isSecret 기본값은 false, languageCode는 optional/nullable이다.
  • 댓글 수정 request는 comment만 받으며 target AI 캐릭터가 작성한 활성 댓글/답글만 수정한다.
  • 삭제는 target 소유 활성 오디오 콘텐츠의 댓글/답글 row 하나만 isActive=false로 변경한다. 하위 답글을 cascade 삭제하지 않으며 mutation 성공 datanull이다.

Series

  • request: CreateSeriesRequest, ModifySeriesRequest, AddingContentToTheSeriesRequest, RemoveContentToTheSeriesRequest, UpdateOrdersRequest
  • response: GetCreatorAdminContentSeriesListResponse, GetCreatorAdminContentSeriesContentResponse, SearchContentNotInSeriesResponse, GetSeriesGenreListResponse
  • publishedDaysOfWeek enum은 SUN..SAT, RANDOM이며 state는 PROCEEDING, SUSPEND, COMPLETE다.
  • 상세 data는 목록 items의 단일 객체와 동일하게 seriesId, title, introduction, coverImageUrl, publishedDaysOfWeek, genreId, isAdult, state, isActive, writer, studio를 반환한다. 별도 genre, keywords는 반환하지 않는다.
  • 등록용 장르 목록은 활성 장르를 orders 오름차순으로 조회하고 id, genre, isAdult의 직접 배열을 반환한다.
  • 연결 request는 contentIdList, 순서 request는 ids다.

Community

  • request: CreateCommunityPostRequest, ModifyCommunityPostRequest, UpdateCommunityPostFixedRequest, CreateCommunityPostCommentRequest, ModifyCommunityPostCommentRequest
  • response: GetCommunityPostListResponse, GetCommunityPostCommentListResponse, GetCommunityPostCommentListItem
  • 2026-07-29 사용자 확정에 따라 목록은 레거시 직접 배열의 예외다. timezone query를 제거하고 dataAiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)로 반환한다.
  • totalCount는 target creatorMember 소유 active 게시글 전체 개수이고, items는 기존 GetCommunityPostListResponse 필드·고정 우선 정렬을 유지한다. hasNext는 현재 page 뒤에 active owner 게시글이 더 있는지를 나타낸다.
  • 생성 part 이름은 postImage, audioFile이다.
  • 수정 endpoint는 기존 본문 수정과 고정/해제를 합치므로 ID를 제외한 content, isCommentAvailable, isAdult, isActive, isFixed를 받는다.
  • 레거시에 없는 수정 price, audioFile은 포함하지 않는다.
  • 댓글/답글 목록은 timezone 없이 page, size와 레거시 totalCount, items를 유지하고, 각 date를 ISO-8601 UTC(Z)로 반환한다.
  • 댓글 생성은 target AI 캐릭터 명의로 수행하고 optional/nullable parentId와 기본값 falseisSecret을 받는다.
  • 댓글 수정 request는 comment만 받으며 target AI 캐릭터가 작성한 활성 댓글/답글만 수정한다.
  • 삭제는 target 소유 활성 게시글의 댓글/답글 row 하나만 isActive=false로 변경한다. 하위 답글을 cascade 삭제하지 않으며 mutation 성공 datanull이다.

FanTalk

  • 목록 response: CreatorChannelFanTalkTabResponse, CreatorChannelFanTalkResponse, CreatorChannelFanTalkReplyResponse
  • 공개 v2 endpoint를 직접 사용하지 않는다. 공개 v2는 creatorId, viewer 인증과 block filter, 다른 CORS 경계를 사용하기 때문이다.
  • 신규 관리자 목록은 characterId로 creator를 해석하고 공개 v2 response field만 유지한다.
  • 답변 작성 request는 path로 이동한 creatorId, parentId를 제외하고 content만 받는다.
  • 답변 작성 response는 사용자 승인 예외인 신규 축약 형태다.
  • 답변 수정 request는 레거시 PutWriteCheersRequest에서 path로 이동한 cheersId만 제외하고 optional/nullable content, isActive를 받는다. 두 필드를 함께 입력할 수 있고, 모두 생략하거나 null이면 성공 no-op이다.
  • 답변 수정 대상은 target AI가 writer이자 creator이고 path의 활성 root에 직접 연결된 reply로 한정한다. reply 자체가 비활성이어도 isActive=true로 재활성화할 수 있으며, 다른 target/root·팬 작성 row·nested mismatch는 400이다.
  • 답변 수정은 non-null field만 반영하고 기존 languageCode를 변경하거나 언어 감지·이벤트를 발생시키지 않는다.
  • 답변 수정 response data는 레거시 CreatorChannelFanTalkResponse 필드 형태다. fanTalkId는 수정한 reply row ID이고 creatorReplies는 빈 배열이다.
  • 삭제는 target 채널에 속한 팬 작성 원글만 허용하고 해당 root row만 isActive=false로 변경한다. 연결된 creator reply row는 유지되며 목록에서 root가 제외되므로 함께 노출되지 않는다.
  • 이미 비활성인 원글 삭제는 성공 no-op이고 성공 datanull이다.

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만 제외하는 이유는 저장소 라이선스 값을 추측해 계약에 추가하지 않기 위해서다. 생성 클라이언트에는 계약의 37개 operation이 모두 포함된다. 37개 모두 실제 route가 구현되어 있고 implemented 상태다.

OpenAPI의 / server URL은 현재 host를 의미하지만 OpenAPI Generator 7.24.0의 typescript-fetch runtime 기본값은 http://localhost다. 실제 클라이언트는 배포 환경의 API origin을 new Configuration({ basePath: "..." })로 반드시 지정한다.