// 상품상세 사은품 안내 — 지급조건을 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}
${gifts.map(giftRow).join('')}
`;
}
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(() => {});
})();