글쓰기 태그 제한과 표 기능 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 17:10:16 +09:00
parent ed30926250
commit 95d234a625
24 changed files with 560 additions and 54 deletions

View File

@@ -20,6 +20,7 @@ import {
normalizeSignupBlockedUsernames,
parseSignupBlockedUsernamesFromDb
} from '../../lib/signup-blocked-usernames.js'
import { normalizePostTagLimit } from '../../lib/post-tag-limit.js'
import { normalizeSocialLinks } from '../../lib/social-links.js'
import {
createPostThumbnailForImageUrl,
@@ -116,6 +117,7 @@ const mapSiteSettingsRow = (row) => ({
copyrightText: row.copyright_text,
socialLinks: normalizeSocialLinks(row.social_links),
showPostUpdatedAt: Boolean(row.show_post_updated_at),
postTagLimit: normalizePostTagLimit(row.post_tag_limit),
homeCoverImageUrl: row.home_cover_image_url || '',
homeCoverDarkImageUrl: row.home_cover_dark_image_url || '',
homeCoverTitle: row.home_cover_title || '',
@@ -903,6 +905,7 @@ export const updateSiteSettings = async (input) => {
copyright_text,
social_links,
show_post_updated_at,
post_tag_limit,
home_cover_image_url,
home_cover_dark_image_url,
home_cover_title,
@@ -937,6 +940,7 @@ export const updateSiteSettings = async (input) => {
${input.copyrightText},
${JSON.stringify(normalizeSocialLinks(input.socialLinks))}::jsonb,
${input.showPostUpdatedAt ? true : false},
${normalizePostTagLimit(input.postTagLimit)},
${input.homeCoverImageUrl || ''},
${input.homeCoverDarkImageUrl || ''},
${input.homeCoverTitle || ''},
@@ -971,6 +975,7 @@ export const updateSiteSettings = async (input) => {
copyright_text = EXCLUDED.copyright_text,
social_links = EXCLUDED.social_links,
show_post_updated_at = EXCLUDED.show_post_updated_at,
post_tag_limit = EXCLUDED.post_tag_limit,
home_cover_image_url = EXCLUDED.home_cover_image_url,
home_cover_dark_image_url = EXCLUDED.home_cover_dark_image_url,
home_cover_title = EXCLUDED.home_cover_title,

View File

@@ -1,7 +1,7 @@
import { createError, readBody } from 'h3'
import { requireAdminSession } from '../../../utils/admin-auth'
import { parseAdminPostInput } from '../../../utils/admin-post-input'
import { createAdminPost } from '../../../repositories/content-repository'
import { createAdminPost, getSiteSettings } from '../../../repositories/content-repository'
/**
* 관리자 게시물 생성 API
@@ -20,6 +20,16 @@ export default defineEventHandler(async (event) => {
})
}
const settings = await getSiteSettings()
const postTagLimit = Number(settings.postTagLimit || 5)
if (parsedBody.data.tags.length > postTagLimit) {
throw createError({
statusCode: 400,
message: `태그는 최대 ${postTagLimit}개까지 선택할 수 있습니다.`
})
}
try {
return await createAdminPost(parsedBody.data, adminSession.userId)
} catch (error) {

View File

@@ -1,7 +1,7 @@
import { createError, getRouterParam, readBody } from 'h3'
import { requireAdminSession } from '../../../../utils/admin-auth'
import { parseAdminPostInput } from '../../../../utils/admin-post-input'
import { updateAdminPost } from '../../../../repositories/content-repository'
import { getSiteSettings, updateAdminPost } from '../../../../repositories/content-repository'
/**
* 관리자 게시물 수정 API
@@ -21,6 +21,16 @@ export default defineEventHandler(async (event) => {
})
}
const settings = await getSiteSettings()
const postTagLimit = Number(settings.postTagLimit || 5)
if (parsedBody.data.tags.length > postTagLimit) {
throw createError({
statusCode: 400,
message: `태그는 최대 ${postTagLimit}개까지 선택할 수 있습니다.`
})
}
try {
const post = await updateAdminPost(id, parsedBody.data, adminSession.userId)

View File

@@ -19,6 +19,12 @@ import {
normalizeSignupBlockedUsernames
} from '../../lib/signup-blocked-usernames.js'
import { normalizeSocialLinks } from '../../lib/social-links.js'
import {
DEFAULT_POST_TAG_LIMIT,
MAX_POST_TAG_LIMIT,
MIN_POST_TAG_LIMIT,
normalizePostTagLimit
} from '../../lib/post-tag-limit.js'
export const adminSiteSettingsInputSchema = z.object({
title: z.string().trim().min(1),
@@ -30,6 +36,7 @@ export const adminSiteSettingsInputSchema = z.object({
copyrightText: z.string().trim().min(1),
socialLinks: z.unknown().optional().default([]),
showPostUpdatedAt: z.boolean().optional().default(false),
postTagLimit: z.coerce.number().int().min(MIN_POST_TAG_LIMIT).max(MAX_POST_TAG_LIMIT).optional().default(DEFAULT_POST_TAG_LIMIT),
homeCoverImageUrl: z.string().trim().max(500).optional().default(''),
homeCoverDarkImageUrl: z.string().trim().max(500).optional().default(''),
homeCoverTitle: z.string().trim().max(120).optional().default(''),
@@ -81,6 +88,7 @@ export const adminSiteSettingsInputSchema = z.object({
...data,
brandColor: normalizeBrandColor(data.brandColor),
socialLinks: normalizeSocialLinks(data.socialLinks),
postTagLimit: normalizePostTagLimit(data.postTagLimit),
announcementUrl: normalizeAnnouncementUrl(data.announcementUrl),
announcementBackgroundColor: normalizeAnnouncementBackgroundColor(data.announcementBackgroundColor),
announcementAlignment: normalizeAnnouncementAlignment(data.announcementAlignment),

View File

@@ -4,6 +4,7 @@ import {
DEFAULT_ANNOUNCEMENT_BACKGROUND_COLOR
} from '../../lib/announcement-bar.js'
import { DEFAULT_SIGNUP_BLOCKED_USERNAMES } from '../../lib/signup-blocked-usernames.js'
import { DEFAULT_POST_TAG_LIMIT } from '../../lib/post-tag-limit.js'
/**
* 기본 사이트 설정 반환
@@ -23,6 +24,7 @@ export const getDefaultSiteSettings = () => {
copyrightText: `©${new Date().getFullYear()} ${title}`,
socialLinks: [],
showPostUpdatedAt: false,
postTagLimit: DEFAULT_POST_TAG_LIMIT,
homeCoverImageUrl: '',
homeCoverDarkImageUrl: '',
homeCoverTitle: '',