v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -64,6 +64,8 @@ const selectedMediaUrls = ref([])
|
||||
const lastSelectedIndex = ref(-1)
|
||||
const mediaPage = ref(1)
|
||||
const mediaPageSize = ref(50)
|
||||
/** 그리드 파생 이미지 실패 후 원본으로 되돌린 항목 URL */
|
||||
const mediaGridPreviewFallbackUrls = ref(new Set())
|
||||
const draggingUrls = ref([])
|
||||
const uploadInputRef = ref(null)
|
||||
const uploadingFiles = ref(false)
|
||||
@@ -126,7 +128,10 @@ const { data: mediaResponse, refresh } = await useFetch('/admin/api/media', {
|
||||
unused: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
onResponse () {
|
||||
mediaGridPreviewFallbackUrls.value = new Set()
|
||||
}
|
||||
})
|
||||
|
||||
const mediaItems = computed(() => Array.isArray(mediaResponse.value)
|
||||
@@ -236,6 +241,12 @@ const getMediaPreviewUrl = (item) => {
|
||||
* @returns {string} 그리드 표시 URL
|
||||
*/
|
||||
const getMediaGridPreviewUrl = (item) => {
|
||||
const itemUrl = String(item?.url || '')
|
||||
|
||||
if (!itemUrl || mediaGridPreviewFallbackUrls.value.has(itemUrl)) {
|
||||
return getMediaPreviewUrl(item)
|
||||
}
|
||||
|
||||
if (getMediaItemKind(item) !== 'image') {
|
||||
return getMediaPreviewUrl(item)
|
||||
}
|
||||
@@ -244,12 +255,27 @@ const getMediaGridPreviewUrl = (item) => {
|
||||
return getMediaPreviewUrl(item)
|
||||
}
|
||||
|
||||
const cacheVersion = `${item?.updatedAt || ''}-${item?.size || ''}`
|
||||
|
||||
if (item?.bodyThumbnailStatus?.hasPreviewThumbnail && item.bodyThumbnailStatus.previewThumbnailUrl) {
|
||||
return withGeneratedImageCacheVersion(item.bodyThumbnailStatus.previewThumbnailUrl, cacheVersion)
|
||||
}
|
||||
|
||||
if (item?.bodyThumbnailStatus?.hasThumbnail) {
|
||||
return withGeneratedImageCacheVersion(getPostBodyPreviewThumbnailUrl(item.url), `${item?.updatedAt || ''}-${item?.size || ''}`)
|
||||
const previewUrl = item.bodyThumbnailStatus.previewThumbnailUrl || getPostBodyPreviewThumbnailUrl(item.url)
|
||||
const bodyUrl = item.bodyThumbnailStatus.thumbnailUrl || previewUrl
|
||||
|
||||
if (item.bodyThumbnailStatus.hasPreviewThumbnail === false && bodyUrl) {
|
||||
return withGeneratedImageCacheVersion(bodyUrl, cacheVersion)
|
||||
}
|
||||
|
||||
if (previewUrl) {
|
||||
return withGeneratedImageCacheVersion(previewUrl, cacheVersion)
|
||||
}
|
||||
}
|
||||
|
||||
if (item?.thumbnailStatus?.hasThumbnail && item.thumbnailStatus.thumbnailUrl) {
|
||||
return withGeneratedImageCacheVersion(item.thumbnailStatus.thumbnailUrl, `${item?.updatedAt || ''}-${item?.size || ''}`)
|
||||
return withGeneratedImageCacheVersion(item.thumbnailStatus.thumbnailUrl, cacheVersion)
|
||||
}
|
||||
|
||||
return getMediaPreviewUrl(item)
|
||||
@@ -257,19 +283,21 @@ const getMediaGridPreviewUrl = (item) => {
|
||||
|
||||
/**
|
||||
* 그리드용 파생 이미지 로드 실패 시 원본 이미지로 되돌린다.
|
||||
* @param {Event} event - 이미지 오류 이벤트
|
||||
* @param {Event} _event - 이미지 오류 이벤트
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {void}
|
||||
*/
|
||||
const onMediaGridImageError = (event, item) => {
|
||||
const image = event.target
|
||||
const onMediaGridImageError = (_event, item) => {
|
||||
const itemUrl = String(item?.url || '')
|
||||
|
||||
if (!(image instanceof HTMLImageElement) || image.dataset.fallbackApplied === 'true') {
|
||||
if (!itemUrl || mediaGridPreviewFallbackUrls.value.has(itemUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
image.dataset.fallbackApplied = 'true'
|
||||
image.src = getMediaPreviewUrl(item)
|
||||
mediaGridPreviewFallbackUrls.value = new Set([
|
||||
...mediaGridPreviewFallbackUrls.value,
|
||||
itemUrl
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1419,6 +1447,7 @@ watch(() => mediaPagination.value.page, (page) => {
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-media__filters flex flex-wrap gap-1.5">
|
||||
<template v-if="activeTab === 'library'">
|
||||
<button
|
||||
@@ -1624,45 +1653,47 @@ watch(() => mediaPagination.value.page, (page) => {
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="admin-media__thumb relative flex w-full flex-col text-left outline-none"
|
||||
class="admin-media__thumb flex w-full flex-col text-left outline-none"
|
||||
type="button"
|
||||
@click="openMediaDetail(item)"
|
||||
>
|
||||
<img
|
||||
v-if="getMediaItemKind(item) === 'image'"
|
||||
class="admin-media__image aspect-square w-full bg-surface object-cover"
|
||||
:src="getMediaGridPreviewUrl(item)"
|
||||
:alt="item.title"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@error="onMediaGridImageError($event, item)"
|
||||
>
|
||||
<AdminMediaVideoThumbnail
|
||||
v-else-if="getMediaItemKind(item) === 'video'"
|
||||
:src="item.url"
|
||||
:alt="item.title"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="admin-media__image flex aspect-square w-full items-center justify-center bg-surface text-xs font-bold uppercase tracking-[0.18em] text-muted"
|
||||
>
|
||||
{{ getMediaItemKind(item) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getMediaUsageBadgeCount(item)"
|
||||
class="admin-media__usage-count-badge pointer-events-none absolute right-1.5 top-1.5 rounded bg-[#15171a] px-1.5 py-0.5 text-[10px] font-semibold text-white"
|
||||
>
|
||||
{{ getMediaUsageBadgeCount(item) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getMediaBadgeLabel(item)"
|
||||
class="admin-media__usage-badge pointer-events-none absolute right-1.5 rounded px-1.5 py-0.5 text-[10px] font-semibold"
|
||||
:class="[
|
||||
getMediaBadgeClass(item),
|
||||
getMediaUsageBadgeCount(item) ? 'top-8' : 'top-1.5'
|
||||
]"
|
||||
>
|
||||
{{ getMediaBadgeLabel(item) }}
|
||||
<span class="admin-media__thumb-frame relative block w-full overflow-hidden">
|
||||
<img
|
||||
v-if="getMediaItemKind(item) === 'image'"
|
||||
class="admin-media__image aspect-square w-full bg-surface object-cover"
|
||||
:src="getMediaGridPreviewUrl(item)"
|
||||
:alt="item.title"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@error="onMediaGridImageError($event, item)"
|
||||
>
|
||||
<AdminMediaVideoThumbnail
|
||||
v-else-if="getMediaItemKind(item) === 'video'"
|
||||
:src="item.url"
|
||||
:alt="item.title"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="admin-media__image flex aspect-square w-full items-center justify-center bg-surface text-xs font-bold uppercase tracking-[0.18em] text-muted"
|
||||
>
|
||||
{{ getMediaItemKind(item) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getMediaUsageBadgeCount(item)"
|
||||
class="admin-media__usage-count-badge pointer-events-none absolute right-1.5 top-1.5 z-[1] rounded bg-[#15171a] px-1.5 py-0.5 text-[10px] font-semibold text-white"
|
||||
>
|
||||
{{ getMediaUsageBadgeCount(item) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getMediaBadgeLabel(item)"
|
||||
class="admin-media__usage-badge pointer-events-none absolute right-1.5 z-[1] rounded px-1.5 py-0.5 text-[10px] font-semibold"
|
||||
:class="[
|
||||
getMediaBadgeClass(item),
|
||||
getMediaUsageBadgeCount(item) ? 'top-8' : 'top-1.5'
|
||||
]"
|
||||
>
|
||||
{{ getMediaBadgeLabel(item) }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="admin-media__name block truncate px-2 py-1.5 text-xs font-semibold text-ink">
|
||||
{{ item.name }}
|
||||
@@ -1980,7 +2011,7 @@ watch(() => mediaPagination.value.page, (page) => {
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedMedia.avatarOwner" class="admin-media__usage rounded bg-surface p-3 text-xs">
|
||||
<div class="admin-media__usage rounded bg-surface p-3 text-xs">
|
||||
<strong class="admin-media__usage-title text-ink">
|
||||
사용 현황 {{ selectedMedia.usage.length }}곳
|
||||
</strong>
|
||||
@@ -1998,7 +2029,7 @@ watch(() => mediaPagination.value.page, (page) => {
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="admin-media__usage-empty mt-2 text-muted">
|
||||
사용 중인 곳이 없습니다.
|
||||
{{ selectedMedia.avatarOwner ? '프로필 연결 외에 다른 사용처는 없습니다.' : '사용 중인 곳이 없습니다.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const savePage = async (payload) => {
|
||||
})
|
||||
|
||||
page.value = updatedPage
|
||||
pageForm.value?.markSaved()
|
||||
pageForm.value?.markSaved(payload)
|
||||
showToast('success', '변경 내용이 저장되었습니다.')
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '페이지를 저장하지 못했습니다.'
|
||||
@@ -100,6 +100,7 @@ const deletePage = async () => {
|
||||
await $fetch(`/admin/api/pages/${id.value}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
pageForm.value?.allowNextRouteLeave()
|
||||
await navigateTo('/admin/pages')
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '페이지를 삭제하지 못했습니다.'
|
||||
|
||||
@@ -6,6 +6,7 @@ definePageMeta({
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const pageForm = ref(null)
|
||||
const createdPageId = ref('')
|
||||
const toast = ref(null)
|
||||
let toastTimer = null
|
||||
|
||||
@@ -34,17 +35,18 @@ const savePage = async (payload) => {
|
||||
showToast('info', '페이지를 저장하는 중입니다.')
|
||||
|
||||
try {
|
||||
const page = await $fetch('/admin/api/pages', {
|
||||
method: 'POST',
|
||||
const page = await $fetch(createdPageId.value ? `/admin/api/pages/${createdPageId.value}` : '/admin/api/pages', {
|
||||
method: createdPageId.value ? 'PUT' : 'POST',
|
||||
body: payload
|
||||
})
|
||||
createdPageId.value = String(page.id)
|
||||
|
||||
pageForm.value?.markSaved()
|
||||
pageForm.value?.markSaved(payload)
|
||||
sessionStorage.setItem('SORI_ADMIN_PAGE_TOAST', JSON.stringify({
|
||||
type: 'success',
|
||||
message: '페이지가 저장되었습니다.'
|
||||
}))
|
||||
await navigateTo(`/admin/pages/${page.id}`)
|
||||
await navigateTo(`/admin/pages/${createdPageId.value}`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '페이지를 저장하지 못했습니다.'
|
||||
showToast('error', errorMessage.value)
|
||||
|
||||
@@ -125,11 +125,11 @@ const savePost = async (payload) => {
|
||||
/**
|
||||
* 초안 자동 저장(디바운스 PUT) — 발행·예약 글에는 사용하지 않는다.
|
||||
* @param {Object} payload - `createPostPayload`와 동일 구조
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<boolean>} 저장 성공 여부
|
||||
*/
|
||||
const autosaveDraftPost = async (payload) => {
|
||||
if (!id.value || autoSaving.value) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
autoSaving.value = true
|
||||
@@ -141,6 +141,7 @@ const autosaveDraftPost = async (payload) => {
|
||||
|
||||
post.value = updatedPost
|
||||
postForm.value?.markSaved(payload)
|
||||
return true
|
||||
} catch (error) {
|
||||
const msg = error?.data?.message || error?.message || '자동 저장에 실패했습니다.'
|
||||
postForm.value?.markAutosaveFailed(payload)
|
||||
@@ -149,9 +150,10 @@ const autosaveDraftPost = async (payload) => {
|
||||
toast.value = null
|
||||
errorMessage.value = ''
|
||||
postForm.value?.markSlugError('이미 사용 중인 슬러그입니다.')
|
||||
return
|
||||
return false
|
||||
}
|
||||
showToast('error', msg)
|
||||
return false
|
||||
} finally {
|
||||
autoSaving.value = false
|
||||
}
|
||||
@@ -163,8 +165,7 @@ onBeforeRouteLeave(async () => {
|
||||
if (!payload) {
|
||||
return true
|
||||
}
|
||||
await autosaveDraftPost(payload)
|
||||
return true
|
||||
return autosaveDraftPost(payload)
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,12 +137,12 @@ const savePost = async (payload) => {
|
||||
* 신규 초안 서버 자동 저장(POST). 성공 시 기본은 편집 URL로 교체 이동, 이탈 플러시 시에는 이동하지 않는다.
|
||||
* @param {Object} payload - 게시물 입력값
|
||||
* @param {{ skipNavigate?: boolean }} options - `skipNavigate`가 true면 생성만 하고 URL은 바꾸지 않는다.
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<boolean>} 저장 성공 여부
|
||||
*/
|
||||
const createDraftAutosave = async (payload, options = {}) => {
|
||||
const skipNavigate = Boolean(options.skipNavigate)
|
||||
if (autoSaving.value) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
autoSaving.value = true
|
||||
@@ -161,6 +161,7 @@ const createDraftAutosave = async (payload, options = {}) => {
|
||||
if (!skipNavigate) {
|
||||
await navigateTo(`/admin/posts/${post.id}`, { replace: true })
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
const msg = error?.data?.message || error?.message || '자동 저장에 실패했습니다.'
|
||||
postForm.value?.markAutosaveFailed(payload)
|
||||
@@ -169,10 +170,11 @@ const createDraftAutosave = async (payload, options = {}) => {
|
||||
toast.value = null
|
||||
errorMessage.value = ''
|
||||
postForm.value?.markSlugError('이미 사용 중인 슬러그입니다.')
|
||||
return
|
||||
return false
|
||||
}
|
||||
errorMessage.value = msg
|
||||
showToast('error', msg)
|
||||
return false
|
||||
} finally {
|
||||
autoSaving.value = false
|
||||
}
|
||||
@@ -184,8 +186,7 @@ onBeforeRouteLeave(async () => {
|
||||
if (!payload) {
|
||||
return true
|
||||
}
|
||||
await createDraftAutosave(payload, { skipNavigate: true })
|
||||
return true
|
||||
return createDraftAutosave(payload, { skipNavigate: true })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user