@sayren/store-sdk 0.8.0 → 0.9.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 +135 -11
- package/dist/index.mjs +185 -26
- package/package.json +1 -1
- package/src/client.ts +69 -2
- package/src/schemas/common.ts +3 -0
- package/src/schemas/product.ts +63 -8
- package/src/schemas/store.ts +27 -0
- package/src/schemas/upload.ts +86 -0
package/dist/index.d.mts
CHANGED
|
@@ -1050,6 +1050,7 @@ export declare const changeTypeSchema: z.ZodEnum<{
|
|
|
1050
1050
|
"CLAIM.CANCEL_REQUESTED": "CLAIM.CANCEL_REQUESTED";
|
|
1051
1051
|
"CLAIM.RETURN_REQUESTED": "CLAIM.RETURN_REQUESTED";
|
|
1052
1052
|
"CLAIM.EXCHANGE_REQUESTED": "CLAIM.EXCHANGE_REQUESTED";
|
|
1053
|
+
"CLAIM.COMPLETED": "CLAIM.COMPLETED";
|
|
1053
1054
|
}>;
|
|
1054
1055
|
export type ChangeType = z.infer<typeof changeTypeSchema>;
|
|
1055
1056
|
//#endregion
|
|
@@ -2011,6 +2012,7 @@ export declare const orderItemSchema: z.ZodObject<{
|
|
|
2011
2012
|
"CLAIM.CANCEL_REQUESTED": "CLAIM.CANCEL_REQUESTED";
|
|
2012
2013
|
"CLAIM.RETURN_REQUESTED": "CLAIM.RETURN_REQUESTED";
|
|
2013
2014
|
"CLAIM.EXCHANGE_REQUESTED": "CLAIM.EXCHANGE_REQUESTED";
|
|
2015
|
+
"CLAIM.COMPLETED": "CLAIM.COMPLETED";
|
|
2014
2016
|
}>;
|
|
2015
2017
|
testPayment: z.ZodBoolean;
|
|
2016
2018
|
}, z.core.$strip>;
|
|
@@ -2095,6 +2097,7 @@ export declare const orderSchema: z.ZodObject<{
|
|
|
2095
2097
|
"CLAIM.CANCEL_REQUESTED": "CLAIM.CANCEL_REQUESTED";
|
|
2096
2098
|
"CLAIM.RETURN_REQUESTED": "CLAIM.RETURN_REQUESTED";
|
|
2097
2099
|
"CLAIM.EXCHANGE_REQUESTED": "CLAIM.EXCHANGE_REQUESTED";
|
|
2100
|
+
"CLAIM.COMPLETED": "CLAIM.COMPLETED";
|
|
2098
2101
|
}>;
|
|
2099
2102
|
testPayment: z.ZodBoolean;
|
|
2100
2103
|
}, z.core.$strip>>;
|
|
@@ -2177,6 +2180,7 @@ export declare const orderItemPageSchema: z.ZodObject<{
|
|
|
2177
2180
|
"CLAIM.CANCEL_REQUESTED": "CLAIM.CANCEL_REQUESTED";
|
|
2178
2181
|
"CLAIM.RETURN_REQUESTED": "CLAIM.RETURN_REQUESTED";
|
|
2179
2182
|
"CLAIM.EXCHANGE_REQUESTED": "CLAIM.EXCHANGE_REQUESTED";
|
|
2183
|
+
"CLAIM.COMPLETED": "CLAIM.COMPLETED";
|
|
2180
2184
|
}>;
|
|
2181
2185
|
testPayment: z.ZodBoolean;
|
|
2182
2186
|
}, z.core.$strip>>;
|
|
@@ -2227,6 +2231,7 @@ export declare const orderItemListQuerySchema: z.ZodObject<{
|
|
|
2227
2231
|
"CLAIM.CANCEL_REQUESTED": "CLAIM.CANCEL_REQUESTED";
|
|
2228
2232
|
"CLAIM.RETURN_REQUESTED": "CLAIM.RETURN_REQUESTED";
|
|
2229
2233
|
"CLAIM.EXCHANGE_REQUESTED": "CLAIM.EXCHANGE_REQUESTED";
|
|
2234
|
+
"CLAIM.COMPLETED": "CLAIM.COMPLETED";
|
|
2230
2235
|
}>>;
|
|
2231
2236
|
testPayment: z.ZodOptional<z.ZodEnum<{
|
|
2232
2237
|
true: "true";
|
|
@@ -3458,6 +3463,33 @@ export declare const productSchema: z.ZodObject<{
|
|
|
3458
3463
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
3459
3464
|
}, z.core.$strip>;
|
|
3460
3465
|
export type Product = z.infer<typeof productSchema>;
|
|
3466
|
+
/** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
|
|
3467
|
+
export declare const MAX_PRODUCT_IMAGES = 10;
|
|
3468
|
+
/**
|
|
3469
|
+
* 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
|
|
3470
|
+
*
|
|
3471
|
+
* https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
|
|
3472
|
+
* **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
|
|
3473
|
+
* 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
|
|
3474
|
+
*
|
|
3475
|
+
* 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
|
|
3476
|
+
* 만들면 상품 조회 자체가 막힌다.
|
|
3477
|
+
*/
|
|
3478
|
+
export declare const productImageUrlSchema: z.ZodURL;
|
|
3479
|
+
export interface ProductImageRejection {
|
|
3480
|
+
code: "INVALID_IMAGE_URL" | "UNSUPPORTED_FILE_TYPE";
|
|
3481
|
+
message: string;
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
|
|
3485
|
+
*
|
|
3486
|
+
* - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
|
|
3487
|
+
* 상세 화면에 박히는 주입 경로가 된다.
|
|
3488
|
+
* - 호스트는 제한하지 않는다 — 셀러가 이미 쓰는 이미지 호스트를 그대로 붙일 수 있어야 한다.
|
|
3489
|
+
* - 경로에 확장자가 있으면 상품 이미지 형식이어야 한다(svg·동영상 거부). **확장자가 없으면 통과시킨다**
|
|
3490
|
+
* — 외부 이미지 CDN은 확장자 없는 키를 쓰는 경우가 흔하고, 주소만으로는 실제 형식을 알 수 없다.
|
|
3491
|
+
*/
|
|
3492
|
+
export declare function productImageRejection(url: string): ProductImageRejection | null;
|
|
3461
3493
|
/** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
|
|
3462
3494
|
export declare const MAX_OPTION_GROUPS = 3;
|
|
3463
3495
|
/** 축 하나가 가질 수 있는 값의 최대 개수 */
|
|
@@ -3655,15 +3687,6 @@ export declare const productListQuerySchema: z.ZodObject<{
|
|
|
3655
3687
|
"updatedAt.desc": "updatedAt.desc";
|
|
3656
3688
|
}>>;
|
|
3657
3689
|
}, z.core.$strip>;
|
|
3658
|
-
/** `POST /v1/product-images` 응답 — 업로드된 이미지의 CDN 주소 */
|
|
3659
|
-
export declare const productImageUploadResultSchema: z.ZodObject<{
|
|
3660
|
-
images: z.ZodArray<z.ZodObject<{
|
|
3661
|
-
url: z.ZodURL;
|
|
3662
|
-
width: z.ZodNumber;
|
|
3663
|
-
height: z.ZodNumber;
|
|
3664
|
-
}, z.core.$strip>>;
|
|
3665
|
-
}, z.core.$strip>;
|
|
3666
|
-
export type ProductImageUploadResult = z.infer<typeof productImageUploadResultSchema>;
|
|
3667
3690
|
export declare const discountSchema: z.ZodObject<{
|
|
3668
3691
|
productId: z.ZodString;
|
|
3669
3692
|
discountType: z.ZodEnum<{
|
|
@@ -3706,6 +3729,8 @@ export declare function isWebOrigin(value: string): boolean;
|
|
|
3706
3729
|
export declare const analyticsOriginSchema: z.ZodString;
|
|
3707
3730
|
/** 허용 오리진 상한 */
|
|
3708
3731
|
export declare const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
3732
|
+
/** 자동 배송완료·자동 구매확정 대기일 상한 */
|
|
3733
|
+
export declare const MAX_AUTO_TRANSITION_DAYS = 30;
|
|
3709
3734
|
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
3710
3735
|
export declare const updateStoreRequestSchema: z.ZodObject<{
|
|
3711
3736
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -3715,6 +3740,8 @@ export declare const updateStoreRequestSchema: z.ZodObject<{
|
|
|
3715
3740
|
analyticsAllowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3716
3741
|
storefrontOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3717
3742
|
guestCheckoutEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
3743
|
+
autoDeliverDays: z.ZodOptional<z.ZodNumber>;
|
|
3744
|
+
autoConfirmDays: z.ZodOptional<z.ZodNumber>;
|
|
3718
3745
|
}, z.core.$strip>;
|
|
3719
3746
|
export type UpdateStoreRequest = z.infer<typeof updateStoreRequestSchema>;
|
|
3720
3747
|
export declare const storeSchema: z.ZodObject<{
|
|
@@ -3733,6 +3760,8 @@ export declare const storeSchema: z.ZodObject<{
|
|
|
3733
3760
|
analyticsAllowedOrigins: z.ZodArray<z.ZodString>;
|
|
3734
3761
|
storefrontOrigins: z.ZodArray<z.ZodString>;
|
|
3735
3762
|
guestCheckoutEnabled: z.ZodBoolean;
|
|
3763
|
+
autoDeliverDays: z.ZodNumber;
|
|
3764
|
+
autoConfirmDays: z.ZodNumber;
|
|
3736
3765
|
memberCount: z.ZodNumber;
|
|
3737
3766
|
createdAt: z.ZodString;
|
|
3738
3767
|
}, z.core.$strip>;
|
|
@@ -3829,6 +3858,42 @@ export declare const sellerAddressRequestSchema: z.ZodObject<{
|
|
|
3829
3858
|
export type SellerAddressRequest = z.infer<typeof sellerAddressRequestSchema>;
|
|
3830
3859
|
//#endregion
|
|
3831
3860
|
//#region src/schemas/upload.d.ts
|
|
3861
|
+
/**
|
|
3862
|
+
* 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
|
|
3863
|
+
*
|
|
3864
|
+
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
3865
|
+
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
3866
|
+
*/
|
|
3867
|
+
export declare const uploadPurposeSchema: z.ZodEnum<{
|
|
3868
|
+
"product-image": "product-image";
|
|
3869
|
+
description: "description";
|
|
3870
|
+
}>;
|
|
3871
|
+
export type UploadPurpose = z.infer<typeof uploadPurposeSchema>;
|
|
3872
|
+
export interface UploadPurposeLimit {
|
|
3873
|
+
/** 소문자 확장자 목록 */
|
|
3874
|
+
extensions: readonly string[];
|
|
3875
|
+
maxBytes: number;
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
|
|
3879
|
+
*
|
|
3880
|
+
* 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
|
|
3881
|
+
* 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
|
|
3882
|
+
*/
|
|
3883
|
+
export declare const UPLOAD_PURPOSE_LIMITS: Record<UploadPurpose, UploadPurposeLimit>;
|
|
3884
|
+
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|
|
3885
|
+
export declare function uploadExtensionOf(filename: string): string;
|
|
3886
|
+
export interface UploadRejection {
|
|
3887
|
+
/** 400 형식 불일치 / 413 크기 초과 */
|
|
3888
|
+
status: 400 | 413;
|
|
3889
|
+
code: "UNSUPPORTED_FILE_TYPE" | "FILE_TOO_LARGE";
|
|
3890
|
+
message: string;
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* 용도 제한 위반 여부. 통과하면 `null`.
|
|
3894
|
+
* 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
|
|
3895
|
+
*/
|
|
3896
|
+
export declare function uploadRejection(purpose: UploadPurpose, filename: string, size?: number): UploadRejection | null;
|
|
3832
3897
|
/**
|
|
3833
3898
|
* 미디어 업로드 자리 발급 — `POST /v1/uploads`.
|
|
3834
3899
|
*
|
|
@@ -3838,6 +3903,11 @@ export type SellerAddressRequest = z.infer<typeof sellerAddressRequestSchema>;
|
|
|
3838
3903
|
*/
|
|
3839
3904
|
export declare const uploadRequestSchema: z.ZodObject<{
|
|
3840
3905
|
filename: z.ZodString;
|
|
3906
|
+
purpose: z.ZodDefault<z.ZodEnum<{
|
|
3907
|
+
"product-image": "product-image";
|
|
3908
|
+
description: "description";
|
|
3909
|
+
}>>;
|
|
3910
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
3841
3911
|
}, z.core.$strip>;
|
|
3842
3912
|
export type UploadRequest = z.infer<typeof uploadRequestSchema>;
|
|
3843
3913
|
export declare const uploadTicketSchema: z.ZodObject<{
|
|
@@ -4123,6 +4193,16 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
4123
4193
|
url: string;
|
|
4124
4194
|
uploadUrl: string;
|
|
4125
4195
|
}>;
|
|
4196
|
+
/**
|
|
4197
|
+
* 자리 발급 + 바이트 PUT을 한 번에 — 끝나면 `url`이 공개 주소다.
|
|
4198
|
+
*
|
|
4199
|
+
* 용도 제한(확장자·크기)은 서버가 판정하지만, 같은 규칙으로 여기서 먼저 걸러
|
|
4200
|
+
* 큰 파일을 CDN까지 보내고 나서 거절되는 왕복을 없앤다.
|
|
4201
|
+
*/
|
|
4202
|
+
upload: (file: Blob, options?: {
|
|
4203
|
+
purpose?: UploadPurpose;
|
|
4204
|
+
filename?: string;
|
|
4205
|
+
}) => Promise<UploadTicket>;
|
|
4126
4206
|
};
|
|
4127
4207
|
products: {
|
|
4128
4208
|
list: (params?: ProductListParams) => Promise<{
|
|
@@ -4609,7 +4689,7 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
4609
4689
|
sellerMemo: string | null;
|
|
4610
4690
|
orderedAt: string;
|
|
4611
4691
|
lastChangedAt: string;
|
|
4612
|
-
lastChangedType: "ORDER.PAID" | "ORDER.CONFIRMED" | "ORDER.DISPATCHED" | "ORDER.DELIVERED" | "ORDER.PURCHASE_DECIDED" | "ORDER.ADDRESS_CHANGE_REQUESTED" | "CLAIM.CANCEL_REQUESTED" | "CLAIM.RETURN_REQUESTED" | "CLAIM.EXCHANGE_REQUESTED";
|
|
4692
|
+
lastChangedType: "ORDER.PAID" | "ORDER.CONFIRMED" | "ORDER.DISPATCHED" | "ORDER.DELIVERED" | "ORDER.PURCHASE_DECIDED" | "ORDER.ADDRESS_CHANGE_REQUESTED" | "CLAIM.CANCEL_REQUESTED" | "CLAIM.RETURN_REQUESTED" | "CLAIM.EXCHANGE_REQUESTED" | "CLAIM.COMPLETED";
|
|
4613
4693
|
testPayment: boolean;
|
|
4614
4694
|
}[];
|
|
4615
4695
|
}>;
|
|
@@ -4643,7 +4723,7 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
4643
4723
|
sellerMemo: string | null;
|
|
4644
4724
|
orderedAt: string;
|
|
4645
4725
|
lastChangedAt: string;
|
|
4646
|
-
lastChangedType: "ORDER.PAID" | "ORDER.CONFIRMED" | "ORDER.DISPATCHED" | "ORDER.DELIVERED" | "ORDER.PURCHASE_DECIDED" | "ORDER.ADDRESS_CHANGE_REQUESTED" | "CLAIM.CANCEL_REQUESTED" | "CLAIM.RETURN_REQUESTED" | "CLAIM.EXCHANGE_REQUESTED";
|
|
4726
|
+
lastChangedType: "ORDER.PAID" | "ORDER.CONFIRMED" | "ORDER.DISPATCHED" | "ORDER.DELIVERED" | "ORDER.PURCHASE_DECIDED" | "ORDER.ADDRESS_CHANGE_REQUESTED" | "CLAIM.CANCEL_REQUESTED" | "CLAIM.RETURN_REQUESTED" | "CLAIM.EXCHANGE_REQUESTED" | "CLAIM.COMPLETED";
|
|
4647
4727
|
testPayment: boolean;
|
|
4648
4728
|
}[];
|
|
4649
4729
|
}>;
|
|
@@ -4696,6 +4776,46 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
4696
4776
|
}[];
|
|
4697
4777
|
}>;
|
|
4698
4778
|
updateTracking: (orderItemId: string, carrierCode: string, trackingNumber: string) => Promise<void>;
|
|
4779
|
+
/**
|
|
4780
|
+
* 배송완료 처리 — `DISPATCHED`·`DELIVERING`만 넘어간다. 응답의 `autoDecisionDate`가 자동 구매확정 예정일이다
|
|
4781
|
+
* (스토어 설정 `autoConfirmDays`가 0이면 null).
|
|
4782
|
+
*/
|
|
4783
|
+
markDelivered: (orderItemId: string, idempotencyKey?: string) => Promise<{
|
|
4784
|
+
orderItemId: string;
|
|
4785
|
+
orderId: string;
|
|
4786
|
+
status: "PAID" | "PAYMENT_WAITING" | "CONFIRMED" | "DISPATCHED" | "DELIVERING" | "DELIVERED" | "PURCHASE_DECIDED" | "CANCELED";
|
|
4787
|
+
claimStatus: "CANCEL_REQUESTED" | "RETURN_REQUESTED" | "EXCHANGE_REQUESTED" | null;
|
|
4788
|
+
productId: string;
|
|
4789
|
+
productName: string;
|
|
4790
|
+
optionId: string | null;
|
|
4791
|
+
optionName: string | null;
|
|
4792
|
+
sellerCode: string | null;
|
|
4793
|
+
quantity: number;
|
|
4794
|
+
unitPrice: number;
|
|
4795
|
+
discountAmount: number;
|
|
4796
|
+
totalPrice: number;
|
|
4797
|
+
hold: {
|
|
4798
|
+
reason: string;
|
|
4799
|
+
expectedDispatchDate: string;
|
|
4800
|
+
} | null;
|
|
4801
|
+
autoDecisionDate: string | null;
|
|
4802
|
+
sellerMemo: string | null;
|
|
4803
|
+
orderedAt: string;
|
|
4804
|
+
lastChangedAt: string;
|
|
4805
|
+
lastChangedType: "ORDER.PAID" | "ORDER.CONFIRMED" | "ORDER.DISPATCHED" | "ORDER.DELIVERED" | "ORDER.PURCHASE_DECIDED" | "ORDER.ADDRESS_CHANGE_REQUESTED" | "CLAIM.CANCEL_REQUESTED" | "CLAIM.RETURN_REQUESTED" | "CLAIM.EXCHANGE_REQUESTED" | "CLAIM.COMPLETED";
|
|
4806
|
+
testPayment: boolean;
|
|
4807
|
+
}>;
|
|
4808
|
+
/** 배송완료 일괄 처리 — 항목별 부분 성공이다 */
|
|
4809
|
+
markDeliveredBulk: (orderItemIds: string[], idempotencyKey?: string) => Promise<{
|
|
4810
|
+
succeeded: string[];
|
|
4811
|
+
failed: {
|
|
4812
|
+
code: string;
|
|
4813
|
+
message: string;
|
|
4814
|
+
id?: string | undefined;
|
|
4815
|
+
productId?: string | undefined;
|
|
4816
|
+
orderItemId?: string | undefined;
|
|
4817
|
+
}[];
|
|
4818
|
+
}>;
|
|
4699
4819
|
getDelivery: (orderItemId: string) => Promise<{
|
|
4700
4820
|
orderItemId: string;
|
|
4701
4821
|
deliveryMethod: "COURIER" | "DIRECT" | "QUICK" | "NOTHING";
|
|
@@ -6418,6 +6538,8 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
6418
6538
|
analyticsAllowedOrigins: string[];
|
|
6419
6539
|
storefrontOrigins: string[];
|
|
6420
6540
|
guestCheckoutEnabled: boolean;
|
|
6541
|
+
autoDeliverDays: number;
|
|
6542
|
+
autoConfirmDays: number;
|
|
6421
6543
|
memberCount: number;
|
|
6422
6544
|
createdAt: string;
|
|
6423
6545
|
}>;
|
|
@@ -6437,6 +6559,8 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
6437
6559
|
analyticsAllowedOrigins: string[];
|
|
6438
6560
|
storefrontOrigins: string[];
|
|
6439
6561
|
guestCheckoutEnabled: boolean;
|
|
6562
|
+
autoDeliverDays: number;
|
|
6563
|
+
autoConfirmDays: number;
|
|
6440
6564
|
memberCount: number;
|
|
6441
6565
|
createdAt: string;
|
|
6442
6566
|
}>;
|
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
|
|
@@ -1692,6 +1693,92 @@ const purchaseDecisionExtendResultSchema = z.object({
|
|
|
1692
1693
|
*/
|
|
1693
1694
|
const addressChangeRequestListSchema = z.array(z.record(z.string(), z.unknown())).describe("배송지 변경 요청 목록 (현재 항상 빈 배열)");
|
|
1694
1695
|
//#endregion
|
|
1696
|
+
//#region src/schemas/upload.ts
|
|
1697
|
+
/**
|
|
1698
|
+
* 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
|
|
1699
|
+
*
|
|
1700
|
+
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
1701
|
+
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
1702
|
+
*/
|
|
1703
|
+
const uploadPurposeSchema = z.enum(["product-image", "description"]);
|
|
1704
|
+
const MB = 1048576;
|
|
1705
|
+
/**
|
|
1706
|
+
* 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
|
|
1707
|
+
*
|
|
1708
|
+
* 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
|
|
1709
|
+
* 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
|
|
1710
|
+
*/
|
|
1711
|
+
const UPLOAD_PURPOSE_LIMITS = {
|
|
1712
|
+
"product-image": {
|
|
1713
|
+
extensions: [
|
|
1714
|
+
"jpg",
|
|
1715
|
+
"jpeg",
|
|
1716
|
+
"png",
|
|
1717
|
+
"webp",
|
|
1718
|
+
"avif",
|
|
1719
|
+
"gif"
|
|
1720
|
+
],
|
|
1721
|
+
maxBytes: 10 * MB
|
|
1722
|
+
},
|
|
1723
|
+
description: {
|
|
1724
|
+
extensions: [
|
|
1725
|
+
"jpg",
|
|
1726
|
+
"jpeg",
|
|
1727
|
+
"png",
|
|
1728
|
+
"gif",
|
|
1729
|
+
"webp",
|
|
1730
|
+
"avif",
|
|
1731
|
+
"svg",
|
|
1732
|
+
"mp4",
|
|
1733
|
+
"webm",
|
|
1734
|
+
"mov"
|
|
1735
|
+
],
|
|
1736
|
+
maxBytes: 50 * MB
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|
|
1740
|
+
function uploadExtensionOf(filename) {
|
|
1741
|
+
const dot = filename.lastIndexOf(".");
|
|
1742
|
+
if (dot < 0 || dot === filename.length - 1) return "";
|
|
1743
|
+
return filename.slice(dot + 1).toLowerCase();
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* 용도 제한 위반 여부. 통과하면 `null`.
|
|
1747
|
+
* 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
|
|
1748
|
+
*/
|
|
1749
|
+
function uploadRejection(purpose, filename, size) {
|
|
1750
|
+
const limit = UPLOAD_PURPOSE_LIMITS[purpose];
|
|
1751
|
+
if (!limit.extensions.includes(uploadExtensionOf(filename))) return {
|
|
1752
|
+
status: 400,
|
|
1753
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
1754
|
+
message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`
|
|
1755
|
+
};
|
|
1756
|
+
if (size !== void 0 && size > limit.maxBytes) return {
|
|
1757
|
+
status: 413,
|
|
1758
|
+
code: "FILE_TOO_LARGE",
|
|
1759
|
+
message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`
|
|
1760
|
+
};
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* 미디어 업로드 자리 발급 — `POST /v1/uploads`.
|
|
1765
|
+
*
|
|
1766
|
+
* 파일 바이트는 api를 거치지 않는다. 응답의 `uploadUrl`로 클라이언트가 직접 PUT하고,
|
|
1767
|
+
* 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
|
|
1768
|
+
* (확장자만 쓰이고 이름 자체는 저장되지 않는다).
|
|
1769
|
+
*/
|
|
1770
|
+
const uploadRequestSchema = z.object({
|
|
1771
|
+
filename: z.string().min(1).max(255),
|
|
1772
|
+
purpose: uploadPurposeSchema.default("description").describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
|
|
1773
|
+
size: z.number().int().positive().optional().describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다")
|
|
1774
|
+
});
|
|
1775
|
+
const uploadTicketSchema = z.object({
|
|
1776
|
+
/** 업로드 완료 후 공개될 주소 */
|
|
1777
|
+
url: z.url(),
|
|
1778
|
+
/** 파일 바이트를 PUT할 presigned URL. 1시간 유효 */
|
|
1779
|
+
uploadUrl: z.url()
|
|
1780
|
+
});
|
|
1781
|
+
//#endregion
|
|
1695
1782
|
//#region src/schemas/product.ts
|
|
1696
1783
|
const productStatusSchema = z.enum([
|
|
1697
1784
|
"SALE",
|
|
@@ -1775,6 +1862,54 @@ const productSchema = productSummarySchema.extend({
|
|
|
1775
1862
|
returnAddressId: z.string().nullable().optional(),
|
|
1776
1863
|
createdAt: z.string().optional()
|
|
1777
1864
|
});
|
|
1865
|
+
/** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
|
|
1866
|
+
const MAX_PRODUCT_IMAGES = 10;
|
|
1867
|
+
/**
|
|
1868
|
+
* 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
|
|
1869
|
+
*
|
|
1870
|
+
* https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
|
|
1871
|
+
* **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
|
|
1872
|
+
* 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
|
|
1873
|
+
*
|
|
1874
|
+
* 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
|
|
1875
|
+
* 만들면 상품 조회 자체가 막힌다.
|
|
1876
|
+
*/
|
|
1877
|
+
const productImageUrlSchema = z.url().max(2048).describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
|
|
1878
|
+
/**
|
|
1879
|
+
* 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
|
|
1880
|
+
*
|
|
1881
|
+
* - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
|
|
1882
|
+
* 상세 화면에 박히는 주입 경로가 된다.
|
|
1883
|
+
* - 호스트는 제한하지 않는다 — 셀러가 이미 쓰는 이미지 호스트를 그대로 붙일 수 있어야 한다.
|
|
1884
|
+
* - 경로에 확장자가 있으면 상품 이미지 형식이어야 한다(svg·동영상 거부). **확장자가 없으면 통과시킨다**
|
|
1885
|
+
* — 외부 이미지 CDN은 확장자 없는 키를 쓰는 경우가 흔하고, 주소만으로는 실제 형식을 알 수 없다.
|
|
1886
|
+
*/
|
|
1887
|
+
function productImageRejection(url) {
|
|
1888
|
+
if (url.length > 2048) return {
|
|
1889
|
+
code: "INVALID_IMAGE_URL",
|
|
1890
|
+
message: "이미지 주소가 너무 깁니다"
|
|
1891
|
+
};
|
|
1892
|
+
let parsed;
|
|
1893
|
+
try {
|
|
1894
|
+
parsed = new URL(url);
|
|
1895
|
+
} catch {
|
|
1896
|
+
return {
|
|
1897
|
+
code: "INVALID_IMAGE_URL",
|
|
1898
|
+
message: "이미지 주소 형식이 아닙니다"
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
if (parsed.protocol !== "https:") return {
|
|
1902
|
+
code: "INVALID_IMAGE_URL",
|
|
1903
|
+
message: "이미지 주소는 https로 시작해야 합니다"
|
|
1904
|
+
};
|
|
1905
|
+
const extension = uploadExtensionOf(parsed.pathname);
|
|
1906
|
+
const allowed = UPLOAD_PURPOSE_LIMITS["product-image"].extensions;
|
|
1907
|
+
if (extension && !allowed.includes(extension)) return {
|
|
1908
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
1909
|
+
message: `상품 이미지는 ${allowed.join(", ")}만 쓸 수 있습니다`
|
|
1910
|
+
};
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1778
1913
|
/** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
|
|
1779
1914
|
const MAX_OPTION_GROUPS = 3;
|
|
1780
1915
|
/** 축 하나가 가질 수 있는 값의 최대 개수 */
|
|
@@ -1856,7 +1991,7 @@ const productCreateRequestSchema = z.object({
|
|
|
1856
1991
|
* 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
|
|
1857
1992
|
*/
|
|
1858
1993
|
descriptionJson: z.string().nullable().optional(),
|
|
1859
|
-
images: z.array(
|
|
1994
|
+
images: z.array(productImageUrlSchema).min(1).max(10).optional().describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
|
|
1860
1995
|
attributes: z.array(z.object({
|
|
1861
1996
|
attributeId: z.string(),
|
|
1862
1997
|
value: z.string()
|
|
@@ -1891,12 +2026,6 @@ const productListQuerySchema = z.object({
|
|
|
1891
2026
|
sort: productSortSchema.optional().describe("정렬 (기본 createdAt.desc)"),
|
|
1892
2027
|
...pageQueryShape()
|
|
1893
2028
|
});
|
|
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
2029
|
const discountSchema = z.object({
|
|
1901
2030
|
productId: z.string(),
|
|
1902
2031
|
discountType: z.enum(["RATE", "AMOUNT"]),
|
|
@@ -1985,6 +2114,10 @@ function isWebOrigin(value) {
|
|
|
1985
2114
|
const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요").describe("스토어프론트 오리진. 예: `https://shop.example.com`");
|
|
1986
2115
|
/** 허용 오리진 상한 */
|
|
1987
2116
|
const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
2117
|
+
/** 자동 배송완료·자동 구매확정 대기일 상한 */
|
|
2118
|
+
const MAX_AUTO_TRANSITION_DAYS = 30;
|
|
2119
|
+
const autoDeliverDaysDescription = "발송 후 자동 배송완료까지의 일수(0~30, 기본 7). 발송(`DISPATCHED`)·배송중(`DELIVERING`) 주문상품을 이 기간이 지나면 배송완료(`DELIVERED`)로 바꾼다. `0`이면 자동 배송완료를 하지 않는다";
|
|
2120
|
+
const autoConfirmDaysDescription = "배송완료 후 자동 구매확정까지의 일수(0~30, 기본 7). 배송완료 시점에 구매확정 예정일(`autoDecisionDate`)을 이 값으로 잡고, 그 날짜가 지나면 구매확정(`PURCHASE_DECIDED`)으로 바꾼다. 진행 중인 클레임이 있으면 건너뛴다. `0`이면 자동 구매확정을 하지 않는다";
|
|
1988
2121
|
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
1989
2122
|
const updateStoreRequestSchema = z.object({
|
|
1990
2123
|
name: z.string().min(2).max(50).optional(),
|
|
@@ -1993,7 +2126,9 @@ const updateStoreRequestSchema = z.object({
|
|
|
1993
2126
|
businessHours: z.string().max(100).optional(),
|
|
1994
2127
|
analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프론트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다"),
|
|
1995
2128
|
storefrontOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("스토어프론트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. `analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)"),
|
|
1996
|
-
guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다")
|
|
2129
|
+
guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다"),
|
|
2130
|
+
autoDeliverDays: z.number().int().min(0).max(30).optional().describe(autoDeliverDaysDescription),
|
|
2131
|
+
autoConfirmDays: z.number().int().min(0).max(30).optional().describe(autoConfirmDaysDescription)
|
|
1997
2132
|
}).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
|
|
1998
2133
|
const storeSchema = z.object({
|
|
1999
2134
|
storeId: z.string(),
|
|
@@ -2011,6 +2146,8 @@ const storeSchema = z.object({
|
|
|
2011
2146
|
analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프론트 오리진. 비어 있으면 모든 오리진에서 받는다"),
|
|
2012
2147
|
storefrontOrigins: z.array(z.string()).describe("스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)"),
|
|
2013
2148
|
guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
|
|
2149
|
+
autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
|
|
2150
|
+
autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
|
|
2014
2151
|
memberCount: z.number().int(),
|
|
2015
2152
|
createdAt: z.string()
|
|
2016
2153
|
});
|
|
@@ -2068,22 +2205,6 @@ const sellerAddressRequestSchema = z.object({
|
|
|
2068
2205
|
address2: z.string().optional()
|
|
2069
2206
|
});
|
|
2070
2207
|
//#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
2208
|
//#region src/client.ts
|
|
2088
2209
|
function formBody(fields) {
|
|
2089
2210
|
return new URLSearchParams(fields).toString();
|
|
@@ -2211,7 +2332,35 @@ function createStoreClient(options) {
|
|
|
2211
2332
|
* 미디어 업로드 — 자리를 받아(`POST /uploads`) 파일 바이트는 호출자가
|
|
2212
2333
|
* `uploadUrl`로 직접 PUT한다. 업로드가 끝나면 `url`이 공개 주소가 된다.
|
|
2213
2334
|
*/
|
|
2214
|
-
uploads: {
|
|
2335
|
+
uploads: {
|
|
2336
|
+
create: (body) => storeHttp.request("POST", "/uploads", uploadTicketSchema, { body: uploadRequestSchema.parse(body) }),
|
|
2337
|
+
/**
|
|
2338
|
+
* 자리 발급 + 바이트 PUT을 한 번에 — 끝나면 `url`이 공개 주소다.
|
|
2339
|
+
*
|
|
2340
|
+
* 용도 제한(확장자·크기)은 서버가 판정하지만, 같은 규칙으로 여기서 먼저 걸러
|
|
2341
|
+
* 큰 파일을 CDN까지 보내고 나서 거절되는 왕복을 없앤다.
|
|
2342
|
+
*/
|
|
2343
|
+
upload: async (file, options) => {
|
|
2344
|
+
const filename = options?.filename ?? (typeof File !== "undefined" && file instanceof File ? file.name : "");
|
|
2345
|
+
if (!filename) throw new ApiError(400, "UPLOAD_FILENAME_REQUIRED", "파일명이 필요합니다");
|
|
2346
|
+
if (file.size === 0) throw new ApiError(400, "EMPTY_FILE", `파일이 비어 있습니다: ${filename}`);
|
|
2347
|
+
const purpose = options?.purpose ?? "description";
|
|
2348
|
+
const rejection = uploadRejection(purpose, filename, file.size);
|
|
2349
|
+
if (rejection) throw new ApiError(rejection.status, rejection.code, rejection.message);
|
|
2350
|
+
const ticket = await storeHttp.request("POST", "/uploads", uploadTicketSchema, { body: uploadRequestSchema.parse({
|
|
2351
|
+
filename,
|
|
2352
|
+
purpose,
|
|
2353
|
+
size: file.size
|
|
2354
|
+
}) });
|
|
2355
|
+
const put = await fetchFn(ticket.uploadUrl, {
|
|
2356
|
+
method: "PUT",
|
|
2357
|
+
headers: { "content-type": file.type || "application/octet-stream" },
|
|
2358
|
+
body: file
|
|
2359
|
+
});
|
|
2360
|
+
if (!put.ok) throw new ApiError(put.status, "UPLOAD_PUT_FAILED", `업로드에 실패했습니다: ${filename}`);
|
|
2361
|
+
return ticket;
|
|
2362
|
+
}
|
|
2363
|
+
},
|
|
2215
2364
|
products: {
|
|
2216
2365
|
list: (params) => storeHttp.request("GET", "/products", pageSchema(productSummarySchema), { query: params }),
|
|
2217
2366
|
get: (productId) => storeHttp.request("GET", path`/products/${productId}`, productSchema),
|
|
@@ -2292,6 +2441,16 @@ function createStoreClient(options) {
|
|
|
2292
2441
|
carrierCode,
|
|
2293
2442
|
trackingNumber
|
|
2294
2443
|
} }),
|
|
2444
|
+
/**
|
|
2445
|
+
* 배송완료 처리 — `DISPATCHED`·`DELIVERING`만 넘어간다. 응답의 `autoDecisionDate`가 자동 구매확정 예정일이다
|
|
2446
|
+
* (스토어 설정 `autoConfirmDays`가 0이면 null).
|
|
2447
|
+
*/
|
|
2448
|
+
markDelivered: (orderItemId, idempotencyKey) => storeHttp.request("POST", path`/order-items/${orderItemId}/delivered`, orderItemSchema, { headers: idempotency(idempotencyKey) }),
|
|
2449
|
+
/** 배송완료 일괄 처리 — 항목별 부분 성공이다 */
|
|
2450
|
+
markDeliveredBulk: (orderItemIds, idempotencyKey) => storeHttp.request("POST", "/order-items/delivered", bulkResultSchema, {
|
|
2451
|
+
body: { orderItemIds },
|
|
2452
|
+
headers: idempotency(idempotencyKey)
|
|
2453
|
+
}),
|
|
2295
2454
|
getDelivery: (orderItemId) => storeHttp.request("GET", path`/order-items/${orderItemId}/delivery`, deliveryStatusSchema)
|
|
2296
2455
|
},
|
|
2297
2456
|
carriers: { list: () => storeHttp.request("GET", "/carriers", z.array(carrierSchema)) },
|
|
@@ -2926,4 +3085,4 @@ function createStoreSession(options) {
|
|
|
2926
3085
|
};
|
|
2927
3086
|
}
|
|
2928
3087
|
//#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,
|
|
3088
|
+
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, 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, 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, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, rejectClaimRequestSchema, 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 };
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { base64Utf8 } from "./base64";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ApiError,
|
|
5
|
+
createHttp,
|
|
6
|
+
path,
|
|
7
|
+
type Query,
|
|
8
|
+
resolveToken,
|
|
9
|
+
type TokenSource,
|
|
10
|
+
unwrapData,
|
|
11
|
+
} from "./http";
|
|
4
12
|
import {
|
|
5
13
|
type AnalyticsPagesParams,
|
|
6
14
|
type AnalyticsProductsParams,
|
|
@@ -173,7 +181,14 @@ import {
|
|
|
173
181
|
type UpdateStoreRequest,
|
|
174
182
|
updateStoreRequestSchema,
|
|
175
183
|
} from "./schemas/store";
|
|
176
|
-
import {
|
|
184
|
+
import {
|
|
185
|
+
type UploadPurpose,
|
|
186
|
+
type UploadRequest,
|
|
187
|
+
type UploadTicket,
|
|
188
|
+
uploadRejection,
|
|
189
|
+
uploadRequestSchema,
|
|
190
|
+
uploadTicketSchema,
|
|
191
|
+
} from "./schemas/upload";
|
|
177
192
|
import type { AuthorizeOptions, StoreSession } from "./session";
|
|
178
193
|
|
|
179
194
|
export interface StoreClientOptions {
|
|
@@ -372,6 +387,44 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
372
387
|
storeHttp.request("POST", "/uploads", uploadTicketSchema, {
|
|
373
388
|
body: uploadRequestSchema.parse(body),
|
|
374
389
|
}),
|
|
390
|
+
/**
|
|
391
|
+
* 자리 발급 + 바이트 PUT을 한 번에 — 끝나면 `url`이 공개 주소다.
|
|
392
|
+
*
|
|
393
|
+
* 용도 제한(확장자·크기)은 서버가 판정하지만, 같은 규칙으로 여기서 먼저 걸러
|
|
394
|
+
* 큰 파일을 CDN까지 보내고 나서 거절되는 왕복을 없앤다.
|
|
395
|
+
*/
|
|
396
|
+
upload: async (
|
|
397
|
+
file: Blob,
|
|
398
|
+
options?: { purpose?: UploadPurpose; filename?: string },
|
|
399
|
+
): Promise<UploadTicket> => {
|
|
400
|
+
const filename =
|
|
401
|
+
options?.filename ??
|
|
402
|
+
(typeof File !== "undefined" && file instanceof File ? file.name : "");
|
|
403
|
+
if (!filename) {
|
|
404
|
+
throw new ApiError(400, "UPLOAD_FILENAME_REQUIRED", "파일명이 필요합니다");
|
|
405
|
+
}
|
|
406
|
+
if (file.size === 0) {
|
|
407
|
+
// 빈 파일은 PUT 자체는 성공하고 0바이트 객체가 공개돼 깨진 이미지로 남는다
|
|
408
|
+
throw new ApiError(400, "EMPTY_FILE", `파일이 비어 있습니다: ${filename}`);
|
|
409
|
+
}
|
|
410
|
+
const purpose = options?.purpose ?? "description";
|
|
411
|
+
const rejection = uploadRejection(purpose, filename, file.size);
|
|
412
|
+
if (rejection) {
|
|
413
|
+
throw new ApiError(rejection.status, rejection.code, rejection.message);
|
|
414
|
+
}
|
|
415
|
+
const ticket = await storeHttp.request("POST", "/uploads", uploadTicketSchema, {
|
|
416
|
+
body: uploadRequestSchema.parse({ filename, purpose, size: file.size }),
|
|
417
|
+
});
|
|
418
|
+
const put = await fetchFn(ticket.uploadUrl, {
|
|
419
|
+
method: "PUT",
|
|
420
|
+
headers: { "content-type": file.type || "application/octet-stream" },
|
|
421
|
+
body: file,
|
|
422
|
+
});
|
|
423
|
+
if (!put.ok) {
|
|
424
|
+
throw new ApiError(put.status, "UPLOAD_PUT_FAILED", `업로드에 실패했습니다: ${filename}`);
|
|
425
|
+
}
|
|
426
|
+
return ticket;
|
|
427
|
+
},
|
|
375
428
|
},
|
|
376
429
|
|
|
377
430
|
products: {
|
|
@@ -532,6 +585,20 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
532
585
|
storeHttp.requestVoid("PUT", path`/order-items/${orderItemId}/dispatch/tracking`, {
|
|
533
586
|
body: { carrierCode, trackingNumber },
|
|
534
587
|
}),
|
|
588
|
+
/**
|
|
589
|
+
* 배송완료 처리 — `DISPATCHED`·`DELIVERING`만 넘어간다. 응답의 `autoDecisionDate`가 자동 구매확정 예정일이다
|
|
590
|
+
* (스토어 설정 `autoConfirmDays`가 0이면 null).
|
|
591
|
+
*/
|
|
592
|
+
markDelivered: (orderItemId: string, idempotencyKey?: string) =>
|
|
593
|
+
storeHttp.request("POST", path`/order-items/${orderItemId}/delivered`, orderItemSchema, {
|
|
594
|
+
headers: idempotency(idempotencyKey),
|
|
595
|
+
}),
|
|
596
|
+
/** 배송완료 일괄 처리 — 항목별 부분 성공이다 */
|
|
597
|
+
markDeliveredBulk: (orderItemIds: string[], idempotencyKey?: string) =>
|
|
598
|
+
storeHttp.request("POST", "/order-items/delivered", bulkResultSchema, {
|
|
599
|
+
body: { orderItemIds },
|
|
600
|
+
headers: idempotency(idempotencyKey),
|
|
601
|
+
}),
|
|
535
602
|
getDelivery: (orderItemId: string) =>
|
|
536
603
|
storeHttp.request("GET", path`/order-items/${orderItemId}/delivery`, deliveryStatusSchema),
|
|
537
604
|
},
|
package/src/schemas/common.ts
CHANGED
|
@@ -67,6 +67,9 @@ export const changeTypeSchema = z.enum([
|
|
|
67
67
|
"CLAIM.CANCEL_REQUESTED",
|
|
68
68
|
"CLAIM.RETURN_REQUESTED",
|
|
69
69
|
"CLAIM.EXCHANGE_REQUESTED",
|
|
70
|
+
// 취소·반품 승인으로 주문상품이 취소됐다 — 서버가 이 값을 쓰므로 목록에 있어야 한다
|
|
71
|
+
// (빠져 있던 동안 클레임이 끝난 주문상품이 섞이면 응답 검증이 깨졌다)
|
|
72
|
+
"CLAIM.COMPLETED",
|
|
70
73
|
"ORDER.ADDRESS_CHANGE_REQUESTED",
|
|
71
74
|
]);
|
|
72
75
|
|
package/src/schemas/product.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type PageParams, pageQueryShape } from "./common";
|
|
3
|
+
import { UPLOAD_PURPOSE_LIMITS, uploadExtensionOf } from "./upload";
|
|
3
4
|
|
|
4
5
|
export const productStatusSchema = z.enum(["SALE", "SUSPENDED", "OUT_OF_STOCK", "CLOSED"]);
|
|
5
6
|
|
|
@@ -96,6 +97,62 @@ export const productSchema = productSummarySchema.extend({
|
|
|
96
97
|
|
|
97
98
|
export type Product = z.infer<typeof productSchema>;
|
|
98
99
|
|
|
100
|
+
/** 상품 하나에 등록할 수 있는 이미지 최대 장수 — 첫 장이 대표(썸네일)다 */
|
|
101
|
+
export const MAX_PRODUCT_IMAGES = 10;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 상품 이미지 주소의 와이어 형식 — URL이고 2048자 이하다.
|
|
105
|
+
*
|
|
106
|
+
* https·확장자 규칙은 여기서 강제하지 않고 `productImageRejection`이 판정한다. 서버가
|
|
107
|
+
* **새로 추가된 주소에만** 그 규칙을 적용해야 하기 때문이다(규칙이 생기기 전에 저장된
|
|
108
|
+
* 주소 하나로 상품 수정 전체가 막히면 셀러가 빠져나갈 길이 없다).
|
|
109
|
+
*
|
|
110
|
+
* 응답 스키마(`productSchema.images`)도 넓게 둔다. 과거에 저장된 주소를 파싱 오류로
|
|
111
|
+
* 만들면 상품 조회 자체가 막힌다.
|
|
112
|
+
*/
|
|
113
|
+
export const productImageUrlSchema = z
|
|
114
|
+
.url()
|
|
115
|
+
.max(2048)
|
|
116
|
+
.describe("상품 이미지 주소. https만 허용하고, 확장자가 있으면 이미지 형식이어야 한다");
|
|
117
|
+
|
|
118
|
+
export interface ProductImageRejection {
|
|
119
|
+
code: "INVALID_IMAGE_URL" | "UNSUPPORTED_FILE_TYPE";
|
|
120
|
+
message: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 상품에 **새로 붙이는** 이미지 주소의 규칙. 통과하면 `null`.
|
|
125
|
+
*
|
|
126
|
+
* - https만 받는다. http는 https 스토어프론트에서 혼합 콘텐츠로 차단되고 `data:`·`javascript:`는
|
|
127
|
+
* 상세 화면에 박히는 주입 경로가 된다.
|
|
128
|
+
* - 호스트는 제한하지 않는다 — 셀러가 이미 쓰는 이미지 호스트를 그대로 붙일 수 있어야 한다.
|
|
129
|
+
* - 경로에 확장자가 있으면 상품 이미지 형식이어야 한다(svg·동영상 거부). **확장자가 없으면 통과시킨다**
|
|
130
|
+
* — 외부 이미지 CDN은 확장자 없는 키를 쓰는 경우가 흔하고, 주소만으로는 실제 형식을 알 수 없다.
|
|
131
|
+
*/
|
|
132
|
+
export function productImageRejection(url: string): ProductImageRejection | null {
|
|
133
|
+
if (url.length > 2048) {
|
|
134
|
+
return { code: "INVALID_IMAGE_URL", message: "이미지 주소가 너무 깁니다" };
|
|
135
|
+
}
|
|
136
|
+
let parsed: URL;
|
|
137
|
+
try {
|
|
138
|
+
parsed = new URL(url);
|
|
139
|
+
} catch {
|
|
140
|
+
return { code: "INVALID_IMAGE_URL", message: "이미지 주소 형식이 아닙니다" };
|
|
141
|
+
}
|
|
142
|
+
if (parsed.protocol !== "https:") {
|
|
143
|
+
return { code: "INVALID_IMAGE_URL", message: "이미지 주소는 https로 시작해야 합니다" };
|
|
144
|
+
}
|
|
145
|
+
const extension = uploadExtensionOf(parsed.pathname);
|
|
146
|
+
const allowed = UPLOAD_PURPOSE_LIMITS["product-image"].extensions;
|
|
147
|
+
if (extension && !allowed.includes(extension)) {
|
|
148
|
+
return {
|
|
149
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
150
|
+
message: `상품 이미지는 ${allowed.join(", ")}만 쓸 수 있습니다`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
99
156
|
/** 옵션 축(그룹) 최대 개수 — 스키마 상한의 단일 원천 */
|
|
100
157
|
export const MAX_OPTION_GROUPS = 3;
|
|
101
158
|
/** 축 하나가 가질 수 있는 값의 최대 개수 */
|
|
@@ -212,7 +269,12 @@ export const productCreateRequestSchema = z
|
|
|
212
269
|
* 표시는 `description`(HTML)이 맡는다 — 소비자는 이 필드를 몰라도 된다.
|
|
213
270
|
*/
|
|
214
271
|
descriptionJson: z.string().nullable().optional(),
|
|
215
|
-
images: z
|
|
272
|
+
images: z
|
|
273
|
+
.array(productImageUrlSchema)
|
|
274
|
+
.min(1)
|
|
275
|
+
.max(MAX_PRODUCT_IMAGES)
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("상품 이미지 주소 목록. 첫 장이 대표 이미지(썸네일)다"),
|
|
216
278
|
attributes: z.array(z.object({ attributeId: z.string(), value: z.string() })).optional(),
|
|
217
279
|
deliveryType: deliveryTypeSchema
|
|
218
280
|
.default("PAID")
|
|
@@ -276,13 +338,6 @@ export const productListQuerySchema = z.object({
|
|
|
276
338
|
...pageQueryShape(),
|
|
277
339
|
});
|
|
278
340
|
|
|
279
|
-
/** `POST /v1/product-images` 응답 — 업로드된 이미지의 CDN 주소 */
|
|
280
|
-
export const productImageUploadResultSchema = z.object({
|
|
281
|
-
images: z.array(z.object({ url: z.url(), width: z.number().int(), height: z.number().int() })),
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
export type ProductImageUploadResult = z.infer<typeof productImageUploadResultSchema>;
|
|
285
|
-
|
|
286
341
|
export const discountSchema = z.object({
|
|
287
342
|
productId: z.string(),
|
|
288
343
|
discountType: z.enum(["RATE", "AMOUNT"]),
|
package/src/schemas/store.ts
CHANGED
|
@@ -102,6 +102,17 @@ export const analyticsOriginSchema = z
|
|
|
102
102
|
/** 허용 오리진 상한 */
|
|
103
103
|
export const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
104
104
|
|
|
105
|
+
/** 자동 배송완료·자동 구매확정 대기일 상한 */
|
|
106
|
+
export const MAX_AUTO_TRANSITION_DAYS = 30;
|
|
107
|
+
|
|
108
|
+
const autoDeliverDaysDescription =
|
|
109
|
+
"발송 후 자동 배송완료까지의 일수(0~30, 기본 7). 발송(`DISPATCHED`)·배송중(`DELIVERING`) 주문상품을 " +
|
|
110
|
+
"이 기간이 지나면 배송완료(`DELIVERED`)로 바꾼다. `0`이면 자동 배송완료를 하지 않는다";
|
|
111
|
+
|
|
112
|
+
const autoConfirmDaysDescription =
|
|
113
|
+
"배송완료 후 자동 구매확정까지의 일수(0~30, 기본 7). 배송완료 시점에 구매확정 예정일(`autoDecisionDate`)을 이 값으로 잡고, " +
|
|
114
|
+
"그 날짜가 지나면 구매확정(`PURCHASE_DECIDED`)으로 바꾼다. 진행 중인 클레임이 있으면 건너뛴다. `0`이면 자동 구매확정을 하지 않는다";
|
|
115
|
+
|
|
105
116
|
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
106
117
|
export const updateStoreRequestSchema = z
|
|
107
118
|
.object({
|
|
@@ -132,6 +143,20 @@ export const updateStoreRequestSchema = z
|
|
|
132
143
|
"비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. " +
|
|
133
144
|
"장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다",
|
|
134
145
|
),
|
|
146
|
+
autoDeliverDays: z
|
|
147
|
+
.number()
|
|
148
|
+
.int()
|
|
149
|
+
.min(0)
|
|
150
|
+
.max(MAX_AUTO_TRANSITION_DAYS)
|
|
151
|
+
.optional()
|
|
152
|
+
.describe(autoDeliverDaysDescription),
|
|
153
|
+
autoConfirmDays: z
|
|
154
|
+
.number()
|
|
155
|
+
.int()
|
|
156
|
+
.min(0)
|
|
157
|
+
.max(MAX_AUTO_TRANSITION_DAYS)
|
|
158
|
+
.optional()
|
|
159
|
+
.describe(autoConfirmDaysDescription),
|
|
135
160
|
})
|
|
136
161
|
.refine((value) => Object.values(value).some((v) => v !== undefined), {
|
|
137
162
|
message: "최소 1개 필드가 필요합니다",
|
|
@@ -156,6 +181,8 @@ export const storeSchema = z.object({
|
|
|
156
181
|
"스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)",
|
|
157
182
|
),
|
|
158
183
|
guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
|
|
184
|
+
autoDeliverDays: z.number().int().describe(autoDeliverDaysDescription),
|
|
185
|
+
autoConfirmDays: z.number().int().describe(autoConfirmDaysDescription),
|
|
159
186
|
memberCount: z.number().int(),
|
|
160
187
|
createdAt: z.string(),
|
|
161
188
|
});
|
package/src/schemas/upload.ts
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
|
|
5
|
+
*
|
|
6
|
+
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
7
|
+
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
8
|
+
*/
|
|
9
|
+
export const uploadPurposeSchema = z.enum(["product-image", "description"]);
|
|
10
|
+
|
|
11
|
+
export type UploadPurpose = z.infer<typeof uploadPurposeSchema>;
|
|
12
|
+
|
|
13
|
+
export interface UploadPurposeLimit {
|
|
14
|
+
/** 소문자 확장자 목록 */
|
|
15
|
+
extensions: readonly string[];
|
|
16
|
+
maxBytes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MB = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 용도별 업로드 제한 — 서버(`POST /v1/uploads`)와 클라이언트가 공유하는 단일 원천이다.
|
|
23
|
+
*
|
|
24
|
+
* 크기는 **요청이 스스로 신고한 값**으로만 검사한다. 바이트는 api를 거치지 않고 CDN으로
|
|
25
|
+
* 직접 PUT되므로 이 검사는 셀러가 올리기 전에 알게 하는 장치이고, 실제 상한은 CDN이 강제한다.
|
|
26
|
+
*/
|
|
27
|
+
export const UPLOAD_PURPOSE_LIMITS: Record<UploadPurpose, UploadPurposeLimit> = {
|
|
28
|
+
// svg는 스크립트를 품을 수 있고, 동영상은 목록 썸네일 자리에 쓸 수 없다
|
|
29
|
+
"product-image": {
|
|
30
|
+
extensions: ["jpg", "jpeg", "png", "webp", "avif", "gif"],
|
|
31
|
+
maxBytes: 10 * MB,
|
|
32
|
+
},
|
|
33
|
+
description: {
|
|
34
|
+
extensions: ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg", "mp4", "webm", "mov"],
|
|
35
|
+
maxBytes: 50 * MB,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|
|
40
|
+
export function uploadExtensionOf(filename: string): string {
|
|
41
|
+
const dot = filename.lastIndexOf(".");
|
|
42
|
+
if (dot < 0 || dot === filename.length - 1) return "";
|
|
43
|
+
return filename.slice(dot + 1).toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UploadRejection {
|
|
47
|
+
/** 400 형식 불일치 / 413 크기 초과 */
|
|
48
|
+
status: 400 | 413;
|
|
49
|
+
code: "UNSUPPORTED_FILE_TYPE" | "FILE_TOO_LARGE";
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 용도 제한 위반 여부. 통과하면 `null`.
|
|
55
|
+
* 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
|
|
56
|
+
*/
|
|
57
|
+
export function uploadRejection(
|
|
58
|
+
purpose: UploadPurpose,
|
|
59
|
+
filename: string,
|
|
60
|
+
size?: number,
|
|
61
|
+
): UploadRejection | null {
|
|
62
|
+
const limit = UPLOAD_PURPOSE_LIMITS[purpose];
|
|
63
|
+
if (!limit.extensions.includes(uploadExtensionOf(filename))) {
|
|
64
|
+
return {
|
|
65
|
+
status: 400,
|
|
66
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
67
|
+
message: `지원하지 않는 파일 형식입니다. ${limit.extensions.join(", ")}만 올릴 수 있습니다`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (size !== undefined && size > limit.maxBytes) {
|
|
71
|
+
return {
|
|
72
|
+
status: 413,
|
|
73
|
+
code: "FILE_TOO_LARGE",
|
|
74
|
+
message: `파일이 너무 큽니다. ${Math.floor(limit.maxBytes / MB)}MB까지 올릴 수 있습니다`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
3
80
|
/**
|
|
4
81
|
* 미디어 업로드 자리 발급 — `POST /v1/uploads`.
|
|
5
82
|
*
|
|
@@ -9,6 +86,15 @@ import { z } from "zod";
|
|
|
9
86
|
*/
|
|
10
87
|
export const uploadRequestSchema = z.object({
|
|
11
88
|
filename: z.string().min(1).max(255),
|
|
89
|
+
purpose: uploadPurposeSchema
|
|
90
|
+
.default("description")
|
|
91
|
+
.describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
|
|
92
|
+
size: z
|
|
93
|
+
.number()
|
|
94
|
+
.int()
|
|
95
|
+
.positive()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다"),
|
|
12
98
|
});
|
|
13
99
|
|
|
14
100
|
export type UploadRequest = z.infer<typeof uploadRequestSchema>;
|