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 { parseCanPriceInput } from "@/shared/validation/can-price"; export function createInitialPrice(price: number | undefined): string { return String(price ?? 0); } export function parsePrice(value: string): number | null { return parseCanPriceInput(value); } export const selectionCardClassName = "flex min-h-11 cursor-pointer items-center gap-3 rounded-md border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground hover:bg-accent hover:text-accent-foreground focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 has-[:checked]:border-input has-[:checked]:bg-accent has-[:checked]:text-accent-foreground"; 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; 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, }; }