feat(ai-character): 오디오 콘텐츠 관리 기능 구현

This commit is contained in:
Yu Sung
2026-08-01 01:30:36 +09:00
parent 286c536ba7
commit f9ed4d0094
26 changed files with 3600 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
import type { AudioContentDetail, AudioContentListItem } from "@/features/audio-contents/model/types";
import { audioContentDetailSchema, audioContentListResponseSchema } from "@/features/audio-contents/model/types";
import { audioContentCreateRequestSchema, audioContentCreateResponseSchema, audioContentDeactivateRequestSchema, audioContentUpdateRequestSchema } from "@/features/audio-contents/schemas/audio-content-schema";
import type { AudioContentCreateRequest, AudioContentCreateResponse, AudioContentDeactivateRequest, AudioContentUpdateRequest } from "@/features/audio-contents/schemas/audio-content-schema";
import type { ApiClient } from "@/shared/api/client";
import type { PageData } from "@/shared/api/pagination";
import { z } from "zod";
export type GetAudioContentsParams = {
readonly characterId: string;
readonly page?: number;
readonly search_word?: string;
readonly size?: number;
};
export type GetAudioContentParams = {
readonly characterId: string;
readonly contentId: string;
};
export type CreateAudioContentParams = {
readonly characterId: string;
readonly contentFile: File;
readonly coverImage: File;
readonly request: AudioContentCreateRequest;
};
export type UpdateAudioContentParams = {
readonly coverImage?: File;
readonly request: AudioContentUpdateRequest;
};
const nullSuccessSchema = z.null();
function createAudioContentFormData(contentFile: File | undefined, coverImage: File | undefined, request: object): FormData {
const body = new FormData();
if (contentFile !== undefined) {
body.append("contentFile", contentFile);
}
if (coverImage !== undefined) {
body.append("coverImage", coverImage);
}
body.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
return body;
}
export function createAudioContentCreateBody(params: CreateAudioContentParams): FormData {
return createAudioContentFormData(params.contentFile, params.coverImage, audioContentCreateRequestSchema.parse(params.request));
}
export function createAudioContentUpdateBody(params: UpdateAudioContentParams): FormData {
return createAudioContentFormData(undefined, params.coverImage, audioContentUpdateRequestSchema.parse(params.request));
}
export function createAudioContentDeactivateBody(request: AudioContentDeactivateRequest): FormData {
return createAudioContentFormData(undefined, undefined, audioContentDeactivateRequestSchema.parse(request));
}
function normalizePage(value: number | undefined): number {
return Math.max(0, Math.trunc(value ?? 0));
}
function normalizeSize(value: number | undefined): number {
return Math.max(1, Math.trunc(value ?? 20));
}
export async function getAudioContents(apiClient: ApiClient, params: GetAudioContentsParams): Promise<PageData<AudioContentListItem>> {
const page = normalizePage(params.page);
const size = normalizeSize(params.size);
const query = new URLSearchParams({ page: String(page), size: String(size) });
const searchWord = params.search_word?.trim();
if (searchWord !== undefined && searchWord.length >= 2) {
query.set("search_word", searchWord);
}
const response = await apiClient.request({
path: `/api/v2/admin/ai-characters/${encodeURIComponent(params.characterId)}/audio-contents?${query.toString()}`,
responseSchema: audioContentListResponseSchema,
authentication: "required",
});
return {
totalCount: response.totalCount,
page,
size,
hasNext: (page + 1) * size < response.totalCount,
items: response.items,
};
}
export function getAudioContent(apiClient: ApiClient, params: GetAudioContentParams): Promise<AudioContentDetail> {
return apiClient.request({
path: `/api/v2/admin/ai-characters/${encodeURIComponent(params.characterId)}/audio-contents/${encodeURIComponent(params.contentId)}`,
responseSchema: audioContentDetailSchema,
authentication: "required",
});
}
export function createAudioContent(apiClient: ApiClient, params: CreateAudioContentParams): Promise<AudioContentCreateResponse> {
return apiClient.request({
path: `/api/v2/admin/ai-characters/${encodeURIComponent(params.characterId)}/audio-contents`,
method: "POST",
body: createAudioContentCreateBody(params),
responseSchema: audioContentCreateResponseSchema,
authentication: "required",
});
}
export function updateAudioContent(apiClient: ApiClient, characterId: string, contentId: string, params: UpdateAudioContentParams): Promise<null> {
return apiClient.request({
path: `/api/v2/admin/ai-characters/${encodeURIComponent(characterId)}/audio-contents/${encodeURIComponent(contentId)}`,
method: "PUT",
body: createAudioContentUpdateBody(params),
responseSchema: nullSuccessSchema,
authentication: "required",
});
}
export function deactivateAudioContent(apiClient: ApiClient, characterId: string, contentId: string): Promise<null> {
return apiClient.request({
path: `/api/v2/admin/ai-characters/${encodeURIComponent(characterId)}/audio-contents/${encodeURIComponent(contentId)}`,
method: "PUT",
body: createAudioContentDeactivateBody({ isActive: false }),
responseSchema: nullSuccessSchema,
authentication: "required",
});
}