문서 / 기능 붙이기
이 문서의 큐
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-count3종만 보므로, 어느 통로든 안전하게<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-ready | window | { shop } — 부트 완료. provider 미지원이면 발행되지 않음 |
saeroon:cart-change | window | { cart } — 담기·수정·삭제·새로고침 |
saeroon:products-rendered | window | { root, products, page, total } |
saeroon:product-loaded | window | { root, product } |
saeroon:shipping-change | window | { subtotal, zipCode, shipping, total, error } |
saeroon:address-selected | document | { address, button } |
saeroon:order-loaded | window | { order } |
saeroon:return-created | window | { return } |
saeroon:wishlist-change | window | { items } |
saeroon:toast | document | { 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).
| 통로 | 무엇 | 비고 | |
|---|---|---|---|
| MCP | saeroon_define_products | dry_run: true 로 먼저 불러 행별 계획을 보고, false 로 커밋 | |
| CLI | `shop products import <products.json\ | .csv> --site <id> → --commit` | 기본이 dry-run · list · export 도 있음 |
| REST | POST /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)를 한 줄씩 부릅니다. 상품·재고·배송비 정책·주문 처리는 대시보드에서 운영자가 관리합니다.