feat(ai-character): 오디오 콘텐츠 관리 기능 구현
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import type { ReleaseScheduleValue } from "@/features/audio-contents/components/ReleaseScheduleField";
|
||||
import type { AudioContentDetail } from "@/features/audio-contents/model/types";
|
||||
import type { AudioContentCreateRequest, AudioContentUpdateRequest } from "@/features/audio-contents/schemas/audio-content-schema";
|
||||
import { validateAudioCoverFile } from "@/features/audio-contents/validation/audio-cover-policy";
|
||||
import { validateAudioFile } from "@/shared/validation/audio-file-policy";
|
||||
import { formatCanPriceInput, parseCanPriceInput } from "@/shared/validation/can-price";
|
||||
|
||||
export function createInitialPrice(price: number | undefined): string {
|
||||
return price === undefined ? "" : `${price.toLocaleString("ko-KR")}캔`;
|
||||
}
|
||||
|
||||
export function parsePrice(value: string): number | null {
|
||||
return parseCanPriceInput(value);
|
||||
}
|
||||
|
||||
export function formatPrice(value: string): string {
|
||||
return formatCanPriceInput(value);
|
||||
}
|
||||
|
||||
const localDateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/;
|
||||
|
||||
function toUtcMilliseconds(value: string, offsetHours: number): number {
|
||||
if (!localDateTimePattern.test(value)) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
return Date.UTC(Number(value.slice(0, 4)), Number(value.slice(5, 7)) - 1, Number(value.slice(8, 10)), Number(value.slice(11, 13)) - offsetHours, Number(value.slice(14, 16)));
|
||||
}
|
||||
|
||||
export function toUtcReleaseDate(value: string, timezone: "Asia/Seoul"): string {
|
||||
const offsetHours = timezone === "Asia/Seoul" ? 9 : 0;
|
||||
|
||||
return new Date(toUtcMilliseconds(value, offsetHours)).toISOString().replace(".000Z", "Z");
|
||||
}
|
||||
|
||||
export function isFutureLocalDateTime(value: string): boolean {
|
||||
const selected = toUtcMilliseconds(value, 9);
|
||||
|
||||
return Number.isFinite(selected) && selected > Date.now();
|
||||
}
|
||||
|
||||
export type AudioContentCreateSettings = Pick<AudioContentCreateRequest, "purchaseOption" | "limited" | "isAdult" | "isGeneratePreview" | "isOnlyRental" | "isPointAvailable" | "isCommentAvailable" | "isFullDetailVisible" | "previewStartTime" | "previewEndTime" | "languageCode">;
|
||||
|
||||
export const defaultAudioContentCreateSettings = {
|
||||
purchaseOption: "BOTH",
|
||||
limited: null,
|
||||
isAdult: false,
|
||||
isGeneratePreview: false,
|
||||
isOnlyRental: false,
|
||||
isPointAvailable: false,
|
||||
isCommentAvailable: false,
|
||||
isFullDetailVisible: true,
|
||||
previewStartTime: null,
|
||||
previewEndTime: null,
|
||||
languageCode: null,
|
||||
} satisfies AudioContentCreateSettings;
|
||||
|
||||
export function audioErrorMessage(file: File | null, mode: "create" | "edit"): string | undefined {
|
||||
if (mode === "edit") {
|
||||
return undefined;
|
||||
}
|
||||
if (file === null) {
|
||||
return "오디오 파일을 선택하세요.";
|
||||
}
|
||||
const result = validateAudioFile(file);
|
||||
if (result.ok) {
|
||||
return undefined;
|
||||
}
|
||||
if (result.reason === "size") {
|
||||
return "오디오 파일은 1,024,000,000 bytes 이하만 업로드할 수 있습니다.";
|
||||
}
|
||||
if (result.reason === "mimeExtensionCombination") {
|
||||
return "확장자와 MIME 조합이 올바른 MP3, AAC, M4A 파일만 업로드하세요.";
|
||||
}
|
||||
|
||||
return "MP3, AAC, M4A 파일만 업로드하세요.";
|
||||
}
|
||||
|
||||
export function coverErrorMessage(file: File | null, mode: "create" | "edit"): string | undefined {
|
||||
if (file === null) {
|
||||
return mode === "create" ? "커버 이미지를 선택하세요." : undefined;
|
||||
}
|
||||
const result = validateAudioCoverFile(file);
|
||||
if (result.ok) {
|
||||
return undefined;
|
||||
}
|
||||
if (result.reason === "size") {
|
||||
return "커버 이미지는 10MB 이하만 업로드할 수 있습니다.";
|
||||
}
|
||||
|
||||
return "JPEG 또는 PNG 파일만 업로드하세요.";
|
||||
}
|
||||
|
||||
export function toCreateRequest(params: { readonly detail: string; readonly price: number; readonly releaseSchedule: ReleaseScheduleValue; readonly settings: AudioContentCreateSettings; readonly tags: string; readonly themeId: number; readonly title: string }): AudioContentCreateRequest {
|
||||
return {
|
||||
title: params.title,
|
||||
detail: params.detail,
|
||||
tags: params.tags,
|
||||
price: params.price,
|
||||
purchaseOption: params.settings.purchaseOption,
|
||||
limited: params.settings.limited,
|
||||
releaseDate: params.releaseSchedule.publishMode === "immediate" ? null : toUtcReleaseDate(params.releaseSchedule.releaseDateTime, "Asia/Seoul"),
|
||||
themeId: params.themeId,
|
||||
isAdult: params.settings.isAdult,
|
||||
isGeneratePreview: params.settings.isGeneratePreview,
|
||||
isOnlyRental: params.settings.isOnlyRental,
|
||||
isPointAvailable: params.settings.isPointAvailable,
|
||||
isCommentAvailable: params.settings.isCommentAvailable,
|
||||
isFullDetailVisible: params.settings.isFullDetailVisible,
|
||||
previewStartTime: params.settings.previewStartTime,
|
||||
previewEndTime: params.settings.previewEndTime,
|
||||
languageCode: params.settings.languageCode,
|
||||
};
|
||||
}
|
||||
|
||||
export function toUpdateRequest(params: { readonly detail: string; readonly price: number; readonly tags: string; readonly title: string; readonly audio: AudioContentDetail }): AudioContentUpdateRequest {
|
||||
return {
|
||||
title: params.title,
|
||||
detail: params.detail,
|
||||
tags: params.tags,
|
||||
price: params.price,
|
||||
isAdult: params.audio.isAdult,
|
||||
isPointAvailable: params.audio.isAvailableUsePoint,
|
||||
isCommentAvailable: params.audio.isCommentAvailable,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user