// 상품상세 사은품 안내 — 지급조건을 shop-api 로 직접 조회해 배송비 줄 위에 그린다. // SDK 템플릿 컨텍스트에는 사은품 데이터가 없어서(1.11.0 도 조회 함수만 제공) 직접 호출로 채운다. (function () { const WAIT_TIMEOUT = 20000; function escapeHtml(value) { return String(value == null ? '' : value).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[ch]); } function money(value) { return Number(value || 0).toLocaleString('ko-KR'); } // 헤더는 금액 조건으로 고정하고, 어드민 안내문구(giveConditionExplain)는 아래 보조 줄로 따로 붙인다. // 문구가 헤더를 대체하면 금액 조건이 화면에서 사라져 구매 판단이 안 된다. function conditionText(condition) { const price = Number(condition.upperPrice) || 0; if (!price) return '구매 시'; return condition.byOrderAmount ? `장바구니 합계 ${money(price)}원 이상 시` : `${money(price)}원 이상 구매 시`; } function headText(condition, isSingle) { const base = conditionText(condition); const giftCount = (condition.freeGifts || []).length; if (isSingle) return `${base} 증정`; if (condition.freeGiftOptionCountType === 'SELECT') { return `${base} · 아래 중 ${condition.freeGiftOptionCount || 1}개 선택`; } return `${base} · 아래 ${giftCount}종 모두 증정`; } function giftRow(gift) { const image = gift.imageUrl ? escapeHtml(gift.imageUrl) : ''; const thumb = image ? `` : ''; return `
  • ${thumb}${escapeHtml(gift.productName)}
  • `; } function conditionBlock(condition, isSingle) { const gifts = condition.freeGifts || []; const explain = condition.giveConditionExplain ? `

    ${escapeHtml(condition.giveConditionExplain)}

    ` : ''; return `

    ${headText(condition, isSingle)}

    ${explain}
    `; } function buildElement(conditions) { // 조건도 품목도 하나뿐이면 조건 헤더에 지급방식을 따로 쓸 게 없어 "증정" 한 줄로 줄인다 const isSingle = conditions.length === 1 && (conditions[0].freeGifts || []).length === 1; const dl = document.createElement('dl'); dl.className = 'product-summary__info-freegift pd-freegift'; dl.innerHTML = `
    사은품
    ${conditions.map((condition) => conditionBlock(condition, isSingle)).join('')}

    결제 완료 시 주문에 자동 추가 · 재고 소진 시 조기 종료될 수 있습니다

    `; return dl; } function render(info, conditions) { if (info.querySelector('.pd-freegift')) return; const element = buildElement(conditions); const freight = info.querySelector('.product-summary__freight'); if (freight) freight.insertAdjacentElement('beforebegin', element); else info.appendChild(element); } function waitForInfo() { return new Promise((resolve) => { const found = document.querySelector('.product-summary__info'); if (found) { resolve(found); return; } const observer = new MutationObserver(() => { const element = document.querySelector('.product-summary__info'); if (!element) return; observer.disconnect(); resolve(element); }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(document.querySelector('.product-summary__info')); }, WAIT_TIMEOUT); }); } const productNo = new URLSearchParams(location.search).get('productNo'); if (!productNo || typeof window.fetchShopApi !== 'function') return; const listOf = (data) => (data && data.freeGiftConditions) || []; // order-amount 응답에는 특정 상품에만 걸린 조건까지 섞여 오는데, 응답만으로는 대상 상품을 알 수 없다. // 조건명 앞에 이 표식을 붙인 것만 전상품 대상으로 보고 노출한다(조건명은 어드민 전용이라 화면에 안 나간다). const ALL_PRODUCT_MARK = '[전상품]'; const isAllProduct = (condition) => String(condition.giveConditionName || '').trim().indexOf(ALL_PRODUCT_MARK) === 0; Promise.all([ window.fetchShopApi(`/free-gift-condition/${productNo}`).catch(() => null), // 주문금액(장바구니 합계) 기준 조건은 상품번호 조회에 안 실려서 몰 단위로 따로 받는다. // 이 응답은 다른 상품 전용 조건까지 섞여 오므로 상품 조회분과 표식으로 걸러낸다. window.fetchShopApi('/free-gift-condition/order-amount').catch(() => null), ]) .then(([byProduct, byOrder]) => { const productConditions = listOf(byProduct); const seen = new Set(productConditions.map((condition) => condition.giveConditionNo)); const orderConditions = listOf(byOrder) .filter((condition) => !seen.has(condition.giveConditionNo) && isAllProduct(condition)) .map((condition) => Object.assign({}, condition, { byOrderAmount: true })); const conditions = productConditions.concat(orderConditions); if (!conditions.length) return null; return waitForInfo().then((info) => { if (!info) return; render(info, conditions); // 옵션 변경 등으로 요약 영역이 다시 그려지면 사은품 줄이 함께 날아간다 new MutationObserver(() => { const current = document.querySelector('.product-summary__info'); if (current && !current.querySelector('.pd-freegift')) render(current, conditions); }).observe(info.parentElement || info, { childList: true, subtree: true }); }); }) .catch(() => {}); })();