게시물 광고 배치 조정

This commit is contained in:
2026-06-05 16:32:29 +09:00
parent cc9e5949fa
commit 629ef8c4c6
16 changed files with 129 additions and 31 deletions

View File

@@ -92,6 +92,12 @@ const BLANK_PARAGRAPH_MARKER = '<!--sori:blank-paragraph-->'
const LIVE_IMAGE_DRAG_MIME = 'application/x-sori-live-image'
/** @type {string} 본문 리스트 마커 색상 */
const CONTENT_LIST_MARKER_COLOR = '#2eb6ea'
/** @type {number} 인아티클 광고 최소 본문 글자 수 */
const IN_ARTICLE_AD_MIN_TEXT_LENGTH = 2000
/** @type {number} 인아티클 광고 2회 표시 기준 본문 글자 수 */
const IN_ARTICLE_AD_SECOND_TEXT_LENGTH = 6000
/** @type {number} 인아티클 광고 사이 최소 블록 간격 */
const IN_ARTICLE_AD_MIN_BLOCK_SPACING = 8
const activeLightboxImages = ref([])
const activeLightboxIndex = ref(0)
@@ -784,19 +790,38 @@ const isInArticleAdCandidateBlock = (block) => {
}
/**
* 본문 인아티클 광고를 삽입할 블록 ID를 반환한다.
* @returns {string} 삽입 기준 블록 ID
* 본문 인아티클 광고 목표 위치 비율 목록을 반환한다.
* @param {number} plainTextLength - 본문 글자 수
* @returns {number[]} 광고 목표 위치 비율
*/
const inArticleAdAfterBlockId = computed(() => {
const getInArticleAdTargetRatios = (plainTextLength) => {
if (plainTextLength >= IN_ARTICLE_AD_SECOND_TEXT_LENGTH) {
return [0.35, 0.7]
}
if (plainTextLength >= IN_ARTICLE_AD_MIN_TEXT_LENGTH) {
return [0.4]
}
return []
}
/**
* 본문 인아티클 광고를 삽입할 블록 ID 목록을 반환한다.
* @returns {string[]} 삽입 기준 블록 ID 목록
*/
const inArticleAdAfterBlockIds = computed(() => {
if (props.interactive || !normalizedInArticleAdCode.value) {
return ''
return []
}
const currentBlocks = blocks.value
const contentBlocks = currentBlocks.filter((block) => !['spacer', 'divider'].includes(block.type))
const plainTextLength = contentBlocks.reduce((sum, block) => sum + getBlockPlainTextLength(block), 0)
const targetRatios = getInArticleAdTargetRatios(plainTextLength)
if (contentBlocks.length < 10) {
return ''
if (contentBlocks.length < 10 || !targetRatios.length) {
return []
}
const candidates = currentBlocks
@@ -804,29 +829,48 @@ const inArticleAdAfterBlockId = computed(() => {
.filter((item) => isInArticleAdCandidateBlock(item.block))
if (candidates.length < 6) {
return ''
return []
}
const minIndex = Math.max(3, Math.floor(currentBlocks.length * 0.2))
const maxIndex = Math.min(currentBlocks.length - 4, Math.floor(currentBlocks.length * 0.75))
const maxIndex = Math.min(currentBlocks.length - 4, Math.floor(currentBlocks.length * 0.8))
if (maxIndex < minIndex) {
return ''
return []
}
const targetIndex = Math.floor((currentBlocks.length - 1) * 0.4)
const rangedCandidates = candidates.filter((item) => item.index >= minIndex && item.index <= maxIndex)
if (!rangedCandidates.length) {
return ''
return []
}
const selected = rangedCandidates.reduce((closest, item) => {
return Math.abs(item.index - targetIndex) < Math.abs(closest.index - targetIndex) ? item : closest
}, rangedCandidates[0])
const selectedItems = []
return selected.block.id
for (const ratio of targetRatios) {
const targetIndex = Math.floor((currentBlocks.length - 1) * ratio)
const availableCandidates = rangedCandidates.filter((item) => {
return selectedItems.every((selected) => Math.abs(selected.index - item.index) >= IN_ARTICLE_AD_MIN_BLOCK_SPACING)
})
if (!availableCandidates.length) {
continue
}
const selected = availableCandidates.reduce((closest, item) => {
return Math.abs(item.index - targetIndex) < Math.abs(closest.index - targetIndex) ? item : closest
}, availableCandidates[0])
selectedItems.push(selected)
}
return selectedItems
.sort((a, b) => a.index - b.index)
.map((item) => item.block.id)
})
const inArticleAdAfterBlockIdSet = computed(() => new Set(inArticleAdAfterBlockIds.value))
const headingIdsByBlock = computed(() => {
const createHeadingId = createHeadingIdFactory()
const ids = {}
@@ -3331,7 +3375,7 @@ onBeforeUnmount(() => {
</p>
<SiteAdSlot
v-if="block.id === inArticleAdAfterBlockId"
v-if="inArticleAdAfterBlockIdSet.has(block.id)"
class="content-markdown-renderer__in-article-ad my-8"
:code="normalizedInArticleAdCode"
location="post-in-article"