v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.

세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 11:05:40 +09:00
parent 9bc0f0a90e
commit de767095ba
116 changed files with 8128 additions and 2933 deletions

View File

@@ -1340,52 +1340,61 @@ const uploadLogo = async (event) => {
/**
* 사이트 설정 PUT 요청 본문을 구성한다.
* @param {Array<string>|null} [fields] - 포함할 필드 목록
* @returns {Object} API 본문
*/
const buildSiteSettingsPayload = () => ({
title: form.title,
description: form.description,
siteUrl: form.siteUrl,
logoText: form.logoText || '井',
logoUrl: form.logoUrl,
faviconUrl: form.faviconUrl,
copyrightText: form.copyrightText,
socialLinks: normalizeSocialLinks(form.socialLinks),
showPostUpdatedAt: Boolean(form.showPostUpdatedAt),
postTagLimit: normalizePostTagLimit(form.postTagLimit),
homeLatestPostLimit: normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT),
postListPageSize: normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE),
tagPostListPageSize: normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE),
featuredPostLimit: normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT),
pageSectionItemLimit: normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT),
uploadMaxFileCount: normalizeUploadMaxFileCount(form.uploadMaxFileCount),
uploadMaxTotalSize: normalizeUploadMaxTotalSize(form.uploadMaxTotalSize),
homeCoverImageUrl: form.homeCoverImageUrl || '',
homeCoverDarkImageUrl: form.homeCoverDarkImageUrl || '',
homeCoverTitle: form.homeCoverTitle || '',
homeCoverText: form.homeCoverText || '',
brandColor: normalizeBrandColor(form.brandColor || DEFAULT_BRAND_COLOR),
announcementEnabled: Boolean(form.announcementEnabled),
announcementText: form.announcementText || '',
announcementUrl: form.announcementUrl || '',
announcementBackgroundColor: normalizeAnnouncementBackgroundColor(form.announcementBackgroundColor || DEFAULT_ANNOUNCEMENT_BACKGROUND_COLOR),
announcementAlignment: normalizeAnnouncementAlignment(form.announcementAlignment || DEFAULT_ANNOUNCEMENT_ALIGNMENT),
signupBlockedUsernames: normalizeSignupBlockedUsernames(form.signupBlockedUsernames),
adsTxt: form.adsTxt || '',
customHeadCode: form.customHeadCode || '',
customFooterCode: form.customFooterCode || '',
adHomeFeedCode: form.adHomeFeedCode || '',
adHomeInfeedCode: form.adHomeInfeedCode || '',
adSidebarCode: form.adSidebarCode || '',
adPostSidebarCode: form.adPostSidebarCode || '',
adPostTopCode: form.adPostTopCode || '',
adPostInArticleCode: form.adPostInArticleCode || '',
adPostBottomCode: form.adPostBottomCode || ''
})
const buildSiteSettingsPayload = (fields = null) => {
const payload = {
title: form.title,
description: form.description,
siteUrl: form.siteUrl,
logoText: form.logoText || '井',
logoUrl: form.logoUrl,
faviconUrl: form.faviconUrl,
copyrightText: form.copyrightText,
socialLinks: normalizeSocialLinks(form.socialLinks),
showPostUpdatedAt: Boolean(form.showPostUpdatedAt),
postTagLimit: normalizePostTagLimit(form.postTagLimit),
homeLatestPostLimit: normalizeDisplayCount(form.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT),
postListPageSize: normalizeDisplayCount(form.postListPageSize, DEFAULT_POST_LIST_PAGE_SIZE),
tagPostListPageSize: normalizeDisplayCount(form.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE),
featuredPostLimit: normalizeDisplayCount(form.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT),
pageSectionItemLimit: normalizeDisplayCount(form.pageSectionItemLimit, DEFAULT_PAGE_SECTION_ITEM_LIMIT),
uploadMaxFileCount: normalizeUploadMaxFileCount(form.uploadMaxFileCount),
uploadMaxTotalSize: normalizeUploadMaxTotalSize(form.uploadMaxTotalSize),
homeCoverImageUrl: form.homeCoverImageUrl || '',
homeCoverDarkImageUrl: form.homeCoverDarkImageUrl || '',
homeCoverTitle: form.homeCoverTitle || '',
homeCoverText: form.homeCoverText || '',
brandColor: normalizeBrandColor(form.brandColor || DEFAULT_BRAND_COLOR),
announcementEnabled: Boolean(form.announcementEnabled),
announcementText: form.announcementText || '',
announcementUrl: form.announcementUrl || '',
announcementBackgroundColor: normalizeAnnouncementBackgroundColor(form.announcementBackgroundColor || DEFAULT_ANNOUNCEMENT_BACKGROUND_COLOR),
announcementAlignment: normalizeAnnouncementAlignment(form.announcementAlignment || DEFAULT_ANNOUNCEMENT_ALIGNMENT),
signupBlockedUsernames: normalizeSignupBlockedUsernames(form.signupBlockedUsernames),
adsTxt: form.adsTxt || '',
customHeadCode: form.customHeadCode || '',
customFooterCode: form.customFooterCode || '',
adHomeFeedCode: form.adHomeFeedCode || '',
adHomeInfeedCode: form.adHomeInfeedCode || '',
adSidebarCode: form.adSidebarCode || '',
adPostSidebarCode: form.adPostSidebarCode || '',
adPostTopCode: form.adPostTopCode || '',
adPostInArticleCode: form.adPostInArticleCode || '',
adPostBottomCode: form.adPostBottomCode || ''
}
if (!Array.isArray(fields)) {
return payload
}
return Object.fromEntries(fields.map((field) => [field, payload[field]]))
}
/**
* 현재 폼 값으로 사이트 설정을 서버에 저장한다.
* @param {{ successToast?: string, savingFlag: import('vue').Ref<boolean> }} options - 저장 옵션
* @param {{ successToast?: string, savingFlag: import('vue').Ref<boolean>, fields: Array<string> }} options - 저장 옵션
* @returns {Promise<boolean>} 성공 여부
*/
const persistSiteSettings = async (options) => {
@@ -1395,11 +1404,14 @@ const persistSiteSettings = async (options) => {
showToast('info', '설정을 저장하는 중입니다.')
try {
const payload = buildSiteSettingsPayload(options.fields)
const updatedSettings = await $fetch('/admin/api/settings', {
method: 'PUT',
body: buildSiteSettingsPayload()
method: 'PATCH',
body: payload
})
options.fields.forEach((field) => {
form[field] = updatedSettings[field]
})
Object.assign(form, updatedSettings)
showToast('success', successToast)
return true
} catch (error) {
@@ -1442,7 +1454,8 @@ const saveTitleDescSection = async () => {
const ok = await persistSiteSettings({
successToast: '블로그 제목·설명이 저장되었습니다.',
savingFlag: savingTitleDesc
savingFlag: savingTitleDesc,
fields: ['title', 'description']
})
if (ok) {
@@ -1487,7 +1500,8 @@ const saveMiscSection = async () => {
const ok = await persistSiteSettings({
successToast: '사이트 정보가 저장되었습니다.',
savingFlag: savingMisc
savingFlag: savingMisc,
fields: ['siteUrl', 'logoText', 'logoUrl', 'faviconUrl', 'copyrightText']
})
if (ok) {
@@ -1574,7 +1588,8 @@ const saveSocialSection = async () => {
const ok = await persistSiteSettings({
successToast: 'SNS 정보가 저장되었습니다.',
savingFlag: savingSocial
savingFlag: savingSocial,
fields: ['socialLinks']
})
if (ok) {
@@ -1636,7 +1651,18 @@ const savePostSection = async () => {
const ok = await persistSiteSettings({
successToast: 'POST 설정이 저장되었습니다.',
savingFlag: savingPost
savingFlag: savingPost,
fields: [
'showPostUpdatedAt',
'postTagLimit',
'homeLatestPostLimit',
'postListPageSize',
'tagPostListPageSize',
'featuredPostLimit',
'pageSectionItemLimit',
'uploadMaxFileCount',
'uploadMaxTotalSize'
]
})
if (ok) {
@@ -1811,7 +1837,13 @@ const saveHomeCoverSection = async () => {
const ok = await persistSiteSettings({
successToast: '메인 화면 설정이 저장되었습니다.',
savingFlag: savingHomeCover
savingFlag: savingHomeCover,
fields: [
'homeCoverImageUrl',
'homeCoverDarkImageUrl',
'homeCoverTitle',
'homeCoverText'
]
})
if (ok) {
@@ -1863,7 +1895,8 @@ const saveBrandSection = async () => {
const ok = await persistSiteSettings({
successToast: '브랜드 컬러가 저장되었습니다.',
savingFlag: savingBrand
savingFlag: savingBrand,
fields: ['brandColor']
})
if (ok) {
@@ -1909,7 +1942,14 @@ const saveAnnouncementSection = async () => {
const ok = await persistSiteSettings({
successToast: '어나운스 바 설정이 저장되었습니다.',
savingFlag: savingAnnouncement
savingFlag: savingAnnouncement,
fields: [
'announcementEnabled',
'announcementText',
'announcementUrl',
'announcementBackgroundColor',
'announcementAlignment'
]
})
if (ok) {
@@ -1951,7 +1991,8 @@ const saveSpamSection = async () => {
const ok = await persistSiteSettings({
successToast: '스팸 필터 설정이 저장되었습니다.',
savingFlag: savingSpam
savingFlag: savingSpam,
fields: ['signupBlockedUsernames']
})
if (ok) {
@@ -1993,7 +2034,8 @@ const saveSiteCodeSection = async () => {
const ok = await persistSiteSettings({
successToast: '사이트 코드 설정이 저장되었습니다.',
savingFlag: savingSiteCode
savingFlag: savingSiteCode,
fields: ['adsTxt', 'customHeadCode', 'customFooterCode']
})
if (ok) {
@@ -2045,7 +2087,16 @@ const saveAdsSection = async () => {
const ok = await persistSiteSettings({
successToast: '광고 슬롯 설정이 저장되었습니다.',
savingFlag: savingAds
savingFlag: savingAds,
fields: [
'adHomeFeedCode',
'adHomeInfeedCode',
'adSidebarCode',
'adPostSidebarCode',
'adPostTopCode',
'adPostInArticleCode',
'adPostBottomCode'
]
})
if (ok) {