Saeroon Developer Center
1Step 1 Start2Step 2 Connect· My statusDocs

이 문서의 큐

  1. 0. 배포 시 자동 연결
  2. 1. 스크립트로 쓸 때 — window.saeroonShop
  3. 2. 상품 진열
  4. 3. 담기 · 카트 배지 · 카트 페이지
  5. 4. 배송비 미리보기 · 우편번호
  6. 5. 체크아웃 (결제 시작)
  7. 6. 주문완료 (결제 확정)
  8. 7. 주문조회 · 배송추적
  9. 8. 반품·교환 신청
  10. 9. 위시리스트(찜)
  11. 10. 이벤트
  12. 11. 메서드
  13. 12. 사업자정보·환불정책 표시 (전자상거래법)
  14. 13. 제약과 함정
  15. 14. 속성 요약
  16. 15. 상품 올리기 — AI·CLI·REST (운영자 통로)

개요

시작하기

Quickstart

기능 붙이기

FormsMembersBoardsShopCatalogsGroupsHoldings블로그 가져오기

앱 붙이기

앱 백엔드 붙이기앱 콜백 브리지

레퍼런스

속성 계약 (data-saeroon-*)CLI 레퍼런스 (@saeroon/cli)APIMCP예약 경로

마켓플레이스

Marketplace

문서 / 기능 붙이기

이 문서의 큐
  1. 0. 배포 시 자동 연결
  2. 1. 스크립트로 쓸 때 — window.saeroonShop
  3. 2. 상품 진열
  4. 3. 담기 · 카트 배지 · 카트 페이지
  5. 4. 배송비 미리보기 · 우편번호
  6. 5. 체크아웃 (결제 시작)
  7. 6. 주문완료 (결제 확정)
  8. 7. 주문조회 · 배송추적
  9. 8. 반품·교환 신청
  10. 9. 위시리스트(찜)
  11. 10. 이벤트
  12. 11. 메서드
  13. 12. 사업자정보·환불정책 표시 (전자상거래법)
  14. 13. 제약과 함정
  15. 14. 속성 요약
  16. 15. 상품 올리기 — AI·CLI·REST (운영자 통로)

Shop

정적 사이트에 쇼핑몰을 붙입니다. 진열·담기·배송비·주문조회·반품·찜은 HTML 속성(data-saeroon-*)으로 선언하고, 결제 시작과 결제 완료 처리 두 곳만 스크립트(window.saeroonShop)를 한 줄씩 부릅니다. 상품·재고·배송비 정책·주문 처리는 대시보드에서 운영자가 관리합니다.

이 문서는 손님이 밟는 순서대로 씁니다. 각 절의 스니펫은 packages/shop-sdk/examples/의 예제 페이지와 1:1 입니다.

0. 배포 시 자동 연결

쇼핑 속성이 있는 HTML 을 배포하면 shop SDK 로더(https://cdn.saeroon.com/sdk/shop/v1/loader.js)가 <head> 에 자동 주입됩니다. 공개 쇼핑 API 는 사이트 slug 로 범위가 정해지므로 로더에 data-site-slug 가 실립니다.

cd my-shop
npx @saeroon/cli deploy .
  • slug 는 saeroon.config.json 의 siteSlug 를 쓰고, 없으면 배포 대상 사이트를 API 로 조회해 채웁니다(2026-09-04 이후 CLI). 그래도 못 구하면 쇼핑 페이지 수를 경고로 찍습니다 — 그 페이지는 카트가 동작하지 않습니다.
  • 주입 트리거는 아래 표의 모든 기능 속성입니다. MCP 로 배포하는 경우는 아직 data-saeroon-shop·add-to-cart·cart-count 3종만 보므로, 어느 통로든 안전하게 <body data-saeroon-shop> 을 붙이세요.
  • 같은 사이트에 회원·게시판도 쓴다면 --pk pk_live_… 를 넘깁니다. 쇼핑만 쓰는 사이트는 공개 키가 필요 없습니다.
  • 결제 provider 는 현재 toss 만 지원합니다. data-provider 에 다른 값을 넣으면 SDK 가 시작하지 않고 콘솔에 사유를 남깁니다(saeroon:shop-ready 미발행).

1. 스크립트로 쓸 때 — window.saeroonShop

속성만으로 안 되는 두 곳(결제 시작·결제 완료)과 커스텀 화면은 전역 인스턴스를 씁니다. 이름은 소문자 saeroonShop 입니다 — 대문자 SaeroonShop 은 번들 네임스페이스라 다릅니다.

<script>
  window.addEventListener('saeroon:shop-ready', async (e) => {
    const shop = e.detail.shop;            // = window.saeroonShop
    await shop.ready;                      // 카트 1회 조회 완료
    console.log(shop.cart.count());
  });
</script>
  • CDN 로더는 type="module" 이라 DOMContentLoaded 전에 부트하고 saeroon:shop-ready 를 쏩니다. DOMContentLoaded 안에서 리스너를 달면 놓칩니다 — 아래처럼 「이미 떠 있으면 바로, 아니면 이벤트」로 기다리세요.
function whenShopReady(cb) {
  if (window.saeroonShop) cb(window.saeroonShop);
  else window.addEventListener('saeroon:shop-ready', (e) => cb(e.detail.shop), { once: true });
}
  • 번들러를 쓰는 앱은 @saeroon/shop-sdk 를 import 해 createShop({ siteSlug, apiBase, provider: 'toss' }) 로 같은 인터페이스를 만듭니다(npm 발행은 별도 게이트).
  • 모든 메서드는 { ok, status, body, error } 봉투를 돌려주고 HTTP 오류에 throw 하지 않습니다.

2. 상품 진열

목록 컨테이너 안에 <template data-product> 를 두면 상품마다 그 마크업이 복제됩니다. 디자인은 그대로, 값만 data-product-field 자리에 들어갑니다.

<section data-saeroon-product-list data-page-size="12" data-detail-base="/product.html">
  <template data-product>
    <article>
      <img data-product-field="thumbnail" alt="">
      <h3 data-product-field="name"></h3>
      <p><s data-product-field="compareAtPrice"></s> <b data-product-field="price"></b> <em data-product-field="discountPercentage"></em></p>
      <span data-product-field="isAvailable"></span>
      <a data-product-link>자세히</a>
      <button data-saeroon-add-to-cart data-toast="담았습니다">담기</button>
      <button data-saeroon-wishlist-toggle>♡</button>
    </article>
  </template>
  <p data-saeroon-empty hidden>등록된 상품이 없습니다.</p>
</section>
  • data-category-id 로 카테고리를 거르고, data-page-size(기본 20)·data-page 로 페이지를 정합니다. 카테고리 목록은 saeroonShop.products.categories().
  • 필드: name · price(22,800원) · compareAtPrice(정가, 없으면 숨김) · discountPercentage · description · thumbnail/image · slug · stock · isAvailable(구매 가능/품절) · category.
  • [data-product-link] 의 href 는 data-detail-base?product=<slug> 가 됩니다. 복제본 안의 담기·찜 버튼에는 상품 id 가 자동으로 붙습니다.
  • 상품 이미지 목록(imageUrls)은 API 가 JSON 문자열로 줍니다. 직접 쓸 때는 parseImageUrls(product.imageUrls) 로 푸세요 — "{}" 도 참으로 평가되는 값입니다.

상세 페이지는 컨테이너에 data-product-slug 를 적거나 URL ?product=<slug> 로 엽니다.

<article data-saeroon-product-detail>
  <div data-product-images><template data-product-image><img data-product-field="image" alt=""></template></div>
  <h1 data-product-field="name"></h1>
  <p data-product-field="price"></p>
  <p data-product-field="description"></p>
  <label>옵션 <select data-product-variants required></select></label>
  <label>수량 <input id="qty" type="number" value="1" min="1"></label>
  <button data-saeroon-add-to-cart data-quantity-from="#qty" data-toast="담았습니다">담기</button>
  <button data-saeroon-add-to-cart data-quantity-from="#qty" data-redirect="/checkout.html">바로 구매</button>
  <div data-saeroon-shop-error></div>
</article>

옵션(사이즈·색상) 상품은 <select data-product-variants> 에 변형이 채워지고, 담기 버튼에 data-variant-from 이 자동 연결됩니다. 옵션을 고르지 않고 누르면 "옵션을 선택해 주세요" 가 오류 슬롯에 뜹니다.

3. 담기 · 카트 배지 · 카트 페이지

담기 버튼은 상품을 GUID(data-product-id) 또는 slug(data-product-slug)로, 수량은 data-quantity 또는 data-quantity-from, 변형은 data-variant-id 또는 data-variant-from 으로 지정합니다.

<button data-saeroon-add-to-cart data-product-slug="rose-bouquet" data-variant-id="{variantId}" data-quantity="2">담기</button>
<span data-saeroon-cart-count>0</span>
<div data-saeroon-shop-error></div>

카트 페이지는 서버 카트가 진실입니다(브라우저 저장소 없음). 목록은 cart.current() 로 그리고 saeroon:cart-change 로 갱신합니다.

<ul id="cart"></ul>
<p>소계 <span data-saeroon-cart-subtotal></span> · 배송비 <span data-saeroon-shipping-cost></span> · 합계 <b data-saeroon-order-total></b></p>
<script>
  function paint(cart) {
    document.getElementById('cart').innerHTML = (cart?.items ?? []).map((i) =>
      `<li>${i.productName} ${i.selectedOptions ?? ''} × <input type="number" value="${i.quantity}" min="1" data-pid="${i.productId}"> ${i.totalPrice.toLocaleString()}원
         <button data-remove="${i.productId}">삭제</button></li>`).join('') || '<li>비어 있습니다.</li>';
  }
  window.addEventListener('saeroon:cart-change', (e) => paint(e.detail.cart));
  window.addEventListener('saeroon:shop-ready', (e) => e.detail.shop.ready.then(() => paint(e.detail.shop.cart.current())));
  document.addEventListener('change', (e) => { const t = e.target; if (t.dataset.pid) window.saeroonShop.cart.update(t.dataset.pid, Number(t.value)); });
  document.addEventListener('click', (e) => { const t = e.target; if (t.dataset.remove) window.saeroonShop.cart.remove(t.dataset.remove); });
</script>

4. 배송비 미리보기 · 우편번호

결제 전에 배송비를 보여 줍니다. 우편번호가 비면 제주·도서산간 같은 권역 추가배송비가 조용히 0 이 되므로, 우편번호 검색 버튼을 꼭 두세요(Kakao 우편번호 서비스, 키 불필요).

<form id="checkout-form">
  <input name="shippingName" placeholder="받는 분" required>
  <input name="shippingPhone" placeholder="연락처" required>
  <input name="shippingPostalCode" id="zip" placeholder="우편번호" readonly>
  <button type="button" data-saeroon-address-search data-zip-target="#zip" data-address-target="#addr" data-detail-target="#addr2">우편번호 찾기</button>
  <input name="shippingAddress" id="addr" placeholder="주소" readonly>
  <input id="addr2" placeholder="상세주소">
  <input name="shippingMemo" placeholder="배송 메모">
  <input name="customerEmail" type="email" placeholder="이메일 (주문 확인 메일)">
</form>
<p>소계 <span data-saeroon-cart-subtotal></span></p>
<p>배송비 <span data-saeroon-shipping-cost data-zip-from="#zip"></span> <small data-saeroon-shipping-cost data-shipping-field="matchedZoneName"></small></p>
<p>합계 <b data-saeroon-order-total></b></p>
  • 배송비는 카트 변경·우편번호 입력·주소 선택마다 다시 계산됩니다. 무료배송 임계를 넘으면 "무료".
  • data-embed="#wrap" 을 주면 팝업 대신 그 요소 안에 검색창이 열립니다.
  • 표시는 안내용입니다 — 결제 금액은 서버가 주문을 만들 때 다시 계산합니다.

5. 체크아웃 (결제 시작)

동의 체크와 결제수단을 받은 뒤 saeroonShop.checkout() 을 부릅니다. 주문이 서버에 만들어지고 토스 결제창이 열리며, 성공하면 successUrl 로 이동합니다.

<div data-saeroon-checkout-consents></div>   <!-- 이용약관·개인정보(필수)·마케팅(선택) 체크박스 자동 렌더 -->
<label><input type="radio" name="pay" value="CARD" checked> 카드</label>
<label><input type="radio" name="pay" value="VIRTUAL_ACCOUNT"> 가상계좌</label>
<label><input type="radio" name="pay" value="TOSSPAY"> 토스페이</label>
<button id="pay-btn">결제하기</button>
<div data-saeroon-shop-error></div>
<script>
  document.getElementById('pay-btn').addEventListener('click', async () => {
    const f = document.getElementById('checkout-form');
    const v = (n) => f.elements[n]?.value?.trim() ?? '';
    const shop = window.saeroonShop;
    const items = shop.cart.current()?.items ?? [];
    const r = await shop.checkout({
      request: {
        shippingName: v('shippingName'), shippingPhone: v('shippingPhone'),
        shippingAddress: [v('shippingAddress'), document.getElementById('addr2').value].filter(Boolean).join(' '),
        shippingPostalCode: v('shippingPostalCode'), shippingMemo: v('shippingMemo'),
        customerEmail: v('customerEmail'),                 // 주문 확인 메일(백엔드 지원 시)
        // couponCode: 'WELCOME', pointsToRedeem: 0,       // 쿠폰·포인트(선택)
        // consents: [...]                                  // 비우면 문서의 [data-consent-type] 을 자동 수집
      },
      method: document.querySelector('input[name="pay"]:checked').value,   // CARD · TRANSFER · VIRTUAL_ACCOUNT · TOSSPAY · KAKAOPAY · NAVERPAY
      orderName: items.length > 1 ? `${items[0].productName} 외 ${items.length - 1}건` : items[0]?.productName ?? '주문',
      successUrl: location.origin + '/order-complete.html',
      failUrl: location.origin + '/checkout.html?fail=1',
      customerEmail: v('customerEmail'),
    });
    if (!r.ok) document.querySelector('[data-saeroon-shop-error]').textContent = r.error;   // 필수 동의 누락 · 재고 부족 · 빈 카트 등
  });
</script>
  • 필수 동의(required)가 빠지면 주문을 만들지 않고 { ok:false, reason:'consent' } 를 돌려줍니다.
  • 쿠폰은 saeroonShop.coupons.validate(code, subtotal) 로 미리 검증하고, 적용은 request.couponCode(여러 장은 couponCodes)로 합니다.
  • 결제창에서 손님이 취소하면 조용히 돌아옵니다(예외 없음). 실패는 failUrl 로 이동하며 토스가 ?code=&message= 를 붙입니다.

6. 주문완료 (결제 확정)

successUrl 페이지에서 confirmFromRedirect() 를 부르면 토스가 붙인 paymentKey·orderId·amount 로 서버 승인을 하고 결과를 다섯 갈래로 돌려줍니다.

<h1 id="title">확인 중…</h1>
<dl id="summary"></dl>
<script>
  window.addEventListener('saeroon:shop-ready', async (e) => {
    const out = await e.detail.shop.confirmFromRedirect();
    const t = document.getElementById('title'), s = document.getElementById('summary');
    switch (out.status) {
      case 'confirmed':          // 결제 완료 — out.order = 전체 주문(품목·배송지 포함)
        t.textContent = `주문 ${out.order.orderNumber} 이 완료되었습니다.`;
        s.innerHTML = out.order.items.map((i) => `<dt>${i.productName}</dt><dd>${i.quantity}개</dd>`).join('');
        break;
      case 'waiting-deposit':    // 가상계좌 — 입금 안내
        t.textContent = `${out.order.virtualAccountBank} ${out.order.virtualAccountNumber} (${out.order.virtualAccountHolder}) 로 ${new Date(out.order.virtualAccountDueDate).toLocaleString()} 까지 입금해 주세요.`;
        break;
      case 'already-processed':  // 새로고침·뒤로가기 — out.order 는 공개 상태(품목 없음)
        t.textContent = `주문 ${out.order.orderNumber} 은 이미 결제되었습니다.`;
        break;
      case 'no-params':          // 결제 없이 직접 진입
        location.replace('/'); break;
      case 'error':
        t.textContent = `결제 확인 실패: ${out.error}`; break;
    }
  });
</script>

already-processed 의 order 는 개인정보 없는 11필드(PublicOrderStatusDto)라 품목이 없습니다. 품목·배송지를 다시 보여 주려면 아래 주문조회(orders.lookup)를 쓰세요.

7. 주문조회 · 배송추적

비회원은 주문번호 + 주문 시 전화번호로 조회합니다. 조회가 되면 같은 페이지의 추적·반품 영역도 이어서 채워집니다(추적은 X-Order-Phone 헤더 자격 — SDK 가 붙입니다).

<form data-saeroon-order-lookup>
  <input name="orderNumber" placeholder="주문번호 (SH…)" required>
  <input name="phone" placeholder="주문 시 전화번호" required>
  <button>조회</button>
  <p data-saeroon-order-error hidden></p>
</form>

<section data-saeroon-order-result hidden>
  <p><b data-order-field="orderNumber"></b> · <span data-order-field="statusLabel"></span> · <span data-order-field="createdAt"></span></p>
  <ul data-saeroon-order-items>
    <template data-order-item><li><span data-order-item-field="productName"></span> <small data-order-item-field="optionSnapshot"></small> × <span data-order-item-field="quantity"></span> = <span data-order-item-field="totalPrice"></span></li></template>
  </ul>
  <p>배송지 <span data-order-field="shippingName"></span> · <span data-order-field="shippingAddress"></span></p>
  <p>결제 <span data-order-field="total"></span> (배송비 <span data-order-field="shippingCost"></span>)</p>

  <div data-saeroon-order-tracking hidden>
    <p><span data-tracking-field="carrier"></span> <span data-tracking-field="trackingNumber"></span> — <span data-tracking-field="currentStatus"></span>
       <a data-tracking-link target="_blank" rel="noopener">택배사에서 조회</a></p>
    <ol data-tracking-events><template data-tracking-event><li><time data-tracking-field="time"></time> <span data-tracking-field="status"></span> <span data-tracking-field="location"></span></li></template></ol>
  </div>

  <ul data-saeroon-order-returns hidden>
    <template data-return-row><li><span data-return-field="returnNumber"></span> <span data-return-field="typeLabel"></span> · <span data-return-field="statusLabel"></span></li></template>
  </ul>
  <button type="button" data-saeroon-return-open>반품·교환 신청</button>
</section>
  • 주문번호는 비밀이 아니므로 전화번호 대조가 자격입니다. 전화번호가 틀리면 404("주문을 찾을 수 없습니다"), 헤더가 빠지면 400 — 둘 다 [data-saeroon-order-error] 에 사용자 문구로 표시됩니다.
  • 배송 추적 이벤트는 현재 주문 상태 시각으로 합성됩니다(택배사 API 연동은 후속). 송장번호·택배사 딥링크는 운영자가 송장을 입력한 뒤 나옵니다.
  • 회원 주문내역(orders.mine)·포인트·내 리뷰·리뷰 수정/삭제는 사이트 회원 토큰(samt_…)으로 열립니다. createShop(config) 의 accessTokenProvider 로 토큰을 넘기면 SDK 가 Authorization: Bearer 로 싣습니다(플랫폼 로그인 토큰은 회원이 아니라 401). 리뷰 작성은 회원 본인 주문이거나 phone(주문자 전화 대조)이면 됩니다.
  • 고객 취소 = POST /api/v1/public/sites/{siteSlug}/shop/orders/{orderNumber}/cancel — 본문 { phone?, reason?, refundReceiveAccount? }. 회원 본인 또는 전화 대조가 맞아야 하고(아니면 403), 미결제(가상계좌 미입금 포함)는 취소, 결제 직후(Paid)는 전액 환불, 준비·배송 중은 400(반품·교환 신청으로). 조회 응답의 canCancel 이 그 가능 여부입니다. SDK 메서드(orders.cancel)는 후속입니다.

8. 반품·교환 신청

조회한 주문에서 품목을 골라 신청합니다. 폼은 기본 숨김이고 [data-saeroon-return-open] 이 엽니다.

<form data-saeroon-return-form hidden>
  <label><input type="radio" name="type" value="Return" checked> 반품</label>
  <label><input type="radio" name="type" value="Exchange"> 교환</label>
  <div data-saeroon-return-items></div>        <!-- 반품 가능 품목이 체크박스 + 수량으로 렌더 -->
  <textarea name="reason" placeholder="사유" required></textarea>
  <button>신청</button>
  <p data-saeroon-return-result hidden></p>
  <p data-saeroon-order-error hidden></p>
</form>

품목·사유가 비면 네트워크 없이 오류 문구가 뜨고, 접수되면 접수번호가 [data-saeroon-return-result] 에 표시됩니다. 취소 요청 통로(결제 전·직후 취소)는 아직 없습니다 — 운영자에게 연락하도록 안내하세요.

9. 위시리스트(찜)

<button data-saeroon-wishlist-toggle data-product-id="{id}">♡</button>
<span data-saeroon-wishlist-count>0</span>
<ul data-saeroon-wishlist-list data-detail-base="/product.html">
  <template data-wishlist-item><li><span data-wishlist-field="productName"></span> <span data-wishlist-field="price"></span> <button data-saeroon-add-to-cart>담기</button> <button data-saeroon-wishlist-toggle>삭제</button></li></template>
  <li data-saeroon-empty hidden>찜한 상품이 없습니다.</li>
</ul>

세션(카트와 같은 쿠키) 기반이라 로그인과 합쳐지지 않습니다. 토글 버튼은 aria-pressed 와 data-saeroon-wishlisted="true|false" 로 상태를 드러냅니다.

10. 이벤트

이벤트대상detail
saeroon:shop-readywindow{ shop } — 부트 완료. provider 미지원이면 발행되지 않음
saeroon:cart-changewindow{ cart } — 담기·수정·삭제·새로고침
saeroon:products-renderedwindow{ root, products, page, total }
saeroon:product-loadedwindow{ root, product }
saeroon:shipping-changewindow{ subtotal, zipCode, shipping, total, error }
saeroon:address-selecteddocument{ address, button }
saeroon:order-loadedwindow{ order }
saeroon:return-createdwindow{ return }
saeroon:wishlist-changewindow{ items }
saeroon:toastdocument{ message, error? } — 담기·찜·주소 성공/실패

11. 메서드

window.saeroonShop 의 공개 표면입니다. 이 표는 SDK 소스(SaeroonShopApi)에서 자동 생성되며 시험이 드리프트를 막습니다(pnpm --filter @saeroon/shop-sdk gen:docs).

<!-- shop-sdk:methods:start -->

멤버인자반환설명
saeroonShop.config—ShopConfig부트 설정(siteSlug · apiBase · provider · sessionHeader).
saeroonShop.client—ShopApiClient저수준 클라이언트 — 공개 32경로 37 오퍼레이션 전부. 표의 메서드들은 이것을 감싼 것이다.
saeroonShop.cart—CartController세션 카트 — refresh · add · addBySlug · update · remove · clear · current · count.
saeroonShop.products—(그룹)상품 조회. 목록·상세는 60초 캐시(공개 API 는 IP 당 분당 30회).
saeroonShop.products.list`(page?: number, pageSize?: number, categoryId?: string \undefined)`Promise<ApiResult<PagedResponse<PublicShopProductDto>>>상품 목록(page · pageSize · categoryId).
saeroonShop.products.get(slug: string)Promise<ApiResult<PublicShopProductDto>>상품 상세(slug). imageUrls 는 JSON 문자열 — parseImageUrls 로 푼다.
saeroonShop.products.search(query?: SearchProductsQuery)Promise<ApiResult<ProductSearchResultDto>>검색(q · categoryId · priceMin/Max · inStockOnly · sort · page).
saeroonShop.products.autocomplete(q: string, maxResults?: number)Promise<ApiResult<AutocompleteItemDto[]>>검색어 자동완성.
saeroonShop.products.categories()Promise<ApiResult<PublicShopCategoryDto[]>>카테고리 트리(활성만).
saeroonShop.products.recommendations(productId: string, maxResults?: number)Promise<ApiResult<ProductRecommendationsDto>>연관 상품 추천.
saeroonShop.products.popular(maxResults?: number)Promise<ApiResult<RecommendedProductDto[]>>인기 상품.
saeroonShop.orders—(그룹)주문 조회·추적·반품. 비회원은 주문번호 + 주문 시 전화번호로 대조한다.
saeroonShop.orders.get(orderNumber: string)Promise<ApiResult<PublicOrderStatusDto>>공개 상태만(개인정보 없는 11필드). 품목·배송지는 lookup.
saeroonShop.orders.lookup(input: LookupOrderInput)Promise<ApiResult<PublicOrderDetailDto>>비회원 주문조회 — 전체 주문(품목·배송지·송장).
saeroonShop.orders.tracking(orderNumber: string, phone: string)Promise<ApiResult<DeliveryTrackingInfo>>배송 추적(택배사·송장·이벤트). 전화번호 없으면 400 을 사용자 문구로 돌려준다.
saeroonShop.orders.returns(orderNumber: string, phone: string)Promise<ApiResult<ShopReturnDto[]>>반품·교환 신청 목록.
saeroonShop.orders.createReturn(orderNumber: string, input: CreateReturnInput)Promise<ApiResult<ShopReturnDto>>반품·교환 신청(items 는 orderItemId).
saeroonShop.orders.mine(page?: number, pageSize?: number)Promise<ApiResult<PagedResponse<OrderSummaryDto>>>회원 주문 목록 — 사이트 회원 토큰(accessTokenProvider) 필요. 없으면 401.
saeroonShop.shipping—(그룹)배송비·주소.
saeroonShop.shipping.estimate(input: EstimateShippingInput)Promise<ApiResult<ShippingCostCalculationResult>>배송비 미리보기(소계 · 우편번호). 우편번호가 비면 권역 추가금 0.
saeroonShop.shipping.openAddressSearch`(opts?: { doc?: Document \undefined; embedTarget?: HTMLElement \null \undefined; })``Promise<AddressResult \null>`우편번호·도로명주소 검색창(Kakao Postcode, 키 불필요). 완료 시 주소, 닫으면 null.
saeroonShop.shipping.settings()Promise<ApiResult<PublicShopSettingsDto>>배송비 정책(기본 배송비 · 무료배송 임계 · 권역).
saeroonShop.coupons—(그룹)쿠폰 검증(할인액 미리보기). 적용은 체크아웃 요청의 couponCode/couponCodes.
saeroonShop.coupons.validate(code: string, subtotal: number)Promise<ApiResult<CouponValidationResultDto>>쿠폰 1장.
saeroonShop.coupons.validateStacked(codes: string[], subtotal: number)Promise<ApiResult<StackedCouponValidationResultDto>>여러 장 중복 적용.
saeroonShop.wishlist—(그룹)위시리스트 — 회원이면 회원 기준, 아니면 세션 기준.
saeroonShop.wishlist.list()Promise<ApiResult<WishlistItemDto[]>>찜 목록.
saeroonShop.wishlist.add(productId: string)Promise<ApiResult<{ message: string; }>>찜 추가.
saeroonShop.wishlist.remove(productId: string)`Promise<ApiResult<{ message: string; } \null>>`찜 해제.
saeroonShop.wishlist.check(productId: string)Promise<ApiResult<{ isWishlisted: boolean; }>>찜 여부.
saeroonShop.reviews—(그룹)리뷰. 작성 = 회원 본인 주문 또는 phone 대조 · 수정·삭제·내 리뷰 = 사이트 회원 토큰.
saeroonShop.reviews.list`(productId: string, page?: number, pageSize?: number, rating?: number \undefined)`Promise<ApiResult<PagedResponse<ShopReviewDto>>>상품 리뷰 목록(별점 필터).
saeroonShop.reviews.stats(productId: string)Promise<ApiResult<ReviewStatsDto>>리뷰 통계(평균 · 개수 · 분포).
saeroonShop.reviews.create(req: CreateReviewRequest)Promise<ApiResult<ShopReviewDto>>리뷰 작성.
saeroonShop.reviews.mine()Promise<ApiResult<ShopReviewDto[]>>내 리뷰.
saeroonShop.reviews.update(reviewId: string, req: UpdateReviewRequest)Promise<ApiResult<ShopReviewDto>>리뷰 수정.
saeroonShop.reviews.remove(reviewId: string)Promise<ApiResult<null>>리뷰 삭제.
saeroonShop.points—(그룹)포인트 — 사이트 회원 토큰 필요(잔액은 이력이 없어도 0 으로 응답).
saeroonShop.points.balance()Promise<ApiResult<CustomerPointDto>>잔액.
saeroonShop.points.transactions(page?: number, pageSize?: number)Promise<ApiResult<PointTransactionListDto>>거래 내역.
saeroonShop.site—(그룹)사이트 정보.
saeroonShop.site.businessInfo()Promise<ApiResult<PublicBusinessInfoDto>>사업자 정보(전자상거래법 표시 의무 항목 — 상호 · 대표 · 사업자번호 · 통신판매업 · 주소 · 연락처).
saeroonShop.consents`(scope?: ParentNode \undefined)`CollectedConsents문서의 [data-consent-type] 동의 체크 상태 수집(필수 미체크 = missing).
saeroonShop.checkout(opts: StartCheckoutOptions)Promise<StartCheckoutResult>주문 생성 → 결제창. 필수 동의가 빠지면 주문을 만들지 않는다. 성공 시 successUrl 로 이탈.
saeroonShop.confirmFromRedirect`(search?: string \undefined)`Promise<ConfirmOutcome>결제 복귀 페이지에서 승인 — confirmed · waiting-deposit · already-processed · no-params · error 5분기.
saeroonShop.bindings—ShopBindings선언 바인더 핸들(wishlist 컨트롤러 · 주문조회 세션 · 배송비 재계산 · 진열 완료 Promise).
saeroonShop.ready—Promise<void>부트 완료(DOM 배선 + 카트 1회 조회).

<!-- shop-sdk:methods:end -->

12. 사업자정보·환불정책 표시 (전자상거래법)

통신판매업자는 상호·대표자·사업자등록번호·통신판매업신고번호·주소·연락처를 사이트에 표시해야 하고, 청약철회(환불·교환) 조건을 결제 전에 알려야 합니다. 사업자 정보는 대시보드에 입력하면 site.businessInfo() 로 받을 수 있습니다.

<footer id="biz"></footer>
<script>
  whenShopReady(async (shop) => {          // §1 의 헬퍼
    const r = await shop.site.businessInfo();
    if (!r.ok) return;
    const b = r.body;
    document.getElementById('biz').textContent =
      `${b.companyName} · 대표 ${b.representativeName} · 사업자등록번호 ${b.businessRegistrationNumber} · 통신판매업 ${b.onlineBusinessReportNumber} · ${b.address} · ${b.phone} · ${b.email}`;
  });
</script>

환불·교환 정책과 이용약관·개인정보처리방침 페이지는 사이트가 직접 작성합니다(양식 = PRIVACY_TERMS_TEMPLATE). 체크아웃 동의 체크박스(data-saeroon-checkout-consents)의 라벨에서 그 페이지로 링크하세요.

13. 제약과 함정

  • 호출 한도 — 공개 쇼핑 API 는 IP 당 읽기 120회/분 · 쓰기(카트·체크아웃·찜·리뷰·취소·반품 신청·주문조회) 30회/분 · 결제 승인 10회/분입니다(세 버킷은 서로 소진하지 않습니다). SDK 는 부트에 카트 1회, 목록·상세·카테고리·설정을 60초 캐시하고, 찜 상태는 목록 1회 조회로 칠합니다. 페이지에서 직접 client.* 를 반복 호출하지 마세요.
  • 커스텀 도메인 — 카트 세션은 shop_session 쿠키(SameSite=Lax)입니다. *.saeroon.com 밖 도메인에서는 브라우저가 쿠키를 싣지 않아 카트가 비어 보일 수 있습니다. 백엔드는 X-Shop-Session 헤더를 쿠키보다 먼저 읽습니다(허용 목록 등재 완료). SDK 쪽 헤더 전송(data-session-header)은 그 백엔드가 배포된 뒤 켜세요 — 그 전에 켜면 프리플라이트가 헤더를 거부해 요청 자체가 막힙니다.
  • 회원 — 공개 쇼핑 API 의 회원 경로는 사이트 회원 토큰을 받습니다(호스팅 계정 토큰은 회원이 아닙니다). @saeroon/web-sdk 는 토큰을 메모리에만 두고 페이지에 노출하지 않으므로 같은 페이지에서 두 SDK 를 잇는 훅(accessTokenProvider 에 넣을 값)은 후속입니다. *.saeroon.com 서브도메인 사이트는 회원 세션 쿠키가 같은 사이트로 실려 별도 배선 없이 회원으로 해석됩니다.
  • 결제 provider — toss 만. 위젯 키(gck)가 아니라 개별 키(ck) 흐름이라 결제창은 SDK 가 엽니다.
  • imageUrls — JSON 문자열. parseImageUrls().
  • 테스트 — 결제창은 실제 토스 테스트 키가 있어야 열립니다. 로컬에서는 packages/shop-sdk/examples/ 의 스텁 서버로 결제 직전까지 흐름을 검증할 수 있습니다.

14. 속성 요약

단계속성
진열data-saeroon-product-list · data-saeroon-product-detail · data-product·data-product-field·data-product-link·data-product-images·data-product-variants
담기data-saeroon-add-to-cart(data-product-id/slug · data-quantity/-from · data-variant-id/-from · data-toast · data-redirect) · data-saeroon-cart-count · data-saeroon-shop-error
배송비·주소data-saeroon-address-search(data-zip-target·data-address-target·data-detail-target·data-embed) · data-saeroon-cart-subtotal · data-saeroon-shipping-cost(data-zip-from·data-shipping-field) · data-saeroon-order-total
체크아웃data-saeroon-checkout-consents(data-consent-types) · data-consent-type
주문조회·추적data-saeroon-order-lookup · data-saeroon-order-result(data-order-field) · data-saeroon-order-items(data-order-item·data-order-item-field) · data-saeroon-order-tracking(data-tracking-field·data-tracking-link·data-tracking-events) · data-saeroon-order-returns(data-return-row·data-return-field) · data-saeroon-order-error
반품data-saeroon-return-open · data-saeroon-return-form · data-saeroon-return-items(data-return-item) · data-saeroon-return-result
찜data-saeroon-wishlist-toggle · data-saeroon-wishlist-count · data-saeroon-wishlist-list(data-wishlist-item·data-wishlist-field)

전체 속성 표(값·필수·동작)는 Attributes를 참고하세요.

15. 상품 올리기 — AI·CLI·REST (운영자 통로)

위 §0~§14 는 방문자 쪽(스토어프론트)입니다. 팔 상품은 운영자 쪽에서 올립니다 — 대시보드 폼 말고도 세 통로가 같은 백엔드를 탑니다. 셋 다 sk_live_ 키(npx @saeroon/cli login)로 부르고, 키는 키 소유자의 사이트에만 닿습니다(다른 사이트 = 403).

통로무엇비고
MCPsaeroon_define_productsdry_run: true 로 먼저 불러 행별 계획을 보고, false 로 커밋
CLI`shop products import <products.json\.csv> --site <id> → --commit`기본이 dry-run · list · export 도 있음
RESTPOST /api/v1/hosting/sites/{siteId}/shop/products/import (JSON) · …/import/csv (text/csv) · GET …/export[?format=csv]Authorization: Bearer sk_live_…

규칙은 하나입니다.

  • slug 가 자연키입니다. 있는 slug 는 부분 갱신(안 보낸 필드 유지), 없는 slug 는 새 상품(name·price 필수). 같은 파일을 다시 보내면 결과가 같습니다(멱등).
  • 카테고리는 categorySlugs 로 가리킵니다. 모르는 slug 가 하나라도 있으면 그 행만 오류로 남고 저장되지 않습니다 — 먼저 대시보드나 POST …/shop/categories 로 만드세요.
  • 응답은 행마다 create / update / error(+사유)입니다. dryRun: true 면 아무것도 쓰지 않습니다. 오류 행은 커밋에서 빠지고 나머지는 저장됩니다.
  • status 는 draft(기본) / active / inactive / soldOut. active 이고 쇼핑 기능이 켜져 있어야(대시보드 카드 또는 npx @saeroon/cli feature attach shop --site <id>) 스토어프론트에 보입니다.
  • 옵션·변형(options + variants)은 JSON 통로에서만 받고, 보내면 세트째 교체됩니다. CSV 는 열 slug,name,description,price,compareAtPrice,stock,trackInventory,status,thumbnailUrl,imageUrls,categorySlugs,lowStockThreshold,sortOrder(목록 열은 | 구분)이고 모르는 열은 파일 전체가 400 입니다.
  • 한 번에 500행. 결제·환불·정산은 이 통로에 없습니다(대시보드 전용).
{ "items": [
  { "slug": "linen-shirt", "name": "린넨 셔츠", "price": 39000, "stock": 10, "status": "active", "categorySlugs": ["clothing"] },
  { "slug": "old-cap", "price": 12000 }
], "dryRun": true }

export 가 돌려주는 JSON 은 그대로 import 에 되먹일 수 있습니다(id 는 무시됩니다). 상품 상세·수정·삭제(GET/PATCH/DELETE …/shop/products/{id})와 카테고리 CRUD(…/shop/categories)도 같은 키로 부를 수 있습니다.

요약

정적 사이트에 쇼핑몰을 붙입니다. 진열·담기·배송비·주문조회·반품·찜은 HTML 속성(data-saeroon-*)으로 선언하고, 결제 시작과 결제 완료 처리 두 곳만 스크립트(window.saeroonShop)를 한 줄씩 부릅니다. 상품·재고·배송비 정책·주문 처리는 대시보드에서 운영자가 관리합니다.

마크다운 원문/docs/shop.md