"내 마음대로 할인" 앱이 설치된 쇼핑몰의 메인 · 상품 목록 · 검색 결과 · 상품 상세 페이지에는 지금 보고 있는 방문자에게 해당하는 할인 정보가 JSON 데이터로 들어 있습니다. 스킨에서 이 데이터를 읽으면 할인 표시를 원하는 모양으로 직접 만들 수 있습니다.
데이터는 페이지 <head>에 아래 태그로 들어갑니다. 화면에는 보이지 않습니다.
<script type="application/json" id="wonlab-dp-discount-data">
...JSON...
</script>
document에서 wonlab:dp:discount-data 이벤트가 발생합니다."지금 있는 데이터를 한 번 읽고 + 이후 바뀔 때마다 다시 읽기"를 함께 쓰면 타이밍 문제 없이 동작합니다.
function readDiscountData() {
var tag = document.getElementById('wonlab-dp-discount-data');
if (!tag) return null;
try { return JSON.parse(tag.textContent); } catch (e) { return null; }
}
function render() {
var data = readDiscountData();
if (!data || !data.products) return;
// 여기서 할인 표시를 그립니다 (예제는 7장 참고)
}
render(); // 이미 들어 있는 경우
document.addEventListener('wonlab:dp:discount-data', render); // 나중에 들어오거나 바뀌는 경우
event.detail.product_nos에는 이번에 추가·갱신된 상품 번호 목록이 들어 있습니다. 빈 배열이면 회원 정보 같은 페이지 공통 정보만 바뀐 것입니다. 전체를 다시 그려도 되고, 이 목록의 상품만 다시 그려도 됩니다.예시 값입니다. 실제로는 해당하는 할인만 들어 있습니다.
{
"version": 1,
"products": {
"123": {
"source": "detail",
"price": 42000,
"discounts": {
"base": { "type": "rate", "value": 10,
"start_date": "2026-11-01 00:00:00", "end_date": "2026-11-30 23:59:59",
"limit": 100, "limit_type": "daily", "remaining": 37 },
"time_sale": { "type": "rate", "value": 20, "time_start": "10:00:00", "time_end": "22:00:00",
"end_date": "2026-11-30 23:59:59" },
"member_group": { "type": "rate", "value": 5, "group_no": 2, "group_name": "VIP" },
"first_purchase": { "type": "fixed", "value": 3000 },
"product_first_purchase": { "type": "rate", "value": 10 },
"quantity": { "type": "rate", "value": 10, "threshold": 2,
"tiers": [ { "threshold": 2, "value": 10 }, { "threshold": 3, "value": 13 } ] }
},
"is_first_purchase_eligible": true,
"is_product_first_purchase_eligible": false,
"fp_apply_scope": "all",
"excluded_variants": ["P000000X000A"]
},
"456": {
"source": "list",
"price": 29000,
"discounts": {},
"is_first_purchase_eligible": false,
"is_product_first_purchase_eligible": false
}
},
"member": { "logged_in": true, "group_no": 2 },
"server_time": "2026-11-06T14:20:00+09:00"
}
| 필드 | 형식 | 설명 |
|---|---|---|
version | 숫자 | 데이터 형식 버전. 현재 1 |
products | 객체 | 상품번호(문자열)를 키로 하는 상품별 할인 정보. 페이지에 보이는 상품들이 들어 있습니다 |
member | 객체 | logged_in: 로그인 여부(true/false)group_no: 카페24 회원등급 번호. 비회원은 null회원 아이디·이름 같은 개인정보는 들어 있지 않습니다 |
server_time | 문자열 | 할인 정보를 받아 온 시점의 서버 시각(ISO 8601, 한국 시간). 방문자 PC 시계가 틀려도 카운트다운이 맞도록 이 값을 기준으로 계산하세요 |
products["상품번호"])| 필드 | 형식 | 설명 |
|---|---|---|
source | 문자열 | 어느 페이지 영역에서 받은 정보인지. detail = 지금 보고 있는 상품 상세의 상품, list = 목록·메인·검색 결과, 상세 페이지 안의 관련 상품 목록의 상품 |
price | 숫자(원) | 판매가. 할인 계산의 기준 금액입니다 |
discounts | 객체 | 이 방문자에게 지금 보여 줄 할인. 키는 할인 유형(아래 표). 할인이 없으면 빈 객체 {} |
is_first_purchase_eligible | true/false | 이 방문자가 이 상품에서 신규회원 첫 구매 할인을 실제로 받을 수 있으면 true. 비회원은 항상 false |
is_product_first_purchase_eligible | true/false | 이 방문자가 이 상품의 신상품 첫 구매 할인을 실제로 받을 수 있으면 true. 비회원은 항상 false |
fp_apply_scope | 문자열 | 첫 구매 할인이 적용되는 수량. single = 1개분에만, all = 담은 수량 전체. 상품 상세에서만 들어갑니다 |
excluded_variants | 배열 | 할인에서 제외된 품목(옵션) 코드 목록. 이 품목을 고르면 할인되지 않습니다. 상품 상세에서만, 제외 품목이 있을 때만 들어갑니다 |
discounts의 키)| 키 | 할인 | 들어가는 조건 |
|---|---|---|
base | 기본 할인 | 할인 기간 중일 때 (모든 방문자) |
time_sale | 타임세일 | 할인 기간 중이고, 지금이 매일 적용 시간 안일 때 (모든 방문자) |
member_group | 회원등급별 할인 | 로그인한 방문자의 회원등급에 해당하는 할인이 있을 때만 |
first_purchase | 신규회원 첫 구매 할인 | 상품에 설정되어 있을 때. 비회원에게 "가입하고 첫 구매하면 받는 혜택" 안내용으로도 들어갈 수 있습니다(쇼핑몰 설정에 따라 비회원에게는 빠짐). 실제로 받을 수 있는지는 is_first_purchase_eligible로 확인하세요 |
product_first_purchase | 신상품 첫 구매 할인 | 위와 같습니다. 실제로 받을 수 있는지는 is_product_first_purchase_eligible로 확인하세요 |
quantity | 수량별 할인 | 상품에 설정되어 있을 때. 실제 적용은 장바구니에 담는 수량에 따라 정해집니다 |
할인 기간이 시작 전이거나 끝난 할인, 적용 시간 밖의 타임세일, 한정 수량이 다 팔린 할인은 데이터에서 빠집니다.
| 필드 | 형식 | 설명 |
|---|---|---|
type | 문자열 | rate = 정률(%), fixed = 정액(원) |
value | 숫자 | 할인율(%) 또는 할인액(원). 정액은 상품 1개당 금액입니다(수량별 할인은 아래 fixed_mode 참고) |
start_date · end_date | 문자열 | 할인 기간 (예: 2026-11-30 23:59:59, 한국 시간). 기간을 정하지 않은 할인에는 이 필드가 없습니다 |
| 필드 | 유형 | 설명 |
|---|---|---|
time_start · time_end | 타임세일 | 매일 반복되는 적용 시간 (예: 10:00:00). 없으면 하루 종일 적용입니다 |
limit | 기본 할인, 타임세일 | 한정 수량. 한정 수량을 정한 할인에만 들어갑니다 |
limit_type | 기본 할인, 타임세일 | daily = 매일 다시 채워짐, total = 할인 기간 전체에서 한 번 |
remaining | 기본 할인, 타임세일 | 남은 수량. 페이지를 연 시점의 값이며 실시간으로 줄어들지 않습니다 |
group_no · group_name | 회원등급별 할인 | 할인이 걸린 카페24 회원등급 번호와 등급 이름 |
threshold | 수량별 할인 | 할인이 시작되는 수량 (1단계 기준 수량) |
tiers | 수량별 할인 | 수량 단계 목록 [ { threshold, value } ]. 할인 방식(type)은 모든 단계가 같습니다. 도달한 단계 중 가장 높은 단계 하나만 적용됩니다. 없으면 threshold·value 1단계만 있는 것입니다 |
fixed_mode | 수량별 할인 (정액) | once = 상품당 1회 그 금액, per_unit = 담은 개수만큼 곱한 금액 |
아래 경우에는 필드가 아예 없을 수 있습니다. 값이 있는지 확인한 뒤 사용하고, 없으면 해당 표시를 숨기도록 만들어 주세요.
discounts가 빈 객체입니다.price — 목록의 판매가는 화면의 상품 카드에서 읽어 옵니다. 스킨 구조상 가격을 읽지 못한 상품은 price가 없습니다.fp_apply_scope, excluded_variants는 목록 상품에는 없습니다.price, tiers, fixed_mode, limit·limit_type·remaining, member, server_time이 없고, 목록 상품에는 첫 구매 대상 여부(is_*_eligible)도 없습니다. 이 필드가 필요하면 쇼핑몰 운영자에게 새 표시 방식 사용 여부를 확인해 주세요.화면에 할인가를 보여 줄 때는 아래 규칙으로 계산하면 앱의 기본 표시와 같은 값이 나옵니다.
판매가 × 할인율 ÷ 100, 원 미만은 버립니다.value원 (상품 1개 기준).base, time_sale, member_group — 데이터에 있으면 적용됩니다.first_purchase, product_first_purchase — 각각 is_first_purchase_eligible, is_product_first_purchase_eligible이 true일 때만 적용됩니다. false라면 "회원가입 후 첫 구매 시 10% 할인"처럼 안내 문구로 쓰세요.quantity — 담는 수량에 따라 달라지므로 할인가에 미리 넣지 말고 "2개 이상 10% 할인" 같은 안내로 보여 주는 것을 권장합니다.function discountAmount(d, price) {
if (!d || !price) return 0;
if (d.type === 'rate') return Math.floor(price * d.value / 100);
return Number(d.value) || 0;
}
// 1개 구매 기준, 지금 이 방문자에게 적용되는 할인 합계
function currentDiscount(product) {
var ds = product.discounts || {};
var price = product.price;
var sum = discountAmount(ds.base, price)
+ discountAmount(ds.time_sale, price)
+ discountAmount(ds.member_group, price);
if (product.is_first_purchase_eligible) sum += discountAmount(ds.first_purchase, price);
if (product.is_product_first_purchase_eligible) sum += discountAmount(ds.product_first_purchase, price);
return Math.min(sum, price);
}
예제의 클래스명·문구는 자유롭게 바꿔 쓰시면 됩니다. 아래 코드는 2장의 readDiscountData(), 6장의 discountAmount()·currentDiscount()를 함께 넣었다고 가정합니다.
스킨의 상품 목록 모듈에서 카드마다 상품번호를 속성으로 달고, 배지 자리를 만들어 둡니다. ({$product_no}는 카페24 상품 목록 모듈의 상품번호 변수입니다.)
<li class="prd-item" data-my-product="{$product_no}">
...
<span class="my-discount-badge" hidden></span>
</li>
function renderBadges() {
var data = readDiscountData();
if (!data || !data.products) return;
document.querySelectorAll('[data-my-product]').forEach(function (card) {
var product = data.products[card.getAttribute('data-my-product')];
var badge = card.querySelector('.my-discount-badge');
if (!badge) return;
if (!product || !product.price) { badge.hidden = true; return; }
var amount = currentDiscount(product);
var rate = Math.floor(amount / product.price * 100);
badge.textContent = rate + '%';
badge.hidden = !(rate > 0);
});
}
renderBadges();
document.addEventListener('wonlab:dp:discount-data', renderBadges);
지금 보고 있는 상품은 source가 detail인 상품입니다. 방문자 PC 시계 대신 server_time을 기준으로 계산합니다.
<div class="my-timesale" hidden>타임세일 종료까지 <b class="my-timesale-left"></b></div>
var timer = null;
function renderTimeSale() {
var data = readDiscountData();
var box = document.querySelector('.my-timesale');
if (!data || !data.products || !data.server_time || !box) return;
var product = null;
Object.keys(data.products).forEach(function (no) {
if (data.products[no].source === 'detail') product = data.products[no];
});
var ts = product && product.discounts.time_sale;
if (!ts || !ts.time_end) { box.hidden = true; return; }
// 서버 시각과 방문자 PC 시계의 차이를 보정
var offset = Date.parse(data.server_time) - Date.now();
var endTime = ts.time_end.length === 5 ? ts.time_end + ':00' : ts.time_end;
var end = Date.parse(data.server_time.slice(0, 10) + 'T' + endTime + '+09:00');
clearInterval(timer);
function tick() {
var left = Math.floor((end - (Date.now() + offset)) / 1000);
if (left <= 0) { box.hidden = true; clearInterval(timer); return; }
var h = Math.floor(left / 3600), m = Math.floor(left % 3600 / 60), s = left % 60;
box.querySelector('.my-timesale-left').textContent =
h + '시간 ' + String(m).padStart(2, '0') + '분 ' + String(s).padStart(2, '0') + '초';
box.hidden = false;
}
tick();
timer = setInterval(tick, 1000);
}
renderTimeSale();
document.addEventListener('wonlab:dp:discount-data', renderTimeSale);
function limitText(d) {
if (!d || d.remaining == null) return '';
return (d.limit_type === 'daily' ? '오늘 ' : '') + d.remaining + '개 남음';
}
function quantityText(q) {
if (!q) return '';
var tiers = q.tiers || [{ threshold: q.threshold, value: q.value }];
var unit = q.type === 'rate' ? '%' : '원';
return tiers.map(function (t) {
return t.threshold + '개 이상 ' + Number(t.value).toLocaleString() + unit + ' 할인';
}).join(' / ');
}
// 예) "오늘 37개 남음", "2개 이상 10% 할인 / 3개 이상 13% 할인"