230 lines
12 KiB
Markdown
230 lines
12 KiB
Markdown
# 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: "..." })`로 반드시
|
|
지정한다.
|