내 마음대로 할인 | 스킨 연동 가이드

스킨 연동 가이드

대상: 쇼핑몰 스킨을 작업하는 디자이너·퍼블리셔 · 문서 버전 2026-11-06 · 데이터 버전 1

  1. 이 데이터로 할 수 있는 것
  2. 데이터 위치와 읽는 방법
  3. 전체 구조
  4. 필드 설명
  5. 값이 없을 수 있는 경우
  6. 할인 금액 계산
  7. 활용 예제

1. 이 데이터로 할 수 있는 것

"내 마음대로 할인" 앱이 설치된 쇼핑몰의 메인 · 상품 목록 · 검색 결과 · 상품 상세 페이지에는 지금 보고 있는 방문자에게 해당하는 할인 정보가 JSON 데이터로 들어 있습니다. 스킨에서 이 데이터를 읽으면 할인 표시를 원하는 모양으로 직접 만들 수 있습니다.

표시용 안내 데이터입니다
  • 실제 결제 금액은 장바구니·주문서에서 그 시점의 판매가와 수량으로 다시 계산됩니다. 화면에 보여 주는 할인가는 미리 보여 주는 안내입니다.
  • 이 데이터를 스크립트로 바꿔도 결제 금액은 바뀌지 않습니다.
앱이 기본으로 보여 주는 할인 표시와 직접 만든 표시가 겹친다면, 쇼핑몰 운영자에게 앱 관리 화면에서 표시 설정을 조정해 달라고 요청해 주세요.

2. 데이터 위치와 읽는 방법

데이터는 페이지 <head>에 아래 태그로 들어갑니다. 화면에는 보이지 않습니다.

<script type="application/json" id="wonlab-dp-discount-data">
  ...JSON...
</script>

언제 들어가나요

권장 읽기 방법

"지금 있는 데이터를 한 번 읽고 + 이후 바뀔 때마다 다시 읽기"를 함께 쓰면 타이밍 문제 없이 동작합니다.

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); // 나중에 들어오거나 바뀌는 경우

3. 전체 구조

예시 값입니다. 실제로는 해당하는 할인만 들어 있습니다.

{
  "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"
}

4. 필드 설명

최상위

필드형식설명
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_eligibletrue/false이 방문자가 이 상품에서 신규회원 첫 구매 할인을 실제로 받을 수 있으면 true. 비회원은 항상 false
is_product_first_purchase_eligibletrue/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 = 담은 개수만큼 곱한 금액

5. 값이 없을 수 있는 경우

아래 경우에는 필드가 아예 없을 수 있습니다. 값이 있는지 확인한 뒤 사용하고, 없으면 해당 표시를 숨기도록 만들어 주세요.

데이터에 이 문서에 없는 필드가 보일 수 있습니다. 안내하지 않은 필드는 예고 없이 바뀌거나 빠질 수 있으니 사용하지 마세요.

6. 할인 금액 계산

화면에 할인가를 보여 줄 때는 아래 규칙으로 계산하면 앱의 기본 표시와 같은 값이 나옵니다.

지금 이 방문자에게 적용되는 할인

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);
}
쇼핑몰이 장바구니 금액·수량 조건 이벤트를 진행 중이면, 조건을 채운 주문은 상품별 할인 대신 이벤트 할인으로 결제될 수 있습니다. 화면의 할인가는 "상품별 할인 기준 안내"로 표기해 주세요.

7. 활용 예제

예제의 클래스명·문구는 자유롭게 바꿔 쓰시면 됩니다. 아래 코드는 2장의 readDiscountData(), 6장의 discountAmount()·currentDiscount()를 함께 넣었다고 가정합니다.

7-1. 상품 목록 카드에 할인율 배지

스킨의 상품 목록 모듈에서 카드마다 상품번호를 속성으로 달고, 배지 자리를 만들어 둡니다. ({$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);

7-2. 상품 상세에 타임세일 남은 시간

지금 보고 있는 상품은 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);

7-3. 한정 수량 · 수량별 할인 안내 문구

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% 할인"