@sayren/store-sdk 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +557 -11
- package/dist/index.mjs +406 -36
- package/package.json +1 -1
- package/src/client.ts +79 -2
- package/src/index.ts +1 -0
- package/src/schemas/claim.ts +38 -4
- package/src/schemas/common.ts +3 -0
- package/src/schemas/customer.ts +72 -0
- package/src/schemas/order.ts +35 -3
- package/src/schemas/product.ts +84 -10
- package/src/schemas/refund-policy.ts +163 -0
- package/src/schemas/store.ts +85 -0
- package/src/schemas/upload.ts +86 -0
- package/src/schemas/webhook-event.ts +40 -0
package/dist/index.mjs
CHANGED
|
@@ -693,6 +693,7 @@ const changeTypeSchema = z.enum([
|
|
|
693
693
|
"CLAIM.CANCEL_REQUESTED",
|
|
694
694
|
"CLAIM.RETURN_REQUESTED",
|
|
695
695
|
"CLAIM.EXCHANGE_REQUESTED",
|
|
696
|
+
"CLAIM.COMPLETED",
|
|
696
697
|
"ORDER.ADDRESS_CHANGE_REQUESTED"
|
|
697
698
|
]);
|
|
698
699
|
//#endregion
|
|
@@ -732,8 +733,9 @@ const claimSchema = z.object({
|
|
|
732
733
|
reason: claimReasonSchema,
|
|
733
734
|
reasonDetail: z.string().nullable(),
|
|
734
735
|
responsibility: z.enum(["BUYER", "SELLER"]),
|
|
735
|
-
claimDeliveryFee: z.number().int(),
|
|
736
|
-
expectedRefundAmount: z.number().int().nullable(),
|
|
736
|
+
claimDeliveryFee: z.number().int().describe("구매자가 부담하는 반품·교환 배송비. 원 단위"),
|
|
737
|
+
expectedRefundAmount: z.number().int().nullable().describe("접수 시점에 계산한 예상 환불액 — 수량 비율 상품 금액 − 반품 배송비 − 조건부 무료배송 미달 차감. **배송비 환불은 포함되지 않고 `expectedDeliveryFeeRefundAmount`로 따로 온다**(승인 시점의 배송 묶음 상태로 확정되기 때문이다). 교환은 환불이 없어 null이다"),
|
|
738
|
+
expectedDeliveryFeeRefundAmount: z.number().int().nullable().describe("접수 시점에 계산한 예상 배송비 환불액. 배송 묶음(같은 상품의 주문상품)이 모두 취소되거나 판매자 귀책일 때만 0보다 크다. 이 필드가 생기기 전에 접수된 클레임은 null이다"),
|
|
737
739
|
exchangeOption: z.object({
|
|
738
740
|
optionId: z.string(),
|
|
739
741
|
optionName: z.string()
|
|
@@ -787,8 +789,11 @@ const rejectClaimRequestSchema = z.object({
|
|
|
787
789
|
});
|
|
788
790
|
const approveClaimResultSchema = z.object({
|
|
789
791
|
claimId: z.string(),
|
|
790
|
-
refundAmount: z.number().int(),
|
|
791
|
-
deductAmount: z.number().int(),
|
|
792
|
+
refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 금액 + 배송비 환불 − 반품 배송비 − 조건부 무료배송 미달 차감 − 추가 차감(`deductAmount`)"),
|
|
793
|
+
deductAmount: z.number().int().describe("승인 때 셀러가 추가로 차감한 금액"),
|
|
794
|
+
quantity: z.number().int().describe("환불한 수량"),
|
|
795
|
+
deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음 전량 취소나 판매자 귀책일 때만 0보다 크다"),
|
|
796
|
+
deliveryFeeChargeAmount: z.number().int().describe("환불액에서 뺀 **배송비** — 반품·교환 배송비 + 조건부 무료배송 미달 차감. 추가 차감(`deductAmount`)은 포함하지 않는다"),
|
|
792
797
|
refundedAt: z.string()
|
|
793
798
|
});
|
|
794
799
|
//#endregion
|
|
@@ -837,6 +842,64 @@ const customerAddressSchema = z.object({
|
|
|
837
842
|
address1: z.string(),
|
|
838
843
|
address2: z.string().nullable()
|
|
839
844
|
});
|
|
845
|
+
/** 동의 종류 — 이용약관·개인정보 수집이용은 가입 필수, 마케팅 수신은 선택 */
|
|
846
|
+
const consentKindSchema = z.enum([
|
|
847
|
+
"TERMS",
|
|
848
|
+
"PRIVACY",
|
|
849
|
+
"MARKETING"
|
|
850
|
+
]);
|
|
851
|
+
const consentActionSchema = z.enum(["AGREED", "WITHDRAWN"]);
|
|
852
|
+
/**
|
|
853
|
+
* 동의가 일어난 경로. 값이 추가될 수 있어 모르는 값은 그대로 보여 준다.
|
|
854
|
+
* `SIGNUP` 비밀번호 가입 · `SOCIAL_SIGNUP` 소셜 가입 · `DELEGATED` 셀러 자체 회원 연동 ·
|
|
855
|
+
* `RECONSENT` 약관 개정 재동의 · `MEMBER_UPDATE` 구매자가 직접 변경 · `SELLER` 셀러가 처리
|
|
856
|
+
*/
|
|
857
|
+
const consentSourceSchema = z.enum([
|
|
858
|
+
"SIGNUP",
|
|
859
|
+
"SOCIAL_SIGNUP",
|
|
860
|
+
"DELEGATED",
|
|
861
|
+
"RECONSENT",
|
|
862
|
+
"MEMBER_UPDATE",
|
|
863
|
+
"SELLER"
|
|
864
|
+
]);
|
|
865
|
+
/** 동의 이력 한 줄 — 추가만 되고 지워지지 않는다(구매자 탈퇴 시에만 함께 지워진다) */
|
|
866
|
+
const customerConsentSchema = z.object({
|
|
867
|
+
consentId: z.string(),
|
|
868
|
+
kind: consentKindSchema,
|
|
869
|
+
/** 동의한 약관 버전. 마케팅은 버전이 없어 null */
|
|
870
|
+
version: z.string().nullable(),
|
|
871
|
+
/** 동의 시점의 약관 문서 주소. 스토어가 주소를 정하지 않았거나 마케팅이면 null */
|
|
872
|
+
documentUrl: z.string().nullable(),
|
|
873
|
+
action: consentActionSchema,
|
|
874
|
+
source: consentSourceSchema,
|
|
875
|
+
occurredAt: z.string()
|
|
876
|
+
});
|
|
877
|
+
/** 종류별 현재 동의 상태 */
|
|
878
|
+
const customerConsentStateSchema = z.object({
|
|
879
|
+
kind: consentKindSchema,
|
|
880
|
+
agreed: z.boolean(),
|
|
881
|
+
/** 동의한 약관 버전. 마케팅이거나 이력이 없으면 null */
|
|
882
|
+
agreedVersion: z.string().nullable(),
|
|
883
|
+
/** 스토어의 현재 약관 버전. 마케팅은 null */
|
|
884
|
+
currentVersion: z.string().nullable(),
|
|
885
|
+
/** 스토어가 정한 현재 약관 문서 주소. 정하지 않았거나 마케팅이면 null */
|
|
886
|
+
currentDocumentUrl: z.string().nullable(),
|
|
887
|
+
updatedAt: z.string().nullable(),
|
|
888
|
+
/** 약관이 개정돼 다시 받아야 하는가. 마케팅은 항상 false */
|
|
889
|
+
reconsentRequired: z.boolean()
|
|
890
|
+
});
|
|
891
|
+
const customerConsentsSchema = z.object({
|
|
892
|
+
terms: customerConsentStateSchema,
|
|
893
|
+
privacy: customerConsentStateSchema,
|
|
894
|
+
marketing: customerConsentStateSchema,
|
|
895
|
+
/** 필수 동의 중 하나라도 현재 버전이 아니면 true */
|
|
896
|
+
reconsentRequired: z.boolean()
|
|
897
|
+
});
|
|
898
|
+
/** `GET /v1/customers/{id}/consents` 쿼리 — 최근 순이다 */
|
|
899
|
+
const customerConsentListQuerySchema = z.object({
|
|
900
|
+
kind: consentKindSchema.optional().describe("동의 종류 필터. 생략하면 전체"),
|
|
901
|
+
...pageQueryShape()
|
|
902
|
+
});
|
|
840
903
|
/** 구매자 상세 — 휴대폰 전체를 보여 준다 */
|
|
841
904
|
const customerDetailSchema = z.object({
|
|
842
905
|
customerId: z.string(),
|
|
@@ -856,6 +919,7 @@ const customerDetailSchema = z.object({
|
|
|
856
919
|
/** 소셜 연결 시각. 비밀번호는 가입 시각 */
|
|
857
920
|
linkedAt: z.string()
|
|
858
921
|
})).describe("로그인 방법 — 이메일·비밀번호(`password`)와 연결된 소셜 계정"),
|
|
922
|
+
consents: customerConsentsSchema.describe("약관·개인정보·마케팅 동의의 현재 상태. 이력은 `GET /v1/customers/{customerId}/consents`"),
|
|
859
923
|
addresses: z.array(customerAddressSchema),
|
|
860
924
|
stats: customerStatsSchema.extend({
|
|
861
925
|
canceledOrReturnedCount: z.number().int().describe("취소된 주문 상품 + 완료된 반품 수"),
|
|
@@ -1525,10 +1589,12 @@ const orderItemSchema = z.object({
|
|
|
1525
1589
|
optionId: z.string().nullable(),
|
|
1526
1590
|
optionName: z.string().nullable(),
|
|
1527
1591
|
sellerCode: z.string().nullable(),
|
|
1528
|
-
quantity: z.number().int(),
|
|
1592
|
+
quantity: z.number().int().describe("주문 수량"),
|
|
1593
|
+
canceledQuantity: z.number().int().describe("취소·반품으로 환불된 누적 수량. 수량 단위 부분 취소가 없었으면 0이다"),
|
|
1594
|
+
activeQuantity: z.number().int().describe("살아 있는 수량 (`quantity` − `canceledQuantity`). 0이면 주문상품이 전량 취소됐다"),
|
|
1529
1595
|
unitPrice: z.number().int(),
|
|
1530
1596
|
discountAmount: z.number().int(),
|
|
1531
|
-
totalPrice: z.number().int(),
|
|
1597
|
+
totalPrice: z.number().int().describe("주문상품 금액 (할인 반영 단가 × 주문 수량)"),
|
|
1532
1598
|
hold: z.object({
|
|
1533
1599
|
reason: z.string(),
|
|
1534
1600
|
expectedDispatchDate: z.string()
|
|
@@ -1662,12 +1728,17 @@ const cancelBySellerRequestSchema = z.object({
|
|
|
1662
1728
|
"DELIVERY_IMPOSSIBLE",
|
|
1663
1729
|
"ETC"
|
|
1664
1730
|
]),
|
|
1665
|
-
detail: z.string().max(500).optional()
|
|
1731
|
+
detail: z.string().max(500).optional(),
|
|
1732
|
+
quantity: z.number().int().min(1).optional().describe("취소할 수량. 생략하면 남은 수량 전체다. 남은 수량보다 크면 400 `INVALID_QUANTITY`. 일부만 취소하면 주문상품은 상태를 유지한 채 `canceledQuantity`가 늘고, 전 수량이 소진되면 `CANCELED`가 된다")
|
|
1666
1733
|
});
|
|
1667
1734
|
/** `POST /v1/order-items/{orderItemId}/cancel` 응답 — 셀러 직권취소 환불 결과 */
|
|
1668
1735
|
const cancelBySellerResultSchema = z.object({
|
|
1669
1736
|
orderItemId: z.string(),
|
|
1670
|
-
refundAmount: z.number().int(),
|
|
1737
|
+
refundAmount: z.number().int().describe("실제 환불한 금액 — 수량 비율 상품 금액 + 배송비 환불 − 조건부 무료배송 미달 차감"),
|
|
1738
|
+
quantity: z.number().int().describe("취소한 수량"),
|
|
1739
|
+
canceledQuantity: z.number().int().describe("취소 후 누적 취소 수량"),
|
|
1740
|
+
deliveryFeeRefundAmount: z.number().int().describe("환불에 포함된 배송비. 배송 묶음(같은 상품)이 모두 취소됐고 환불 정책이 켜져 있을 때만 0보다 크다"),
|
|
1741
|
+
deliveryFeeChargeAmount: z.number().int().describe("조건부 무료배송 기준 미달로 환불액에서 뺀 배송비. 정책이 꺼져 있으면 항상 0이다"),
|
|
1671
1742
|
refundedAt: z.string()
|
|
1672
1743
|
});
|
|
1673
1744
|
/** `PUT /v1/order-items/{orderItemId}/memo` 응답 — 빈 문자열로 저장하면 메모가 지워져 `null`이다 */
|
|
@@ -1692,6 +1763,202 @@ const purchaseDecisionExtendResultSchema = z.object({
|
|
|
1692
1763
|
*/
|
|
1693
1764
|
const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
|
|
1694
1765
|
//#endregion
|
|
1766
|
+
//#region src/schemas/refund-policy.ts
|
|
1767
|
+
/**
|
|
1768
|
+
* 환불 정책 (이슈 #25) — 취소·반품·교환에서 **배송비를 누가 부담하는지**를 정한다.
|
|
1769
|
+
*
|
|
1770
|
+
* 두 층이다. 스토어 설정(`PATCH /v1/store`의 `refundPolicy`)이 기본값이고, 상품별 정책
|
|
1771
|
+
* (`POST/PUT /v1/products`의 `refundPolicy`)이 그 상품에만 덮어쓴다. 상품이 정하지 않은 항목은
|
|
1772
|
+
* 스토어 기본을 따르고, 스토어도 정하지 않았으면 `REFUND_POLICY_DEFAULTS`다.
|
|
1773
|
+
*
|
|
1774
|
+
* 한 주문에 정책이 섞이면 **상품별로 계산한다**. 배송비 묶음도 상품 단위다(주문서·장바구니의
|
|
1775
|
+
* 배송비 합산과 같은 규칙: 같은 상품의 주문상품이 한 묶음이고 묶음마다 배송비가 붙는다).
|
|
1776
|
+
*/
|
|
1777
|
+
const descriptions = {
|
|
1778
|
+
preShipmentDeliveryFeeRefund: "발송 전 취소로 배송 묶음(같은 상품의 주문상품)이 모두 취소되면 그 묶음의 배송비를 환불한다. false면 배송비를 환불하지 않는다. 기본 true",
|
|
1779
|
+
returnDeliveryFee: "단순 변심(구매자 귀책) 반품의 반품 배송비. 원 단위. 환불액에서 뺀다. 기본 0(무료). 상품별 정책에서 이 값을 주면 상품의 `returnFee`도 같은 값으로 맞춘다",
|
|
1780
|
+
exchangeDeliveryFee: "단순 변심(구매자 귀책) 교환의 교환 배송비. 원 단위. 구매자에게 청구할 금액을 안내값으로 싣는다. 기본 0(무료). 상품별 정책에서 이 값을 주면 상품의 `exchangeFee`도 같은 값으로 맞춘다",
|
|
1781
|
+
sellerFaultFreeShipping: "판매자 귀책(셀러 직권취소, 불량·오배송 반품)이면 배송비를 셀러가 부담한다 — 배송비를 환불하고 반품비를 받지 않는다. false면 구매자 귀책과 같은 규칙을 쓴다. 기본 true",
|
|
1782
|
+
conditionalFreeShortfallCharge: "조건부 무료배송(`CONDITIONAL_FREE`) 상품에서 일부 취소로 남은 금액이 무료 기준액에 미달하면 그 배송비를 환불액에서 뺀다. false면 추가 청구 없이 셀러가 부담한다. 기본 false"
|
|
1783
|
+
};
|
|
1784
|
+
/** 유효 환불 정책 — 스토어 기본 + 상품별 덮어쓰기를 합친 결과 */
|
|
1785
|
+
const refundPolicySchema = z.object({
|
|
1786
|
+
preShipmentDeliveryFeeRefund: z.boolean().describe(descriptions.preShipmentDeliveryFeeRefund),
|
|
1787
|
+
returnDeliveryFee: z.number().int().min(0).describe(descriptions.returnDeliveryFee),
|
|
1788
|
+
exchangeDeliveryFee: z.number().int().min(0).describe(descriptions.exchangeDeliveryFee),
|
|
1789
|
+
sellerFaultFreeShipping: z.boolean().describe(descriptions.sellerFaultFreeShipping),
|
|
1790
|
+
conditionalFreeShortfallCharge: z.boolean().describe(descriptions.conditionalFreeShortfallCharge)
|
|
1791
|
+
});
|
|
1792
|
+
/**
|
|
1793
|
+
* 스토어가 정하지 않았을 때 서버가 쓰는 값.
|
|
1794
|
+
*
|
|
1795
|
+
* `as const`가 아니라 `RefundPolicy`로 타입을 좁힌다 — 리터럴 타입이면 `Partial<typeof …>`로 일부만 바꿀 때
|
|
1796
|
+
* `true` 자리에 `false`를 넣을 수 없다.
|
|
1797
|
+
*/
|
|
1798
|
+
const REFUND_POLICY_DEFAULTS = {
|
|
1799
|
+
preShipmentDeliveryFeeRefund: true,
|
|
1800
|
+
returnDeliveryFee: 0,
|
|
1801
|
+
exchangeDeliveryFee: 0,
|
|
1802
|
+
sellerFaultFreeShipping: true,
|
|
1803
|
+
conditionalFreeShortfallCharge: false
|
|
1804
|
+
};
|
|
1805
|
+
/**
|
|
1806
|
+
* 부분 정책 — 스토어 설정 요청과 상품별 덮어쓰기가 쓴다. 준 항목만 바뀐다.
|
|
1807
|
+
*
|
|
1808
|
+
* `strictObject`라 모르는 키는 400이다. 오타(`returnFee` 등)를 조용히 무시하면 셀러는 저장됐다고
|
|
1809
|
+
* 믿는데 정책은 기본값으로 남는다.
|
|
1810
|
+
*/
|
|
1811
|
+
const refundPolicyPatchSchema = z.strictObject({
|
|
1812
|
+
preShipmentDeliveryFeeRefund: z.boolean().optional().describe(descriptions.preShipmentDeliveryFeeRefund),
|
|
1813
|
+
returnDeliveryFee: z.number().int().min(0).optional().describe(descriptions.returnDeliveryFee),
|
|
1814
|
+
exchangeDeliveryFee: z.number().int().min(0).optional().describe(descriptions.exchangeDeliveryFee),
|
|
1815
|
+
sellerFaultFreeShipping: z.boolean().optional().describe(descriptions.sellerFaultFreeShipping),
|
|
1816
|
+
conditionalFreeShortfallCharge: z.boolean().optional().describe(descriptions.conditionalFreeShortfallCharge)
|
|
1817
|
+
});
|
|
1818
|
+
/**
|
|
1819
|
+
* 유효 정책 해석 — 상품별 덮어쓰기 → 스토어 기본 → `REFUND_POLICY_DEFAULTS` 순이다.
|
|
1820
|
+
*
|
|
1821
|
+
* 반품비·교환비는 레거시 필드(`products.returnFee`·`exchangeFee`)와 **연결**한다. 상품이
|
|
1822
|
+
* `refundPolicy.returnDeliveryFee`를 정하면 그 값이 원천이고(서버가 `returnFee`도 같은 값으로 맞춘다),
|
|
1823
|
+
* 정하지 않았으면 레거시 값이 0보다 클 때 그 값을 쓴다. 0이면 스토어 기본을 따른다 —
|
|
1824
|
+
* 정책이 생기기 전에 만든 상품(반품비 0)이 스토어 기본을 물려받게 하는 규칙이다.
|
|
1825
|
+
*/
|
|
1826
|
+
function resolveRefundPolicy(storePolicy, product) {
|
|
1827
|
+
return {
|
|
1828
|
+
...REFUND_POLICY_DEFAULTS,
|
|
1829
|
+
...dropUndefined(storePolicy),
|
|
1830
|
+
...productRefundPolicyOverride(product) ?? {}
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* 상품의 **덮어쓰기 자체** — 레거시 반품비·교환비를 같은 형태로 승격한 값이다. 덮어쓰기가 없으면 null.
|
|
1835
|
+
*
|
|
1836
|
+
* `resolveRefundPolicy`가 합친 결과로는 "상품이 정한 값"과 "스토어 기본을 물려받은 값"을 가를 수 없다.
|
|
1837
|
+
* 관리 API 응답의 `refundPolicy`와 콘솔 폼의 덮어쓰기 스위치가 이 함수를 원천으로 쓴다 — 가르지 못하면
|
|
1838
|
+
* 스토어 기본을 쓰는 상품을 저장할 때 그 순간의 기본값이 상품에 박히거나, 레거시 값이 조용히 사라진다.
|
|
1839
|
+
*/
|
|
1840
|
+
function productRefundPolicyOverride(product) {
|
|
1841
|
+
const override = dropUndefined(product?.refundPolicy);
|
|
1842
|
+
const legacy = {};
|
|
1843
|
+
if (override.returnDeliveryFee === void 0 && (product?.returnFee ?? 0) > 0) legacy.returnDeliveryFee = product?.returnFee ?? 0;
|
|
1844
|
+
if (override.exchangeDeliveryFee === void 0 && (product?.exchangeFee ?? 0) > 0) legacy.exchangeDeliveryFee = product?.exchangeFee ?? 0;
|
|
1845
|
+
const merged = {
|
|
1846
|
+
...legacy,
|
|
1847
|
+
...override
|
|
1848
|
+
};
|
|
1849
|
+
if (product?.refundPolicy == null && Object.keys(merged).length === 0) return null;
|
|
1850
|
+
return merged;
|
|
1851
|
+
}
|
|
1852
|
+
/** `undefined` 값을 빼고 얕게 복사한다 — 전개 순서로 덮어쓸 때 `undefined`가 기본값을 지우지 않게 */
|
|
1853
|
+
function dropUndefined(patch) {
|
|
1854
|
+
if (!patch) return {};
|
|
1855
|
+
return Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== void 0));
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* 수량 단위 부분 취소의 환불 기준액 — **누적 내림의 차분**이다.
|
|
1859
|
+
*
|
|
1860
|
+
* 원 단위 반올림 규칙: `floor(주문상품 금액 × 누적 수량 ÷ 전체 수량)`을 두 번 구해 그 차를 쓴다.
|
|
1861
|
+
* 부분 취소를 몇 번 나눠도 합이 주문상품 금액과 정확히 같아서 1원이 남거나 넘치지 않는다.
|
|
1862
|
+
* 할인은 이미 단가에 들어 있어(주문 단위 쿠폰이 없다) 별도 안분이 없다.
|
|
1863
|
+
*
|
|
1864
|
+
* @param totalPrice 주문상품 금액 (할인 반영 단가 × 전체 수량)
|
|
1865
|
+
* @param quantity 주문상품 전체 수량
|
|
1866
|
+
* @param settledQuantity 이미 취소·반품된 누적 수량
|
|
1867
|
+
* @param cancelQuantity 이번에 취소·반품할 수량
|
|
1868
|
+
*/
|
|
1869
|
+
function proratedRefundBase(totalPrice, quantity, settledQuantity, cancelQuantity) {
|
|
1870
|
+
if (quantity <= 0 || cancelQuantity <= 0) return 0;
|
|
1871
|
+
const before = Math.floor(totalPrice * settledQuantity / quantity);
|
|
1872
|
+
const after = Math.floor(totalPrice * (settledQuantity + cancelQuantity) / quantity);
|
|
1873
|
+
return Math.max(0, after - before);
|
|
1874
|
+
}
|
|
1875
|
+
//#endregion
|
|
1876
|
+
//#region src/schemas/upload.ts
|
|
1877
|
+
/**
|
|
1878
|
+
* 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
|
|
1879
|
+
*
|
|
1880
|
+
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
1881
|
+
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
1882
|
+
*/
|
|
1883
|
+
const uploadPurposeSchema = z.enum(["product-image", "description"]);
|
|
1884
|
+
const MB = 1048576;
|
|
1885
|
+
/**
|
|
1886
|
+
* 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
|
|
1887
|
+
*
|
|
1888
|
+
* 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
|
|
1889
|
+
* 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
|
|
1890
|
+
*/
|
|
1891
|
+
const UPLOAD_PURPOSE_LIMITS = {
|
|
1892
|
+
"product-image": {
|
|
1893
|
+
extensions: [
|
|
1894
|
+
"jpg",
|
|
1895
|
+
"jpeg",
|
|
1896
|
+
"png",
|
|
1897
|
+
"webp",
|
|
1898
|
+
"avif",
|
|
1899
|
+
"gif"
|
|
1900
|
+
],
|
|
1901
|
+
maxBytes: 10 * MB
|
|
1902
|
+
},
|
|
1903
|
+
description: {
|
|
1904
|
+
extensions: [
|
|
1905
|
+
"jpg",
|
|
1906
|
+
"jpeg",
|
|
1907
|
+
"png",
|
|
1908
|
+
"gif",
|
|
1909
|
+
"webp",
|
|
1910
|
+
"avif",
|
|
1911
|
+
"svg",
|
|
1912
|
+
"mp4",
|
|
1913
|
+
"webm",
|
|
1914
|
+
"mov"
|
|
1915
|
+
],
|
|
1916
|
+
maxBytes: 50 * MB
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|
|
1920
|
+
function uploadExtensionOf(filename) {
|
|
1921
|
+
const dot = filename.lastIndexOf(".");
|
|
1922
|
+
if (dot < 0 || dot === filename.length - 1) return "";
|
|
1923
|
+
return filename.slice(dot + 1).toLowerCase();
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* 용도 제한 위반 여부. 통과하면 `null`.
|
|
1927
|
+
* 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
|
|
1928
|
+
*/
|
|
1929
|
+
function uploadRejection(purpose, filename, size) {
|
|
1930
|
+
const limit = UPLOAD_PURPOSE_LIMITS[purpose];
|
|
1931
|
+
if (!limit.extensions.includes(uploadExtensionOf(filename))) return {
|
|
1932
|
+
status: 400,
|
|
1933
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
1934
|
+
message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`
|
|
1935
|
+
};
|
|
1936
|
+
if (size !== void 0 && size > limit.maxBytes) return {
|
|
1937
|
+
status: 413,
|
|
1938
|
+
code: "FILE_TOO_LARGE",
|
|
1939
|
+
message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`
|
|
1940
|
+
};
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* 미디어 업로드 자리 발급 — `POST /v1/uploads`.
|
|
1945
|
+
*
|
|
1946
|
+
* 파일 바이트는 api를 거치지 않는다. 응답의 `uploadUrl`로 클라이언트가 직접 PUT하고,
|
|
1947
|
+
* 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
|
|
1948
|
+
* (확장자만 쓰이고 이름 자체는 저장되지 않는다).
|
|
1949
|
+
*/
|
|
1950
|
+
const uploadRequestSchema = z.object({
|
|
1951
|
+
filename: z.string().min(1).max(255),
|
|
1952
|
+
purpose: uploadPurposeSchema.default("description").describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
|
|
1953
|
+
size: z.number().int().positive().optional().describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다")
|
|
1954
|
+
});
|
|
1955
|
+
const uploadTicketSchema = z.object({
|
|
1956
|
+
/** 업로드 완료 후 공개될 주소 */
|
|
1957
|
+
url: z.url(),
|
|
1958
|
+
/** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
|
|
1959
|
+
uploadUrl: z.url()
|
|
1960
|
+
});
|
|
1961
|
+
//#endregion
|
|
1695
1962
|
//#region src/schemas/product.ts
|
|
1696
1963
|
const productStatusSchema = z.enum([
|
|
1697
1964
|
"SALE",
|
|
@@ -1771,10 +2038,59 @@ const productSchema = productSummarySchema.extend({
|
|
|
1771
2038
|
conditionalFreeAmount: z.number().int().nullable().optional(),
|
|
1772
2039
|
returnFee: z.number().int().optional(),
|
|
1773
2040
|
exchangeFee: z.number().int().optional(),
|
|
2041
|
+
refundPolicy: refundPolicyPatchSchema.nullable().optional().describe("상품별 환불 정책 덮어쓰기. null이면 스토어 기본 환불 정책을 쓴다"),
|
|
1774
2042
|
shippingAddressId: z.string().nullable().optional(),
|
|
1775
2043
|
returnAddressId: z.string().nullable().optional(),
|
|
1776
2044
|
createdAt: z.string().optional()
|
|
1777
2045
|
});
|
|
2046
|
+
/** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
|
|
2047
|
+
const MAX_PRODUCT_IMAGES = 10;
|
|
2048
|
+
/**
|
|
2049
|
+
* 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
|
|
2050
|
+
*
|
|
2051
|
+
* https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
|
|
2052
|
+
* **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
|
|
2053
|
+
* 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
|
|
2054
|
+
*
|
|
2055
|
+
* 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
|
|
2056
|
+
* 만들면 상품 조회 자체가 막힌다.
|
|
2057
|
+
*/
|
|
2058
|
+
const productImageUrlSchema = z.url().max(2048).describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
|
|
2059
|
+
/**
|
|
2060
|
+
* 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
|
|
2061
|
+
*
|
|
2062
|
+
* - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
|
|
2063
|
+
* 상세 화면에 박히는 주입 경로가 된다.
|
|
2064
|
+
* - 호스트는 제한하지 않는다 — 셀러가 이미 쓰는 이미지 호스트를 그대로 붙일 수 있어야 한다.
|
|
2065
|
+
* - 경로에 확장자가 있으면 상품 이미지 형식이어야 한다(svg·동영상 거부). **확장자가 없으면 통과시킨다**
|
|
2066
|
+
* — 외부 이미지 CDN은 확장자 없는 키를 쓰는 경우가 흔하고, 주소만으로는 실제 형식을 알 수 없다.
|
|
2067
|
+
*/
|
|
2068
|
+
function productImageRejection(url) {
|
|
2069
|
+
if (url.length > 2048) return {
|
|
2070
|
+
code: "INVALID_IMAGE_URL",
|
|
2071
|
+
message: "이미지 주소가 너무 깁니다"
|
|
2072
|
+
};
|
|
2073
|
+
let parsed;
|
|
2074
|
+
try {
|
|
2075
|
+
parsed = new URL(url);
|
|
2076
|
+
} catch {
|
|
2077
|
+
return {
|
|
2078
|
+
code: "INVALID_IMAGE_URL",
|
|
2079
|
+
message: "이미지 주소 형식이 아닙니다"
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
if (parsed.protocol !== "https:") return {
|
|
2083
|
+
code: "INVALID_IMAGE_URL",
|
|
2084
|
+
message: "이미지 주소는 https로 시작해야 합니다"
|
|
2085
|
+
};
|
|
2086
|
+
const extension = uploadExtensionOf(parsed.pathname);
|
|
2087
|
+
const allowed = UPLOAD_PURPOSE_LIMITS["product-image"].extensions;
|
|
2088
|
+
if (extension && !allowed.includes(extension)) return {
|
|
2089
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
2090
|
+
message: `상품 이미지는 ${allowed.join(", ")}만 쓸 수 있습니다`
|
|
2091
|
+
};
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
1778
2094
|
/** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
|
|
1779
2095
|
const MAX_OPTION_GROUPS = 3;
|
|
1780
2096
|
/** 축 하나가 가질 수 있는 값의 최대 개수 */
|
|
@@ -1856,7 +2172,7 @@ const productCreateRequestSchema = z.object({
|
|
|
1856
2172
|
* 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
|
|
1857
2173
|
*/
|
|
1858
2174
|
descriptionJson: z.string().nullable().optional(),
|
|
1859
|
-
images: z.array(
|
|
2175
|
+
images: z.array(productImageUrlSchema).min(1).max(10).optional().describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
|
|
1860
2176
|
attributes: z.array(z.object({
|
|
1861
2177
|
attributeId: z.string(),
|
|
1862
2178
|
value: z.string()
|
|
@@ -1864,8 +2180,9 @@ const productCreateRequestSchema = z.object({
|
|
|
1864
2180
|
deliveryType: deliveryTypeSchema.default("PAID").describe("배송비 유형. FREE 무료, PAID 유료, CONDITIONAL_FREE 조건부 무료"),
|
|
1865
2181
|
deliveryFee: z.number().int().default(0).describe("배송비. 원 단위"),
|
|
1866
2182
|
conditionalFreeAmount: z.number().int().optional().describe("CONDITIONAL_FREE일 때 무료 배송이 되는 주문 금액. 원 단위"),
|
|
1867
|
-
returnFee: z.number().int().default(0).describe("반품 배송비. 원
|
|
1868
|
-
exchangeFee: z.number().int().default(0).describe("교환 배송비. 원
|
|
2183
|
+
returnFee: z.number().int().default(0).describe("반품 배송비. 원 단위. `refundPolicy.returnDeliveryFee`와 같은 값이다"),
|
|
2184
|
+
exchangeFee: z.number().int().default(0).describe("교환 배송비. 원 단위. `refundPolicy.exchangeDeliveryFee`와 같은 값이다"),
|
|
2185
|
+
refundPolicy: refundPolicyPatchSchema.nullish().describe("상품별 환불 정책 — 준 항목만 스토어 기본을 덮어쓴다. `null`이나 생략은 스토어 기본을 그대로 쓴다는 뜻이다. `returnDeliveryFee`·`exchangeDeliveryFee`를 주면 `returnFee`·`exchangeFee`도 같은 값으로 맞춘다"),
|
|
1869
2186
|
shippingAddressId: z.string().optional(),
|
|
1870
2187
|
returnAddressId: z.string().optional()
|
|
1871
2188
|
}).extend(optionModelRequestSchema.shape);
|
|
@@ -1891,12 +2208,6 @@ const productListQuerySchema = z.object({
|
|
|
1891
2208
|
sort: productSortSchema.optional().describe("정렬 (기본 createdAt.desc)"),
|
|
1892
2209
|
...pageQueryShape()
|
|
1893
2210
|
});
|
|
1894
|
-
/** `POST /v1/product-images` 응답 — 업로드된 이미지의 CDN 주소 */
|
|
1895
|
-
const productImageUploadResultSchema = z.object({ images: z.array(z.object({
|
|
1896
|
-
url: z.url(),
|
|
1897
|
-
width: z.number().int(),
|
|
1898
|
-
height: z.number().int()
|
|
1899
|
-
})) });
|
|
1900
2211
|
const discountSchema = z.object({
|
|
1901
2212
|
productId: z.string(),
|
|
1902
2213
|
discountType: z.enum(["RATE", "AMOUNT"]),
|
|
@@ -1985,6 +2296,20 @@ function isWebOrigin(value) {
|
|
|
1985
2296
|
const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요").describe("스토어프론트 오리진. 예: `https://shop.example.com`");
|
|
1986
2297
|
/** 허용 오리진 상한 */
|
|
1987
2298
|
const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
2299
|
+
/** 자동 배송완료·자동 구매확정 대기일 상한 */
|
|
2300
|
+
const MAX_AUTO_TRANSITION_DAYS = 30;
|
|
2301
|
+
/**
|
|
2302
|
+
* 약관 버전 — 스토어가 정하는 문자열이다(`1`·`2`·`2026-09-26`·`v1.2` 등). 형식은 강제하지 않고 값이 다르면 다른 버전이다.
|
|
2303
|
+
* 올리면 그 약관에 동의한 구매자의 `reconsentRequired`가 켜지고, 재동의가 이력에 남는다. 되돌리면 재동의 요구도 풀린다.
|
|
2304
|
+
*/
|
|
2305
|
+
const policyVersionSchema = z.string().trim().min(1).max(20).describe("약관 버전 문자열(1~20자). 예: `1`, `2026-09-26`");
|
|
2306
|
+
/**
|
|
2307
|
+
* 약관 문서 주소 — 구매자에게 보여 주는 약관 본문의 주소다. 동의 시점 값이 이력 행에 복사되므로
|
|
2308
|
+
* 나중에 주소를 바꿔도 "무엇에 동의했는지"가 남는다. `null`로 지울 수 있다.
|
|
2309
|
+
*/
|
|
2310
|
+
const policyDocumentUrlSchema = z.url().max(500).refine((value) => value.startsWith("https://"), "https 주소만 쓸 수 있어요").nullable();
|
|
2311
|
+
const autoDeliverDaysDescription = "발송 후 자동 배송완료까지의 일수(0~30, 기본 7). 발송(`DISPATCHED`)·배송중(`DELIVERING`) 주문상품을 이 기간이 지나면 배송완료(`DELIVERED`)로 바꾼다. `0`이면 자동 배송완료를 하지 않는다";
|
|
2312
|
+
const autoConfirmDaysDescription = "배송완료 후 자동 구매확정까지의 일수(0~30, 기본 7). 배송완료 시점에 구매확정 예정일(`autoDecisionDate`)을 이 값으로 잡고, 그 날짜가 지나면 구매확정(`PURCHASE_DECIDED`)으로 바꾼다. 진행 중인 클레임이 있으면 건너뛴다. `0`이면 자동 구매확정을 하지 않는다";
|
|
1988
2313
|
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
1989
2314
|
const updateStoreRequestSchema = z.object({
|
|
1990
2315
|
name: z.string().min(2).max(50).optional(),
|
|
@@ -1993,7 +2318,14 @@ const updateStoreRequestSchema = z.object({
|
|
|
1993
2318
|
businessHours: z.string().max(100).optional(),
|
|
1994
2319
|
analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프론트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다"),
|
|
1995
2320
|
storefrontOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("스토어프론트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. `analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)"),
|
|
1996
|
-
guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다")
|
|
2321
|
+
guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다"),
|
|
2322
|
+
autoDeliverDays: z.number().int().min(0).max(30).optional().describe(autoDeliverDaysDescription),
|
|
2323
|
+
autoConfirmDays: z.number().int().min(0).max(30).optional().describe(autoConfirmDaysDescription),
|
|
2324
|
+
refundPolicy: refundPolicyPatchSchema.optional().describe("스토어 기본 환불 정책 — 준 항목만 바꾼다. 상품별 정책(`products.refundPolicy`)이 없는 상품에 적용된다"),
|
|
2325
|
+
termsVersion: policyVersionSchema.optional().describe("이용약관 버전. 올리면 이 약관에 동의한 구매자에게 재동의를 요구한다(`GET /storefront/v1/me`의 `reconsentRequired`)"),
|
|
2326
|
+
privacyVersion: policyVersionSchema.optional().describe("개인정보 수집이용 동의 버전. 올리면 구매자에게 재동의를 요구한다"),
|
|
2327
|
+
termsUrl: policyDocumentUrlSchema.optional().describe("이용약관 문서 주소(https). 동의 시점의 값이 동의 이력에 복사된다. `null`을 주면 지운다"),
|
|
2328
|
+
privacyUrl: policyDocumentUrlSchema.optional().describe("개인정보 수집이용 동의 문서 주소(https). `null`을 주면 지운다")
|
|
1997
2329
|
}).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
|
|
1998
2330
|
const storeSchema = z.object({
|
|
1999
2331
|
storeId: z.string(),
|
|
@@ -2011,6 +2343,15 @@ const storeSchema = z.object({
|
|
|
2011
2343
|
analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프론트 오리진. 비어 있으면 모든 오리진에서 받는다"),
|
|
2012
2344
|
storefrontOrigins: z.array(z.string()).describe("스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)"),
|
|
2013
2345
|
guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
|
|
2346
|
+
autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
|
|
2347
|
+
autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
|
|
2348
|
+
refundPolicy: refundPolicySchema.describe("스토어 기본 환불 정책. 스토어가 정하지 않은 항목은 플랫폼 기본값으로 채워 항상 전부 싣는다"),
|
|
2349
|
+
termsVersion: z.string().describe("현재 이용약관 버전. 가입·재동의가 이 값으로 이력에 남는다"),
|
|
2350
|
+
privacyVersion: z.string().describe("현재 개인정보 수집이용 동의 버전"),
|
|
2351
|
+
termsVersionUpdatedAt: z.string().nullable().describe("이용약관 버전을 마지막으로 바꾼 시각. 바꾼 적이 없으면 null"),
|
|
2352
|
+
privacyVersionUpdatedAt: z.string().nullable(),
|
|
2353
|
+
termsUrl: z.string().nullable().describe("이용약관 문서 주소. 정하지 않았으면 null이고 동의 이력에도 주소가 남지 않는다"),
|
|
2354
|
+
privacyUrl: z.string().nullable().describe("개인정보 수집이용 동의 문서 주소"),
|
|
2014
2355
|
memberCount: z.number().int(),
|
|
2015
2356
|
createdAt: z.string()
|
|
2016
2357
|
});
|
|
@@ -2068,22 +2409,6 @@ const sellerAddressRequestSchema = z.object({
|
|
|
2068
2409
|
address2: z.string().optional()
|
|
2069
2410
|
});
|
|
2070
2411
|
//#endregion
|
|
2071
|
-
//#region src/schemas/upload.ts
|
|
2072
|
-
/**
|
|
2073
|
-
* 미디어 업로드 자리 발급 — `POST /v1/uploads`.
|
|
2074
|
-
*
|
|
2075
|
-
* 파일 바이트는 api를 거치지 않는다. 응답의 `uploadUrl`로 클라이언트가 직접 PUT하고,
|
|
2076
|
-
* 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
|
|
2077
|
-
* (확장자만 쓰이고 이름 자체는 저장되지 않는다).
|
|
2078
|
-
*/
|
|
2079
|
-
const uploadRequestSchema = z.object({ filename: z.string().min(1).max(255) });
|
|
2080
|
-
const uploadTicketSchema = z.object({
|
|
2081
|
-
/** 업로드 완료 후 공개될 주소 */
|
|
2082
|
-
url: z.url(),
|
|
2083
|
-
/** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
|
|
2084
|
-
uploadUrl: z.url()
|
|
2085
|
-
});
|
|
2086
|
-
//#endregion
|
|
2087
2412
|
//#region src/client.ts
|
|
2088
2413
|
function formBody(fields) {
|
|
2089
2414
|
return new URLSearchParams(fields).toString();
|
|
@@ -2211,7 +2536,35 @@ function createStoreClient(options) {
|
|
|
2211
2536
|
* 미디어 업로드 — 자리를 받아(`POST /uploads`) 파일 바이트는 호출자가
|
|
2212
2537
|
* `uploadUrl`로 직접 PUT한다. 업로드가 끝나면 `url`이 공개 주소가 된다.
|
|
2213
2538
|
*/
|
|
2214
|
-
uploads: {
|
|
2539
|
+
uploads: {
|
|
2540
|
+
create: (body) => storeHttp.request("POST", "/uploads", uploadTicketSchema, { body: uploadRequestSchema.parse(body) }),
|
|
2541
|
+
/**
|
|
2542
|
+
* 자리 발급 + 바이트 PUT을 한 번에 — 끝나면 `url`이 공개 주소다.
|
|
2543
|
+
*
|
|
2544
|
+
* 용도 제한(확장자·크기)은 서버가 판정하지만, 같은 규칙으로 여기서 먼저 걸러
|
|
2545
|
+
* 큰 파일을 CDN까지 보내고 나서 거절되는 왕복을 없앤다.
|
|
2546
|
+
*/
|
|
2547
|
+
upload: async (file, options) => {
|
|
2548
|
+
const filename = options?.filename ?? (typeof File !== "undefined" && file instanceof File ? file.name : "");
|
|
2549
|
+
if (!filename) throw new ApiError(400, "UPLOAD_FILENAME_REQUIRED", "파일명이 필요합니다");
|
|
2550
|
+
if (file.size === 0) throw new ApiError(400, "EMPTY_FILE", `파일이 비어 있습니다: ${filename}`);
|
|
2551
|
+
const purpose = options?.purpose ?? "description";
|
|
2552
|
+
const rejection = uploadRejection(purpose, filename, file.size);
|
|
2553
|
+
if (rejection) throw new ApiError(rejection.status, rejection.code, rejection.message);
|
|
2554
|
+
const ticket = await storeHttp.request("POST", "/uploads", uploadTicketSchema, { body: uploadRequestSchema.parse({
|
|
2555
|
+
filename,
|
|
2556
|
+
purpose,
|
|
2557
|
+
size: file.size
|
|
2558
|
+
}) });
|
|
2559
|
+
const put = await fetchFn(ticket.uploadUrl, {
|
|
2560
|
+
method: "PUT",
|
|
2561
|
+
headers: { "content-type": file.type || "application/octet-stream" },
|
|
2562
|
+
body: file
|
|
2563
|
+
});
|
|
2564
|
+
if (!put.ok) throw new ApiError(put.status, "UPLOAD_PUT_FAILED", `업로드에 실패했습니다: ${filename}`);
|
|
2565
|
+
return ticket;
|
|
2566
|
+
}
|
|
2567
|
+
},
|
|
2215
2568
|
products: {
|
|
2216
2569
|
list: (params) => storeHttp.request("GET", "/products", pageSchema(productSummarySchema), { query: params }),
|
|
2217
2570
|
get: (productId) => storeHttp.request("GET", path`/products/${productId}`, productSchema),
|
|
@@ -2292,6 +2645,16 @@ function createStoreClient(options) {
|
|
|
2292
2645
|
carrierCode,
|
|
2293
2646
|
trackingNumber
|
|
2294
2647
|
} }),
|
|
2648
|
+
/**
|
|
2649
|
+
* 배송완료 처리 — `DISPATCHED`·`DELIVERING`만 넘어간다. 응답의 `autoDecisionDate`가 자동 구매확정 예정일이다
|
|
2650
|
+
* (스토어 설정 `autoConfirmDays`가 0이면 null).
|
|
2651
|
+
*/
|
|
2652
|
+
markDelivered: (orderItemId, idempotencyKey) => storeHttp.request("POST", path`/order-items/${orderItemId}/delivered`, orderItemSchema, { headers: idempotency(idempotencyKey) }),
|
|
2653
|
+
/** 배송완료 일괄 처리 — 항목별 부분 성공이다 */
|
|
2654
|
+
markDeliveredBulk: (orderItemIds, idempotencyKey) => storeHttp.request("POST", "/order-items/delivered", bulkResultSchema, {
|
|
2655
|
+
body: { orderItemIds },
|
|
2656
|
+
headers: idempotency(idempotencyKey)
|
|
2657
|
+
}),
|
|
2295
2658
|
getDelivery: (orderItemId) => storeHttp.request("GET", path`/order-items/${orderItemId}/delivery`, deliveryStatusSchema)
|
|
2296
2659
|
},
|
|
2297
2660
|
carriers: { list: () => storeHttp.request("GET", "/carriers", z.array(carrierSchema)) },
|
|
@@ -2341,6 +2704,8 @@ function createStoreClient(options) {
|
|
|
2341
2704
|
list: (params) => storeHttp.request("GET", "/customers", pageSchema(customerSummarySchema), { query: params }),
|
|
2342
2705
|
get: (customerId) => storeHttp.request("GET", path`/customers/${customerId}`, customerDetailSchema),
|
|
2343
2706
|
orders: (customerId, params) => storeHttp.request("GET", path`/customers/${customerId}/orders`, pageSchema(customerOrderSchema), { query: params }),
|
|
2707
|
+
/** 동의 이력 — 최근 순. 가입·재동의·마케팅 변경이 한 줄씩 남는다 */
|
|
2708
|
+
consents: (customerId, params) => storeHttp.request("GET", path`/customers/${customerId}/consents`, pageSchema(customerConsentSchema), { query: params }),
|
|
2344
2709
|
withdraw: (customerId, body) => storeHttp.requestVoid("POST", path`/customers/${customerId}/withdraw`, { body: withdrawCustomerRequestSchema.parse(body) })
|
|
2345
2710
|
},
|
|
2346
2711
|
customerInquiries: {
|
|
@@ -2716,6 +3081,11 @@ const claimWebhookEventDataSchema = z.object({
|
|
|
2716
3081
|
orderItemId: z.string().describe("클레임 대상 주문 상품 ID"),
|
|
2717
3082
|
claimType: claimTypeSchema.describe("클레임 유형"),
|
|
2718
3083
|
status: claimStatusSchema.describe("이벤트 발생 시점의 클레임 상태"),
|
|
3084
|
+
quantity: z.number().int().optional().default(0).describe("클레임 대상 수량. 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
|
|
3085
|
+
refundAmount: z.number().int().nullable().optional().default(null).describe("환불액 (원). 배송비 환불을 포함하고 배송비 차감·추가 차감을 뺀 실제 금액이다. 환불이 확정되지 않은 접수 이벤트는 예상 환불액이고, 교환처럼 환불이 없으면 null이다"),
|
|
3086
|
+
deliveryFeeRefundAmount: z.number().int().optional().default(0).describe("환불액에 포함된 배송비 (원). 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
|
|
3087
|
+
deliveryFeeChargeAmount: z.number().int().optional().default(0).describe("환불액에서 뺀 배송비 (원) — 반품·교환 배송비와 조건부 무료배송 미달 차감의 합. 셀러가 승인에 넣은 추가 차감은 `deductAmount`로 따로 온다. 이 필드가 없는 이전 이벤트는 0으로 읽는다"),
|
|
3088
|
+
deductAmount: z.number().int().optional().default(0).describe("환불액에서 뺀 추가 차감 (원) — 셀러가 클레임 승인에 넣은 금액이다(상품 훼손 등). 배송비 차감과 별개이므로 `deliveryFeeChargeAmount`와 더해서 뺀다. 접수 이벤트와 이 필드가 없는 이전 이벤트는 0이다"),
|
|
2719
3089
|
testPayment: z.boolean().optional().default(false).describe("테스트 결제(샌드박스 결제) 주문의 클레임인가. 이 필드가 없는 이전 이벤트는 false로 읽는다")
|
|
2720
3090
|
});
|
|
2721
3091
|
const inquiryWebhookEventDataSchema = z.object({
|
|
@@ -2926,4 +3296,4 @@ function createStoreSession(options) {
|
|
|
2926
3296
|
};
|
|
2927
3297
|
}
|
|
2928
3298
|
//#endregion
|
|
2929
|
-
export { ApiError, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_OPTION_GROUPS, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema,
|
|
3299
|
+
export { ApiError, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, REFUND_POLICY_DEFAULTS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, consentActionSchema, consentKindSchema, consentSourceSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, policyDocumentUrlSchema, policyVersionSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productRefundPolicyOverride, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, proratedRefundBase, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, refundPolicyPatchSchema, refundPolicySchema, rejectClaimRequestSchema, resolveRefundPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };
|