297 lines
20 KiB
Markdown
297 lines
20 KiB
Markdown
# 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-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`, `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 성공 `data`는 `null`이다.
|
|
|
|
### 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를 제거하고 `data`를
|
|
`AiCharacterAdminCommunityPostListResponse(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`와 기본값 `false`인 `isSecret`을
|
|
받는다.
|
|
- 댓글 수정 request는 `comment`만 받으며 target AI 캐릭터가 작성한 활성 댓글/답글만 수정한다.
|
|
- 삭제는 target 소유 활성 게시글의 댓글/답글 row 하나만 `isActive=false`로 변경한다. 하위 답글을 cascade
|
|
삭제하지 않으며 mutation 성공 `data`는 `null`이다.
|
|
|
|
### 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이고 성공 `data`는 `null`이다.
|
|
|
|
## 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`만 제외하는 이유는 저장소 라이선스 값을 추측해 계약에 추가하지 않기 위해서다.
|
|
생성 클라이언트에는 계약의 37개 operation이 모두 포함된다. 37개 모두 실제 route가 구현되어 있고 `implemented` 상태다.
|
|
|
|
OpenAPI의 `/` server URL은 현재 host를 의미하지만 OpenAPI Generator 7.24.0의 `typescript-fetch` runtime 기본값은
|
|
`http://localhost`다. 실제 클라이언트는 배포 환경의 API origin을 `new Configuration({ basePath: "..." })`로 반드시
|
|
지정한다.
|