/** @type {number} 인아티클 광고 최소 본문 글자 수 */ export const IN_ARTICLE_AD_MIN_TEXT_LENGTH = 2000 /** @type {number} 인아티클 광고 2회 표시 기준 본문 글자 수 */ export const IN_ARTICLE_AD_SECOND_TEXT_LENGTH = 6000 /** @type {number} 인아티클 광고 사이 최소 블록 간격 */ export const IN_ARTICLE_AD_MIN_BLOCK_SPACING = 8 /** * 블록의 텍스트 길이를 계산한다. * @param {Object} block - 마크다운 블록 * @returns {number} 공백을 제외한 텍스트 길이 */ export const getBlockPlainTextLength = (block) => { const value = Array.isArray(block?.text) ? block.text.join(' ') : String(block?.text || '') return value.replace(/\s+/g, '').length } /** * 본문 인아티클 광고를 삽입할 수 있는 문단 블록인지 확인한다. * @param {Object} block - 마크다운 블록 * @returns {boolean} 삽입 후보 여부 */ export const isInArticleAdCandidateBlock = (block) => { return block?.type === 'paragraph' && getBlockPlainTextLength(block) >= 12 } /** * 본문 인아티클 광고 목표 위치 비율 목록을 반환한다. * @param {number} plainTextLength - 본문 글자 수 * @returns {number[]} 광고 목표 위치 비율 */ export 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 목록을 반환한다. * @param {Object[]} blocks - 렌더링 블록 목록 * @returns {string[]} 삽입 기준 블록 ID 목록 */ export const getInArticleAdAfterBlockIds = (blocks) => { const currentBlocks = Array.isArray(blocks) ? blocks : [] 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 || !targetRatios.length) { return [] } const candidates = currentBlocks .map((block, index) => ({ block, index })) .filter((item) => isInArticleAdCandidateBlock(item.block)) if (candidates.length < 6) { return [] } const minIndex = Math.max(3, Math.floor(currentBlocks.length * 0.2)) const maxIndex = Math.min(currentBlocks.length - 4, Math.floor(currentBlocks.length * 0.8)) if (maxIndex < minIndex) { return [] } const rangedCandidates = candidates.filter((item) => item.index >= minIndex && item.index <= maxIndex) if (!rangedCandidates.length) { return [] } const selectedItems = [] 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) }