상품·옵션·재고 관리
상품 등록, 옵션 조합별 재고, 판매 상태와 즉시할인
옵션이 있는 상품을 등록하고 조합별 재고·판매 상태·즉시할인을 관리합니다.
준비
- 권한: 소유자·관리자·스태프 또는
product:rw - 카테고리: 상점 관리 › 상품 › 카테고리에서 먼저 만듭니다.
1. 상품 등록
상점 플랫폼에서 상점 관리 › 상품 › 상품 등록으로 이동합니다(소유자·관리자·스태프).
- 카테고리를 고릅니다. 하위 카테고리가 없는 카테고리만 됩니다.
- 아래 섹션을 채웁니다. 배송이 필요 없는 상품은 배송 정책에서 배송 없음을 고릅니다. 8절
- **[등록]**을 누릅니다. 수정 화면에서는 **[저장]**입니다.
| 섹션 | 입력 |
|---|---|
| 상품명 | 100자까지 |
| 이미지 | 10장까지. 첫 장이 대표입니다. 상품 이미지 |
| 판매 정보 | 판매가(최소 10원, 10원 단위), 정가(할인 전 가격 표시용), 재고 |
| 상세 설명 | **[상세설명 작성]**을 누르면 새 창에 편집기가 열립니다 |
| 옵션 | 옵션 축과 값, 조합별 재고. 옵션이 없으면 비워 둡니다 |
| 배송 정책 | 배송 있음/없음. 배송 있음이면 배송비·출고지·반품지. 7절 |
| 환불 정책 | 비우면 상점 기본을 씁니다. 6절 |
완료 기준: 상점 관리 › 상품 › 상품 목록과 스토어프론트 목록에 상품이 판매중으로 보입니다.
API: POST /v1/products
2. 옵션과 조합
옵션은 축(예: 색상)과 값(예: 블랙)입니다. 모든 조합(예: 블랙 / M)이 자동으로 만들어집니다.
축은 3개, 축당 값은 100개, 조합은 500개까지입니다.
| 조합 항목 | 설명 |
|---|---|
| 판매 | 끄면 구매자 화면에 품절로 보이고 주문할 수 없습니다 |
| 추가금액 | 판매가에 더하는 금액(원) |
| 재고 | 판매 가능 수량 |
| SKU 정보 | 바코드·판매자 코드·원가·무게. 내부 관리용 선택 입력 |
- [값 일괄 추가]: 값을 쉼표로 이어 입력합니다(예:
블랙, 화이트, 레드). - [재고 일괄 적용]: 모든 조합의 재고를 같은 수로 덮어씁니다.
- 주문에 담긴 조합은 지울 수 없습니다. 삭제 대신 판매를 끕니다.
추가 선택(재고 없이 값별 추가금만 있는 옵션)·직접 입력·정렬 순서·저장 옵션은 상품 옵션 구성을 봅니다.
API: 옵션 구조 전체 교체는 PUT /v1/products/{productId}/options입니다. 스토어프론트·주문의 optionId는 조합의 variantId입니다.
3. 재고 변경
재고는 조합 단위입니다. 상품 수정 화면의 옵션 섹션에서 바꾸거나 API로 한 조합씩 바꿉니다.
curl -X PATCH https://api.sayren.app/v1/products/{productId}/variants/{variantId}/stock \
-H "Authorization: Bearer {계정 토큰}" -H "X-Store-Id: {storeId}" -H "Content-Type: application/json" \
-d '{ "stockQuantity": 30 }' # 절대값. 증감은 { "adjustment": 20 }stockQuantity와adjustment중 하나만 보냅니다.- 결제되면 줄고, 취소·반품이 끝나면 늘어납니다.
- 0이 되면 품절로 표시되고 웹훅
PRODUCT.OUT_OF_STOCK이 나갑니다.
날짜별 재고(숙박)
객실처럼 날마다 재고와 가격이 다른 상품은 등록할 때 inventoryMode: "DATED"로 만듭니다. 이 상품은 조합 재고를 쓰지 않고 조합 × 날짜 달력이 재고·가격입니다. 판매 이력이 생기면 재고 방식을 바꿀 수 없습니다(409 INVENTORY_MODE_LOCKED).
PUT /v1/products/{productId}/calendar
{ "variantIds": ["var_…"], "from": "2026-11-01", "to": "2026-11-30", "stock": 3, "price": 120000, "daysOfWeek": [5, 6] }
GET /v1/products/{productId}/calendar?from=2026-11-01&to=2026-11-30- 날짜는 한국 날짜(
YYYY-MM-DD)이고 기간은 끝 날짜를 포함합니다. 한 번에 최대 400일입니다. stock·price는 보낸 것만 바뀝니다.price: null이면 그날 가격을 판매가(조합 추가금 포함)로 되돌립니다.daysOfWeek(0=일요일~6=토요일)를 주면 그 요일만 바뀝니다.- 달력에 없는 날은 재고 0이라 판매하지 않습니다.
- 구매자는
GET /storefront/v1/products/{productId}/calendar?from=&to=로 날마다 예약 가능 여부와 가격을 봅니다(최대 120일, 재고 수는 보이지 않습니다). - 장바구니 담기와 바로 구매에
stay: { "checkIn", "checkOut" }를 함께 보냅니다. 체크인 날부터 체크아웃 전날까지가 박이고(최대 30박) 단가는 박마다 가격의 합입니다. 결제하면 박마다 재고가 줄고 취소하면 그 날들이 돌아옵니다. - 기간이 빠지면
400 STAY_REQUIRED, 일반 상품에 기간을 주면400 STAY_NOT_APPLICABLE, 재고가 없는 날이 끼면409 DATES_UNAVAILABLE입니다. - 주문상품 응답의
stay에 체크인·체크아웃과 박마다 주문 시점 가격(nights)이 남습니다.
4. 판매 상태
| 상태 | 구매자 화면 |
|---|---|
| 판매중 | 보이고 주문할 수 있음 |
| 품절 · 판매중지 · 판매종료 | 목록과 상세에 보이지 않음 |
상점 플랫폼에서 상점 관리 › 상품 › 상품 목록으로 이동합니다(소유자·관리자·스태프).
- 상품을 체크합니다.
- 상태를 고르고 **[선택 N개 상태 변경]**을 누릅니다.
API: POST /v1/products/bulk/status (한 번에 100건)
5. 즉시할인
상점 플랫폼에서 상점 관리 › 상품 › 상품 목록으로 이동합니다(소유자·관리자·스태프).
- 목록에서 상품을 선택합니다.
- 즉시할인 섹션에서 할인 방식(정률 % · 정액 원)과 할인 값, 시작일·종료일을 정합니다.
- **[할인 저장]**을 누릅니다. 상품 저장과 따로 저장됩니다. 없애려면 **[할인 삭제]**입니다.
스토어프론트에 할인가와 할인율이 표시됩니다. API: PUT /v1/products/{productId}/discount
할인의 기준은 판매가입니다. 정가는 비교용 표시 값이라 할인 계산과 결제 금액에 쓰지 않습니다. 구매자가 내는 가격은 할인 기간에는 할인가,
그 밖에는 판매가입니다. 스토어프론트 상품 응답의 originalPrice가 정가이고, 기본 템플릿은 정가가 구매자 가격보다 클 때 정가 취소선과
정가 기준 할인율을 표시합니다. 가격 표시
설정된 할인은 상품 상세(GET /v1/products/{productId})의 discount에도 실립니다. 형태는 GET /v1/products/{productId}/discount의 응답과 같고, 할인이 없으면 null입니다. 할인 조회는 할인이 없을 때 404 DISCOUNT_NOT_FOUND를 돌려주므로, 할인 유무를 확인할 때는 상품 상세를 쓰십시오.
6. 환불 정책 덮어쓰기
상품의 환불 정책 섹션이나 요청의 refundPolicy로 상점 기본 정책을 상품별로 덮어씁니다.
준 항목만 덮어쓰고, 생략하거나 null이면 상점 기본을 씁니다. 항목의 뜻과 계산은 환불 정책과 배송비에 있습니다.
{ "refundPolicy": { "returnDeliveryFee": 5000, "exchangeDeliveryFee": 5000, "preShipmentDeliveryFeeRefund": false } }PUT /v1/products/{productId}에서 refundPolicy를 생략하면 저장된 덮어쓰기가 지워집니다(전체 교체). 유지하려면 조회 응답의 refundPolicy를 다시 보냅니다.
- 상품의 반품·교환 배송비는
refundPolicy의returnDeliveryFee·exchangeDeliveryFee로만 정합니다. 옛 필드returnFee·exchangeFee는 없어졌고 보내면400입니다. - 배송 없는 상품(
MANUAL)에returnDeliveryFee·exchangeDeliveryFee를 주면400 REFUND_POLICY_NOT_APPLICABLE입니다.PATCH로 배송 상품을 배송 없는 상품으로 바꾸면서refundPolicy를 생략하면 저장된 두 항목을 지웁니다(PUT에서 생략하면 덮어쓰기 전체가 지워집니다). - 주문의 환불은 결제 시점에 굳힌 정책으로 계산합니다. 상품의 정책을 나중에 바꿔도 이미 들어온 주문의 환불 금액은 바뀌지 않습니다.
- 조회 응답의
refundPolicy는 덮어쓴 항목만 담고, 없으면null입니다. 모르는 항목 이름은400입니다. - 한 주문에 정책이 다른 상품이 섞이면 상품별로 계산합니다.
7. 배송비와 출고지·반품지
배송 상품은 배송 정책 섹션에서 배송비와 출고지·반품지를 정합니다. 배송비를 비우면 그 시점의 상점 기본 배송비가 저장됩니다.
API에서는 fulfillment.shipping입니다(8절).
같은 출고지 + 같은 배송 정책인 상품은 배송비가 한 번 붙습니다. 필드 규칙은 상품의 배송 정책에 있습니다.
8. 상품 유형
상품의 유형과 유형별 속성은 fulfillment 하나에 있습니다. type이 유형이고 같은 이름의 키가 그 유형의 속성입니다. 유형은 배송 상품(SHIPPING)과 배송 없는 상품(MANUAL) 둘입니다(비실물 상품).
type | 상점 플랫폼 배송 정책 | 속성 키 |
|---|---|---|
SHIPPING | 배송 있음 | shipping: deliveryType · deliveryFee · conditionalFreeAmount · shippingAddressId · returnAddressId |
MANUAL | 배송 없음 | 없음. { "type": "MANUAL" }만 보냅니다 |
{
"fulfillment": {
"type": "SHIPPING",
"shipping": { "deliveryType": "PAID", "deliveryFee": 3000, "conditionalFreeAmount": null,
"shippingAddressId": null, "returnAddressId": null }
},
"fulfillmentTypeLocked": false
}등록과 수정
- 등록(
POST /v1/products):fulfillment를 생략하면 배송 상품입니다. 유형 안의 필드도 생략할 수 있고 기본값을 씁니다(예:{ "type": "SHIPPING" }은 유료 배송). 배송비를 생략하면 상점 기본 배송비, 주소를 생략하면 그 시점 상점 기본 주소를 저장합니다. - 수정(
PUT·PATCH /v1/products/{productId}):fulfillment를 생략하면 바꾸지 않습니다. 주면 통째로 바꾸므로 유형 안의 필드를 모두 보내야 합니다. 현재 값과 합치지 않습니다. 조회 응답의fulfillment를 고쳐 보내면 됩니다. - 주소의
null은 비워 두고 상점 기본 주소를 따른다는 뜻입니다. - 요청 최상위는 정한 필드만 받습니다.
deliveryFee·fulfillmentType·returnFee같은 옛 평면 필드를 보내면 조용히 무시하지 않고400입니다. - 응답의
fulfillmentTypeLocked가true면 판매 이력이 있어 유형을 바꿀 수 없습니다(409 FULFILLMENT_TYPE_LOCKED). 같은 유형 안의 속성은 바꿀 수 있습니다. - 다른 요청이 먼저 유형을 바꿨으면
409 PRODUCT_CHANGED입니다. 상품을 다시 조회한 뒤 보냅니다. - 응답의
type은 값이 늘어날 수 있습니다. SDK는 모르는 유형을{ "type": "OTHER", "rawType": "…" }로 읽습니다.
상점 플랫폼에서는 배송 정책 섹션 맨 위의 배송 있음/없음으로 유형을 고릅니다. 배송 없음으로 바꾸면 배송비·출고지·반품지 입력이 사라지므로 입력한 값이 있으면 확인을 받습니다.
자주 막히는 문제
| 증상 | 해결 |
|---|---|
400 INVALID_PRICE_UNIT | 판매가를 10원 단위로 입력합니다 |
CATEGORY_NOT_LEAF | 하위 카테고리가 없는 가장 아래 카테고리를 고릅니다 |
| 조합을 지울 수 없다는 오류 | 주문에 담긴 조합입니다. 판매를 끕니다 |
| 스토어프론트에 상품이 안 보임 | 상태가 판매중인지 확인합니다 |
상품 수정이 400(옛 필드) | 배송비·유형 필드를 fulfillment 안으로 옮깁니다. 8절 |
409 DATES_UNAVAILABLE | 고른 기간에 달력 재고가 없는 날이 있습니다. 달력을 확인합니다 |
409 FULFILLMENT_TYPE_LOCKED | 판매 이력이 있는 상품입니다. 배송 있음/없음을 바꾸려면 새 상품으로 등록합니다 |