사이트 광고 슬롯 설정 추가

This commit is contained in:
2026-06-05 15:43:57 +09:00
parent 928b8446b4
commit 9a4820e69c
16 changed files with 408 additions and 7 deletions

View File

@@ -33,6 +33,7 @@ const savingBrand = ref(false)
const savingAnnouncement = ref(false)
const savingSpam = ref(false)
const savingSiteCode = ref(false)
const savingAds = ref(false)
const uploadingLogo = ref(false)
const uploadingHomeCover = ref(false)
const uploadingHomeCoverDark = ref(false)
@@ -81,6 +82,8 @@ const customizeAnnouncement = ref(false)
const editSpam = ref(false)
/** 사이트 코드 카드 편집 모드 여부 */
const editSiteCode = ref(false)
/** 광고 슬롯 카드 편집 모드 여부 */
const editAds = ref(false)
/** 편집 시작 시점의 제목·설명(취소 시 복원용) */
const titleDescSnapshot = reactive({
title: '',
@@ -131,6 +134,13 @@ const siteCodeSnapshot = reactive({
customHeadCode: '',
customFooterCode: ''
})
/** 편집 시작 시점의 광고 슬롯(취소 시 복원용) */
const adsSnapshot = reactive({
adHomeFeedCode: '',
adSidebarCode: '',
adPostTopCode: '',
adPostBottomCode: ''
})
let toastTimer = null
let scrollSpyFrame = null
let postExportRefreshTimer = null
@@ -166,7 +176,11 @@ const form = reactive({
signupBlockedUsernames: normalizeSignupBlockedUsernames(settings.value?.signupBlockedUsernames),
adsTxt: settings.value?.adsTxt || '',
customHeadCode: settings.value?.customHeadCode || '',
customFooterCode: settings.value?.customFooterCode || ''
customFooterCode: settings.value?.customFooterCode || '',
adHomeFeedCode: settings.value?.adHomeFeedCode || '',
adSidebarCode: settings.value?.adSidebarCode || '',
adPostTopCode: settings.value?.adPostTopCode || '',
adPostBottomCode: settings.value?.adPostBottomCode || ''
})
/**
@@ -251,6 +265,17 @@ const hasSiteCodeChanges = computed(() => editSiteCode.value && (
|| form.customFooterCode !== siteCodeSnapshot.customFooterCode
))
/**
* 광고 슬롯 변경 여부
* @returns {boolean} 변경 여부
*/
const hasAdsChanges = computed(() => editAds.value && (
form.adHomeFeedCode !== adsSnapshot.adHomeFeedCode
|| form.adSidebarCode !== adsSnapshot.adSidebarCode
|| form.adPostTopCode !== adsSnapshot.adPostTopCode
|| form.adPostBottomCode !== adsSnapshot.adPostBottomCode
))
/**
* 최신 게시물 export 작업 목록
* @returns {Array} export 작업 목록
@@ -520,7 +545,8 @@ const settingsNavGroups = [
{ id: 'admin-settings-section-home-cover', label: '메인 화면', keywords: 'home cover hero banner image', iconId: 'home-cover' },
{ id: 'admin-settings-section-brand', label: '브랜드', keywords: 'brand design accent color point 포인트 컬러', iconId: 'site-code' },
{ id: 'admin-settings-section-announcement', label: '어나운스 바', keywords: 'announcement banner notice', iconId: 'announcement' },
{ id: 'admin-settings-section-site-code', label: '사이트 코드', keywords: 'ads ads.txt head footer script code adsense', iconId: 'site-code' }
{ id: 'admin-settings-section-site-code', label: '사이트 코드', keywords: 'ads ads.txt head footer script code adsense', iconId: 'site-code' },
{ id: 'admin-settings-section-ads', label: 'Ads', keywords: 'ads ad slot advertisement adsense 광고 애드센스', iconId: 'site-code' }
]
},
{
@@ -1100,7 +1126,11 @@ const buildSiteSettingsPayload = () => ({
signupBlockedUsernames: normalizeSignupBlockedUsernames(form.signupBlockedUsernames),
adsTxt: form.adsTxt || '',
customHeadCode: form.customHeadCode || '',
customFooterCode: form.customFooterCode || ''
customFooterCode: form.customFooterCode || '',
adHomeFeedCode: form.adHomeFeedCode || '',
adSidebarCode: form.adSidebarCode || '',
adPostTopCode: form.adPostTopCode || '',
adPostBottomCode: form.adPostBottomCode || ''
})
/**
@@ -1684,6 +1714,53 @@ const saveSiteCodeSection = async () => {
}
}
/**
* 광고 슬롯 편집 모드 진입
* @returns {void}
*/
const beginEditAds = () => {
adsSnapshot.adHomeFeedCode = form.adHomeFeedCode
adsSnapshot.adSidebarCode = form.adSidebarCode
adsSnapshot.adPostTopCode = form.adPostTopCode
adsSnapshot.adPostBottomCode = form.adPostBottomCode
editAds.value = true
}
/**
* 광고 슬롯 편집 취소
* @returns {void}
*/
const cancelEditAds = () => {
form.adHomeFeedCode = adsSnapshot.adHomeFeedCode
form.adSidebarCode = adsSnapshot.adSidebarCode
form.adPostTopCode = adsSnapshot.adPostTopCode
form.adPostBottomCode = adsSnapshot.adPostBottomCode
editAds.value = false
}
/**
* 광고 슬롯 설정 저장
* @returns {Promise<void>}
*/
const saveAdsSection = async () => {
if (!hasAdsChanges.value) {
return
}
const ok = await persistSiteSettings({
successToast: '광고 슬롯 설정이 저장되었습니다.',
savingFlag: savingAds
})
if (ok) {
adsSnapshot.adHomeFeedCode = form.adHomeFeedCode
adsSnapshot.adSidebarCode = form.adSidebarCode
adsSnapshot.adPostTopCode = form.adPostTopCode
adsSnapshot.adPostBottomCode = form.adPostBottomCode
editAds.value = false
}
}
/**
* Escape 키: 제목·설명 편집 중이면 취소, 아니면 설정 화면 닫기
* @param {KeyboardEvent} event - 키보드 이벤트
@@ -1728,6 +1805,11 @@ const onGlobalKeydown = (event) => {
cancelEditSiteCode()
return
}
if (editAds.value) {
event.preventDefault()
cancelEditAds()
return
}
closeSettings()
}
@@ -2919,6 +3001,16 @@ onBeforeUnmount(() => {
@save="saveSiteCodeSection"
/>
<AdminAdsSettingsCard
:form="form"
:editing="editAds"
:saving="savingAds"
:has-changes="hasAdsChanges"
@begin="beginEditAds"
@cancel="cancelEditAds"
@save="saveAdsSection"
/>
<h2 class="admin-settings-screen__section-heading z-20 mb-px pt-10 text-2xl font-bold tracking-tight text-[#15171a]">
콘텐츠·안전
</h2>

View File

@@ -348,6 +348,12 @@ const scrollFeatured = (direction) => {
</div>
</section>
<SiteAdSlot
class="home-page__ad-slot px-6 py-4"
:code="siteSettings?.adHomeFeedCode"
location="home-feed"
/>
<section class="latest-posts-section min-h-[360px] py-4 px-6">
<div class="mx-auto max-w-[720px]">
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">

View File

@@ -15,6 +15,9 @@ const { data: tags } = await useFetch('/api/tags', {
const { data: posts } = await useFetch('/api/posts', {
default: () => []
})
const { data: siteSettings } = await useFetch('/api/site-settings', {
default: () => ({})
})
const postToc = useState('post-detail-toc', () => [])
if (!post.value) {
@@ -338,9 +341,19 @@ useHead(() => ({
<section>
<div class="mx-auto max-w-[720px] px-4 sm:px-5">
<SiteAdSlot
class="post-detail__ad-slot post-detail__ad-slot--top mb-8"
:code="siteSettings?.adPostTopCode"
location="post-top"
/>
<ContentRenderer>
<ContentMarkdownRenderer class="post-detail__content" :content="post.content" />
</ContentRenderer>
<SiteAdSlot
class="post-detail__ad-slot post-detail__ad-slot--bottom mt-8"
:code="siteSettings?.adPostBottomCode"
location="post-bottom"
/>
</div>
</section>