Files
sori.studio/server/utils/admin-post-input.js

66 lines
2.3 KiB
JavaScript

import { z } from 'zod'
import { ADMIN_POST_PLACEHOLDER_TITLE } from '../../lib/admin-post-title.js'
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
import {
isPageCoverThumbnailUrl,
isPostBodyThumbnailUrl,
isPostCardThumbnailUrl
} from '../../lib/post-card-thumbnail-url.js'
import { postStatusSchema } from './content-schema.js'
/**
* 자동 생성 파생 이미지 URL인지 확인한다.
* @param {string|null|undefined} imageUrl - 검사할 이미지 URL
* @returns {boolean} 자동 생성 이미지이면 true
*/
const isGeneratedThumbnailUrl = (imageUrl) => isPostCardThumbnailUrl(imageUrl)
|| isPostBodyThumbnailUrl(imageUrl)
|| isPageCoverThumbnailUrl(imageUrl)
export const adminPostInputSchema = z.object({
title: z.preprocess((val) => {
const t = String(val ?? '').trim()
return t.length ? t : ADMIN_POST_PLACEHOLDER_TITLE
}, z.string().min(1)),
slug: z.string().trim().min(1).regex(/^[a-z0-9가-힣]+(?:[-_][a-z0-9가-힣]+)*$/),
content: z.preprocess(normalizeMarkdownContent, z.string()).default(''),
excerpt: z.string().default(''),
featuredImage: z.string()
.trim()
.nullable()
.default(null)
.refine((value) => !isGeneratedThumbnailUrl(value), '자동 생성 이미지는 대표 이미지로 사용할 수 없습니다.'),
showFeaturedImage: z.boolean().default(false),
isFeatured: z.boolean().default(false),
seoTitle: z.string().trim().default(''),
seoDescription: z.string().trim().default(''),
canonicalUrl: z.string().trim().url().or(z.literal('')).default(''),
noindex: z.boolean().default(false),
ogImage: z.string().trim().nullable().default(null),
status: postStatusSchema.default('draft'),
publishedAt: z.string().datetime().nullable().default(null),
tags: z.array(z.string().trim().min(1)).default([]),
tagNames: z.record(z.string()).default({})
})
/**
* 관리자 게시물 입력값 정리
* @param {unknown} body - 요청 본문
* @returns {import('zod').SafeParseReturnType<unknown, Object>} 검증 결과
*/
export const parseAdminPostInput = (body) => {
const parsed = adminPostInputSchema.safeParse(body)
if (!parsed.success || parsed.data.status !== 'published' || parsed.data.publishedAt) {
return parsed
}
return {
...parsed,
data: {
...parsed.data,
publishedAt: new Date().toISOString()
}
}
}