@sayren/store-sdk 0.2.0 → 0.4.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 CHANGED
@@ -1108,6 +1108,145 @@ export declare const approveClaimResultSchema: z.ZodObject<{
1108
1108
  }, z.core.$strip>;
1109
1109
  export type ApproveClaimResult = z.infer<typeof approveClaimResultSchema>;
1110
1110
  //#endregion
1111
+ //#region src/schemas/customer.d.ts
1112
+ /** 구매자 목록 정렬 */
1113
+ export declare const customerSortSchema: z.ZodEnum<{
1114
+ joined_desc: "joined_desc";
1115
+ joined_asc: "joined_asc";
1116
+ total_paid_desc: "total_paid_desc";
1117
+ order_count_desc: "order_count_desc";
1118
+ last_ordered_desc: "last_ordered_desc";
1119
+ }>;
1120
+ export type CustomerSort = z.infer<typeof customerSortSchema>;
1121
+ /** `GET /v1/customers` 쿼리 */
1122
+ export declare const customerListQuerySchema: z.ZodObject<{
1123
+ page: z.ZodOptional<z.ZodString>;
1124
+ size: z.ZodOptional<z.ZodString>;
1125
+ q: z.ZodOptional<z.ZodString>;
1126
+ joinedFrom: z.ZodOptional<z.ZodString>;
1127
+ joinedTo: z.ZodOptional<z.ZodString>;
1128
+ marketing: z.ZodOptional<z.ZodEnum<{
1129
+ agreed: "agreed";
1130
+ declined: "declined";
1131
+ }>>;
1132
+ sort: z.ZodOptional<z.ZodEnum<{
1133
+ joined_desc: "joined_desc";
1134
+ joined_asc: "joined_asc";
1135
+ total_paid_desc: "total_paid_desc";
1136
+ order_count_desc: "order_count_desc";
1137
+ last_ordered_desc: "last_ordered_desc";
1138
+ }>>;
1139
+ }, z.core.$strip>;
1140
+ /** 구매 통계 — 구매 금액은 취소된 주문 상품을 빼고, 완료된 반품은 수량 비율로 뺀 금액이다 */
1141
+ export declare const customerStatsSchema: z.ZodObject<{
1142
+ orderCount: z.ZodNumber;
1143
+ totalPaid: z.ZodNumber;
1144
+ lastOrderedAt: z.ZodNullable<z.ZodString>;
1145
+ }, z.core.$strip>;
1146
+ /** 목록 행 — 휴대폰은 가운데를 가린다(`010-****-1234`) */
1147
+ export declare const customerSummarySchema: z.ZodObject<{
1148
+ customerId: z.ZodString;
1149
+ name: z.ZodString;
1150
+ email: z.ZodString;
1151
+ maskedPhone: z.ZodNullable<z.ZodString>;
1152
+ joinedAt: z.ZodString;
1153
+ point: z.ZodNumber;
1154
+ marketingAgreed: z.ZodBoolean;
1155
+ orderCount: z.ZodNumber;
1156
+ totalPaid: z.ZodNumber;
1157
+ lastOrderedAt: z.ZodNullable<z.ZodString>;
1158
+ }, z.core.$strip>;
1159
+ export type CustomerSummary = z.infer<typeof customerSummarySchema>;
1160
+ export declare const customerAddressSchema: z.ZodObject<{
1161
+ addressId: z.ZodString;
1162
+ alias: z.ZodString;
1163
+ isDefault: z.ZodBoolean;
1164
+ receiverName: z.ZodString;
1165
+ phone: z.ZodString;
1166
+ zipCode: z.ZodString;
1167
+ address1: z.ZodString;
1168
+ address2: z.ZodNullable<z.ZodString>;
1169
+ }, z.core.$strip>;
1170
+ /** 구매자 상세 — 휴대폰 전체를 보여 준다 */
1171
+ export declare const customerDetailSchema: z.ZodObject<{
1172
+ customerId: z.ZodString;
1173
+ name: z.ZodString;
1174
+ email: z.ZodString;
1175
+ phone: z.ZodNullable<z.ZodString>;
1176
+ joinedAt: z.ZodString;
1177
+ point: z.ZodNumber;
1178
+ marketingAgreed: z.ZodBoolean;
1179
+ loginMethods: z.ZodArray<z.ZodEnum<{
1180
+ password: "password";
1181
+ }>>;
1182
+ addresses: z.ZodArray<z.ZodObject<{
1183
+ addressId: z.ZodString;
1184
+ alias: z.ZodString;
1185
+ isDefault: z.ZodBoolean;
1186
+ receiverName: z.ZodString;
1187
+ phone: z.ZodString;
1188
+ zipCode: z.ZodString;
1189
+ address1: z.ZodString;
1190
+ address2: z.ZodNullable<z.ZodString>;
1191
+ }, z.core.$strip>>;
1192
+ stats: z.ZodObject<{
1193
+ orderCount: z.ZodNumber;
1194
+ totalPaid: z.ZodNumber;
1195
+ lastOrderedAt: z.ZodNullable<z.ZodString>;
1196
+ canceledOrReturnedCount: z.ZodNumber;
1197
+ reviewCount: z.ZodNumber;
1198
+ inquiryCount: z.ZodNumber;
1199
+ }, z.core.$strip>;
1200
+ }, z.core.$strip>;
1201
+ export type CustomerDetail = z.infer<typeof customerDetailSchema>;
1202
+ /** 구매자의 주문 — 전체 기간 */
1203
+ export declare const customerOrderSchema: z.ZodObject<{
1204
+ orderId: z.ZodString;
1205
+ orderedAt: z.ZodString;
1206
+ totalAmount: z.ZodNumber;
1207
+ testPayment: z.ZodBoolean;
1208
+ items: z.ZodArray<z.ZodObject<{
1209
+ orderItemId: z.ZodString;
1210
+ productName: z.ZodString;
1211
+ optionName: z.ZodNullable<z.ZodString>;
1212
+ quantity: z.ZodNumber;
1213
+ status: z.ZodEnum<{
1214
+ PAID: "PAID";
1215
+ PAYMENT_WAITING: "PAYMENT_WAITING";
1216
+ CONFIRMED: "CONFIRMED";
1217
+ DISPATCHED: "DISPATCHED";
1218
+ DELIVERING: "DELIVERING";
1219
+ DELIVERED: "DELIVERED";
1220
+ PURCHASE_DECIDED: "PURCHASE_DECIDED";
1221
+ CANCELED: "CANCELED";
1222
+ }>;
1223
+ claimStatus: z.ZodNullable<z.ZodEnum<{
1224
+ CANCEL_REQUESTED: "CANCEL_REQUESTED";
1225
+ RETURN_REQUESTED: "RETURN_REQUESTED";
1226
+ EXCHANGE_REQUESTED: "EXCHANGE_REQUESTED";
1227
+ }>>;
1228
+ }, z.core.$strip>>;
1229
+ }, z.core.$strip>;
1230
+ export type CustomerOrder = z.infer<typeof customerOrderSchema>;
1231
+ /** `POST /v1/customers/{id}/withdraw` 본문 */
1232
+ export declare const withdrawCustomerRequestSchema: z.ZodObject<{
1233
+ reason: z.ZodEnum<{
1234
+ BUYER_REQUEST: "BUYER_REQUEST";
1235
+ DORMANT: "DORMANT";
1236
+ ABUSE: "ABUSE";
1237
+ OTHER: "OTHER";
1238
+ }>;
1239
+ memo: z.ZodOptional<z.ZodString>;
1240
+ }, z.core.$strip>;
1241
+ export type WithdrawCustomerRequest = z.infer<typeof withdrawCustomerRequestSchema>;
1242
+ /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프런트 탈퇴와 같다) */
1243
+ export declare const customerWithdrawalBlockedDetailsSchema: z.ZodObject<{
1244
+ inProgressOrderItems: z.ZodNumber;
1245
+ openClaims: z.ZodNumber;
1246
+ pendingPayments: z.ZodNumber;
1247
+ }, z.core.$strip>;
1248
+ export type CustomerWithdrawalBlockedDetails = z.infer<typeof customerWithdrawalBlockedDetailsSchema>;
1249
+ //#endregion
1111
1250
  //#region src/schemas/misc.d.ts
1112
1251
  export declare const inquirySchema: z.ZodObject<{
1113
1252
  inquiryId: z.ZodString;
@@ -3320,6 +3459,22 @@ export declare const STORE_CODE_PATTERN: RegExp;
3320
3459
  export declare const RESERVED_STORE_CODES: readonly string[];
3321
3460
  export declare function isReservedStoreCode(code: string): boolean;
3322
3461
  export declare const storeCodeSchema: z.ZodString;
3462
+ /** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
3463
+ export declare function isWebOrigin(value: string): boolean;
3464
+ /** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
3465
+ export declare const analyticsOriginSchema: z.ZodString;
3466
+ /** 허용 오리진 상한 */
3467
+ export declare const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
3468
+ /** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
3469
+ export declare const updateStoreRequestSchema: z.ZodObject<{
3470
+ name: z.ZodOptional<z.ZodString>;
3471
+ logoUrl: z.ZodOptional<z.ZodURL>;
3472
+ customerCenterPhone: z.ZodOptional<z.ZodString>;
3473
+ businessHours: z.ZodOptional<z.ZodString>;
3474
+ analyticsAllowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
3475
+ guestCheckoutEnabled: z.ZodOptional<z.ZodBoolean>;
3476
+ }, z.core.$strip>;
3477
+ export type UpdateStoreRequest = z.infer<typeof updateStoreRequestSchema>;
3323
3478
  export declare const storeSchema: z.ZodObject<{
3324
3479
  storeId: z.ZodString;
3325
3480
  name: z.ZodString;
@@ -3333,6 +3488,8 @@ export declare const storeSchema: z.ZodObject<{
3333
3488
  logoUrl: z.ZodNullable<z.ZodURL>;
3334
3489
  businessNumber: z.ZodNullable<z.ZodString>;
3335
3490
  customerCenterPhone: z.ZodNullable<z.ZodString>;
3491
+ analyticsAllowedOrigins: z.ZodArray<z.ZodString>;
3492
+ guestCheckoutEnabled: z.ZodBoolean;
3336
3493
  memberCount: z.ZodNumber;
3337
3494
  createdAt: z.ZodString;
3338
3495
  }, z.core.$strip>;
@@ -4448,6 +4605,76 @@ export declare function createStoreClient(options: StoreClientOptions): {
4448
4605
  answer: (inquiryId: string, content: string) => Promise<void>;
4449
4606
  updateAnswer: (inquiryId: string, content: string) => Promise<void>;
4450
4607
  };
4608
+ /** 구매자 관리 — 조회 `customer:r`, 탈퇴 처리 `customer:rw`(콘솔 OWNER·ADMIN) */
4609
+ customers: {
4610
+ list: (params?: PageParams & Omit<z.input<typeof customerListQuerySchema>, "page" | "size">) => Promise<{
4611
+ page: number;
4612
+ size: number;
4613
+ totalElements: number;
4614
+ totalPages: number;
4615
+ contents: {
4616
+ customerId: string;
4617
+ name: string;
4618
+ email: string;
4619
+ maskedPhone: string | null;
4620
+ joinedAt: string;
4621
+ point: number;
4622
+ marketingAgreed: boolean;
4623
+ orderCount: number;
4624
+ totalPaid: number;
4625
+ lastOrderedAt: string | null;
4626
+ }[];
4627
+ }>;
4628
+ get: (customerId: string) => Promise<{
4629
+ customerId: string;
4630
+ name: string;
4631
+ email: string;
4632
+ phone: string | null;
4633
+ joinedAt: string;
4634
+ point: number;
4635
+ marketingAgreed: boolean;
4636
+ loginMethods: "password"[];
4637
+ addresses: {
4638
+ addressId: string;
4639
+ alias: string;
4640
+ isDefault: boolean;
4641
+ receiverName: string;
4642
+ phone: string;
4643
+ zipCode: string;
4644
+ address1: string;
4645
+ address2: string | null;
4646
+ }[];
4647
+ stats: {
4648
+ orderCount: number;
4649
+ totalPaid: number;
4650
+ lastOrderedAt: string | null;
4651
+ canceledOrReturnedCount: number;
4652
+ reviewCount: number;
4653
+ inquiryCount: number;
4654
+ };
4655
+ }>;
4656
+ orders: (customerId: string, params?: PageParams) => Promise<{
4657
+ page: number;
4658
+ size: number;
4659
+ totalElements: number;
4660
+ totalPages: number;
4661
+ contents: {
4662
+ orderId: string;
4663
+ orderedAt: string;
4664
+ totalAmount: number;
4665
+ testPayment: boolean;
4666
+ items: {
4667
+ orderItemId: string;
4668
+ productName: string;
4669
+ optionName: string | null;
4670
+ quantity: number;
4671
+ status: "PAID" | "PAYMENT_WAITING" | "CONFIRMED" | "DISPATCHED" | "DELIVERING" | "DELIVERED" | "PURCHASE_DECIDED" | "CANCELED";
4672
+ claimStatus: "CANCEL_REQUESTED" | "RETURN_REQUESTED" | "EXCHANGE_REQUESTED" | null;
4673
+ }[];
4674
+ }[];
4675
+ }>;
4676
+ withdraw: (customerId: string, body: WithdrawCustomerRequest) => Promise<void>;
4677
+ };
4451
4678
  customerInquiries: {
4452
4679
  list: (params?: PageParams & {
4453
4680
  answered?: boolean;
@@ -5706,6 +5933,26 @@ export declare function createStoreClient(options: StoreClientOptions): {
5706
5933
  logoUrl: string | null;
5707
5934
  businessNumber: string | null;
5708
5935
  customerCenterPhone: string | null;
5936
+ analyticsAllowedOrigins: string[];
5937
+ guestCheckoutEnabled: boolean;
5938
+ memberCount: number;
5939
+ createdAt: string;
5940
+ }>;
5941
+ /**
5942
+ * `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
5943
+ * 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
5944
+ */
5945
+ update: (body: UpdateStoreRequest) => Promise<{
5946
+ storeId: string;
5947
+ name: string;
5948
+ storeCode: string;
5949
+ storefrontUrl: string;
5950
+ status: "ACTIVE" | "SUSPENDED" | "CLOSED";
5951
+ logoUrl: string | null;
5952
+ businessNumber: string | null;
5953
+ customerCenterPhone: string | null;
5954
+ analyticsAllowedOrigins: string[];
5955
+ guestCheckoutEnabled: boolean;
5709
5956
  memberCount: number;
5710
5957
  createdAt: string;
5711
5958
  }>;
@@ -5815,6 +6062,7 @@ export declare const STORE_SCOPE_RESOURCES: {
5815
6062
  readonly webhook: readonly ["r", "rw"];
5816
6063
  readonly store: readonly ["r", "rw"];
5817
6064
  readonly member: readonly ["r", "rw"];
6065
+ readonly customer: readonly ["r", "rw"];
5818
6066
  };
5819
6067
  export type StoreScopeResource = keyof typeof STORE_SCOPE_RESOURCES;
5820
6068
  export declare const storeScopeResourceSchema: z.ZodEnum<{
@@ -5829,6 +6077,7 @@ export declare const storeScopeResourceSchema: z.ZodEnum<{
5829
6077
  webhook: "webhook";
5830
6078
  store: "store";
5831
6079
  member: "member";
6080
+ customer: "customer";
5832
6081
  }>;
5833
6082
  /** 카탈로그상 존재하는 스코프 문자열인가 (예: `settlement:rw`는 false) */
5834
6083
  export declare function isStoreScope(scope: string): boolean;
@@ -5838,9 +6087,9 @@ export declare function isStoreScope(scope: string): boolean;
5838
6087
  * 발급 시 지정한 스코프만 갖는다(발급자 보유 스코프 이내).
5839
6088
  */
5840
6089
  export declare const STORE_ROLE_SCOPES: {
5841
- readonly OWNER: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "settlement:r", "analytics:r", "webhook:rw", "store:rw", "member:rw"];
5842
- readonly ADMIN: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "analytics:r", "webhook:rw", "store:rw", "member:rw"];
5843
- readonly STAFF: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw"];
6090
+ readonly OWNER: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "settlement:r", "analytics:r", "webhook:rw", "store:rw", "member:rw", "customer:rw"];
6091
+ readonly ADMIN: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "analytics:r", "webhook:rw", "store:rw", "member:rw", "customer:rw"];
6092
+ readonly STAFF: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "customer:r"];
5844
6093
  };
5845
6094
  //#endregion
5846
6095
  //#region src/schemas/webhook-event.d.ts
package/dist/index.mjs CHANGED
@@ -698,6 +698,103 @@ const approveClaimResultSchema = z.object({
698
698
  refundedAt: z.string()
699
699
  });
700
700
  //#endregion
701
+ //#region src/schemas/customer.ts
702
+ /** 구매자 목록 정렬 */
703
+ const customerSortSchema = z.enum([
704
+ "joined_desc",
705
+ "joined_asc",
706
+ "total_paid_desc",
707
+ "order_count_desc",
708
+ "last_ordered_desc"
709
+ ]).describe("정렬. `joined_desc`(기본, 최근 가입순)·`joined_asc`·`total_paid_desc`(구매 금액순)·`order_count_desc`(주문 수순)·`last_ordered_desc`(최근 주문순)");
710
+ /** `GET /v1/customers` 쿼리 */
711
+ const customerListQuerySchema = z.object({
712
+ q: z.string().optional().describe("검색어 — 이름·이메일(포함). 숫자 4자리면 휴대폰 뒤 4자리로도 찾는다"),
713
+ joinedFrom: z.string().optional().describe("가입일 시작(YYYY-MM-DD, KST, 포함)"),
714
+ joinedTo: z.string().optional().describe("가입일 끝(YYYY-MM-DD, KST, 포함)"),
715
+ marketing: z.enum(["agreed", "declined"]).optional().describe("마케팅 수신 동의 필터. 생략하면 전체"),
716
+ sort: customerSortSchema.optional(),
717
+ ...pageQueryShape()
718
+ });
719
+ /** 구매 통계 — 구매 금액은 취소된 주문 상품을 빼고, 완료된 반품은 수량 비율로 뺀 금액이다 */
720
+ const customerStatsSchema = z.object({
721
+ orderCount: z.number().int().describe("주문 수(취소 포함)"),
722
+ totalPaid: z.number().int().describe("구매 금액(원) — 취소·완료된 반품 제외"),
723
+ lastOrderedAt: z.string().nullable().describe("최근 주문 시각")
724
+ });
725
+ /** 목록 행 — 휴대폰은 가운데를 가린다(`010-****-1234`) */
726
+ const customerSummarySchema = z.object({
727
+ customerId: z.string(),
728
+ name: z.string(),
729
+ email: z.string(),
730
+ maskedPhone: z.string().nullable(),
731
+ joinedAt: z.string(),
732
+ point: z.number().int(),
733
+ marketingAgreed: z.boolean()
734
+ }).extend(customerStatsSchema.shape);
735
+ const customerAddressSchema = z.object({
736
+ addressId: z.string(),
737
+ alias: z.string(),
738
+ isDefault: z.boolean(),
739
+ receiverName: z.string(),
740
+ phone: z.string(),
741
+ zipCode: z.string(),
742
+ address1: z.string(),
743
+ address2: z.string().nullable()
744
+ });
745
+ /** 구매자 상세 — 휴대폰 전체를 보여 준다 */
746
+ const customerDetailSchema = z.object({
747
+ customerId: z.string(),
748
+ name: z.string(),
749
+ email: z.string(),
750
+ phone: z.string().nullable(),
751
+ joinedAt: z.string(),
752
+ point: z.number().int(),
753
+ marketingAgreed: z.boolean(),
754
+ loginMethods: z.array(z.enum(["password"])).describe("로그인 방법. 지금은 이메일·비밀번호(`password`)뿐이다"),
755
+ addresses: z.array(customerAddressSchema),
756
+ stats: customerStatsSchema.extend({
757
+ canceledOrReturnedCount: z.number().int().describe("취소된 주문 상품 + 완료된 반품 수"),
758
+ reviewCount: z.number().int(),
759
+ inquiryCount: z.number().int().describe("상품 문의 + 1:1 문의")
760
+ })
761
+ });
762
+ /** 구매자의 주문 — 전체 기간 */
763
+ const customerOrderSchema = z.object({
764
+ orderId: z.string(),
765
+ orderedAt: z.string(),
766
+ totalAmount: z.number().int().describe("결제 금액"),
767
+ testPayment: z.boolean(),
768
+ items: z.array(z.object({
769
+ orderItemId: z.string(),
770
+ productName: z.string(),
771
+ optionName: z.string().nullable(),
772
+ quantity: z.number().int(),
773
+ status: orderItemStatusSchema,
774
+ claimStatus: z.enum([
775
+ "CANCEL_REQUESTED",
776
+ "RETURN_REQUESTED",
777
+ "EXCHANGE_REQUESTED"
778
+ ]).nullable()
779
+ }))
780
+ });
781
+ /** `POST /v1/customers/{id}/withdraw` 본문 */
782
+ const withdrawCustomerRequestSchema = z.object({
783
+ reason: z.enum([
784
+ "BUYER_REQUEST",
785
+ "DORMANT",
786
+ "ABUSE",
787
+ "OTHER"
788
+ ]).describe("처리 사유 — 구매자 요청·장기 미이용·부정 이용·기타. 감사 로그에 남는다"),
789
+ memo: z.string().max(200).optional().describe("사유 보충(선택)")
790
+ });
791
+ /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프런트 탈퇴와 같다) */
792
+ const customerWithdrawalBlockedDetailsSchema = z.object({
793
+ inProgressOrderItems: z.number().int(),
794
+ openClaims: z.number().int(),
795
+ pendingPayments: z.number().int()
796
+ });
797
+ //#endregion
701
798
  //#region src/schemas/misc.ts
702
799
  const inquirySchema = z.object({
703
800
  inquiryId: z.string(),
@@ -1710,6 +1807,28 @@ function isReservedStoreCode(code) {
1710
1807
  return RESERVED_STORE_CODES.includes(code.toLowerCase());
1711
1808
  }
1712
1809
  const storeCodeSchema = z.string().regex(STORE_CODE_PATTERN, "소문자·숫자·하이픈 3~30자여야 하고 하이픈으로 시작하거나 끝날 수 없어요").refine((code) => !isReservedStoreCode(code), "플랫폼이 쓰는 이름이라 스토어 코드로 쓸 수 없어요");
1810
+ /** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
1811
+ function isWebOrigin(value) {
1812
+ try {
1813
+ const url = new URL(value);
1814
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash && !value.includes("*");
1815
+ } catch {
1816
+ return false;
1817
+ }
1818
+ }
1819
+ /** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
1820
+ const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요").describe("스토어프런트 오리진. 예: `https://shop.example.com`");
1821
+ /** 허용 오리진 상한 */
1822
+ const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
1823
+ /** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
1824
+ const updateStoreRequestSchema = z.object({
1825
+ name: z.string().min(2).max(50).optional(),
1826
+ logoUrl: z.url().optional(),
1827
+ customerCenterPhone: z.string().optional(),
1828
+ businessHours: z.string().max(100).optional(),
1829
+ analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("방문 분석 수집을 받을 스토어프런트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다"),
1830
+ guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다")
1831
+ }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
1713
1832
  const storeSchema = z.object({
1714
1833
  storeId: z.string(),
1715
1834
  name: z.string(),
@@ -1723,6 +1842,8 @@ const storeSchema = z.object({
1723
1842
  logoUrl: z.url().nullable(),
1724
1843
  businessNumber: z.string().nullable(),
1725
1844
  customerCenterPhone: z.string().nullable(),
1845
+ analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프런트 오리진. 비어 있으면 모든 오리진에서 받는다"),
1846
+ guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
1726
1847
  memberCount: z.number().int(),
1727
1848
  createdAt: z.string()
1728
1849
  });
@@ -2030,6 +2151,13 @@ function createStoreClient(options) {
2030
2151
  answer: (inquiryId, content) => storeHttp.requestVoid("POST", path`/inquiries/${inquiryId}/answer`, { body: { content } }),
2031
2152
  updateAnswer: (inquiryId, content) => storeHttp.requestVoid("PUT", path`/inquiries/${inquiryId}/answer`, { body: { content } })
2032
2153
  },
2154
+ /** 구매자 관리 — 조회 `customer:r`, 탈퇴 처리 `customer:rw`(콘솔 OWNER·ADMIN) */
2155
+ customers: {
2156
+ list: (params) => storeHttp.request("GET", "/customers", pageSchema(customerSummarySchema), { query: params }),
2157
+ get: (customerId) => storeHttp.request("GET", path`/customers/${customerId}`, customerDetailSchema),
2158
+ orders: (customerId, params) => storeHttp.request("GET", path`/customers/${customerId}/orders`, pageSchema(customerOrderSchema), { query: params }),
2159
+ withdraw: (customerId, body) => storeHttp.requestVoid("POST", path`/customers/${customerId}/withdraw`, { body: withdrawCustomerRequestSchema.parse(body) })
2160
+ },
2033
2161
  customerInquiries: {
2034
2162
  list: (params) => storeHttp.request("GET", "/customer-inquiries", pageSchema(sellerCustomerInquirySchema), { query: params }),
2035
2163
  get: (inquiryId) => storeHttp.request("GET", path`/customer-inquiries/${inquiryId}`, sellerCustomerInquirySchema),
@@ -2212,6 +2340,11 @@ get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, {
2212
2340
  },
2213
2341
  store: {
2214
2342
  get: () => storeHttp.request("GET", "/store", storeSchema),
2343
+ /**
2344
+ * `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
2345
+ * 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
2346
+ */
2347
+ update: (body) => storeHttp.request("PATCH", "/store", storeSchema, { body: updateStoreRequestSchema.parse(body) }),
2215
2348
  listMembers: () => storeHttp.request("GET", "/store/members", z.array(storeMemberSchema)),
2216
2349
  listInvitations: () => storeHttp.request("GET", "/store/invitations", z.array(invitationSchema))
2217
2350
  }
@@ -2317,7 +2450,8 @@ const STORE_SCOPE_RESOURCES = {
2317
2450
  analytics: ["r"],
2318
2451
  webhook: ["r", "rw"],
2319
2452
  store: ["r", "rw"],
2320
- member: ["r", "rw"]
2453
+ member: ["r", "rw"],
2454
+ customer: ["r", "rw"]
2321
2455
  };
2322
2456
  const storeScopeResourceSchema = z.enum(Object.keys(STORE_SCOPE_RESOURCES)).describe("스코프 리소스");
2323
2457
  /** 카탈로그상 존재하는 스코프 문자열인가 (예: `settlement:rw`는 false) */
@@ -2343,7 +2477,8 @@ const STORE_ROLE_SCOPES = {
2343
2477
  "analytics:r",
2344
2478
  "webhook:rw",
2345
2479
  "store:rw",
2346
- "member:rw"
2480
+ "member:rw",
2481
+ "customer:rw"
2347
2482
  ],
2348
2483
  ADMIN: [
2349
2484
  "product:rw",
@@ -2355,7 +2490,8 @@ const STORE_ROLE_SCOPES = {
2355
2490
  "analytics:r",
2356
2491
  "webhook:rw",
2357
2492
  "store:rw",
2358
- "member:rw"
2493
+ "member:rw",
2494
+ "customer:rw"
2359
2495
  ],
2360
2496
  STAFF: [
2361
2497
  "product:rw",
@@ -2363,7 +2499,8 @@ const STORE_ROLE_SCOPES = {
2363
2499
  "order:rw",
2364
2500
  "claim:rw",
2365
2501
  "inquiry:rw",
2366
- "review:rw"
2502
+ "review:rw",
2503
+ "customer:r"
2367
2504
  ]
2368
2505
  };
2369
2506
  //#endregion
@@ -2594,4 +2731,4 @@ function createStoreSession(options) {
2594
2731
  };
2595
2732
  }
2596
2733
  //#endregion
2597
- export { ApiError, 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, analyticsFunnelStepSchema, analyticsOverviewSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, 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, customerInquiryCategorySchema, customerInquiryListQuerySchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, effectivePaymentRoutingModeSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, 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, paymentRoutingModeSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageUploadResultSchema, 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, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema };
2734
+ 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, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, 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, effectivePaymentRoutingModeSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, 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, paymentRoutingModeSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageUploadResultSchema, 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, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -52,6 +52,14 @@ import {
52
52
  rejectClaimRequestSchema,
53
53
  } from "./schemas/claim";
54
54
  import { bulkResultSchema, type PageParams, pageSchema } from "./schemas/common";
55
+ import {
56
+ customerDetailSchema,
57
+ type customerListQuerySchema,
58
+ customerOrderSchema,
59
+ customerSummarySchema,
60
+ type WithdrawCustomerRequest,
61
+ withdrawCustomerRequestSchema,
62
+ } from "./schemas/customer";
55
63
  import {
56
64
  type CustomerInquiryCategory,
57
65
  inquirySchema,
@@ -144,6 +152,8 @@ import {
144
152
  sellerAddressSchema,
145
153
  storeMemberSchema,
146
154
  storeSchema,
155
+ type UpdateStoreRequest,
156
+ updateStoreRequestSchema,
147
157
  } from "./schemas/store";
148
158
  import { type UploadRequest, uploadRequestSchema, uploadTicketSchema } from "./schemas/upload";
149
159
  import type { AuthorizeOptions, StoreSession } from "./session";
@@ -557,6 +567,31 @@ export function createStoreClient(options: StoreClientOptions) {
557
567
  storeHttp.requestVoid("PUT", path`/inquiries/${inquiryId}/answer`, { body: { content } }),
558
568
  },
559
569
 
570
+ /** 구매자 관리 — 조회 `customer:r`, 탈퇴 처리 `customer:rw`(콘솔 OWNER·ADMIN) */
571
+ customers: {
572
+ list: (
573
+ params?: PageParams & Omit<z.input<typeof customerListQuerySchema>, "page" | "size">,
574
+ ) =>
575
+ storeHttp.request("GET", "/customers", pageSchema(customerSummarySchema), {
576
+ query: params as Query,
577
+ }),
578
+ get: (customerId: string) =>
579
+ storeHttp.request("GET", path`/customers/${customerId}`, customerDetailSchema),
580
+ orders: (customerId: string, params?: PageParams) =>
581
+ storeHttp.request(
582
+ "GET",
583
+ path`/customers/${customerId}/orders`,
584
+ pageSchema(customerOrderSchema),
585
+ {
586
+ query: params as Query,
587
+ },
588
+ ),
589
+ withdraw: (customerId: string, body: WithdrawCustomerRequest) =>
590
+ storeHttp.requestVoid("POST", path`/customers/${customerId}/withdraw`, {
591
+ body: withdrawCustomerRequestSchema.parse(body),
592
+ }),
593
+ },
594
+
560
595
  customerInquiries: {
561
596
  list: (
562
597
  params?: PageParams & {
@@ -869,6 +904,14 @@ export function createStoreClient(options: StoreClientOptions) {
869
904
 
870
905
  store: {
871
906
  get: () => storeHttp.request("GET", "/store", storeSchema),
907
+ /**
908
+ * `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
909
+ * 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
910
+ */
911
+ update: (body: UpdateStoreRequest) =>
912
+ storeHttp.request("PATCH", "/store", storeSchema, {
913
+ body: updateStoreRequestSchema.parse(body),
914
+ }),
872
915
  listMembers: () => storeHttp.request("GET", "/store/members", z.array(storeMemberSchema)),
873
916
  listInvitations: () =>
874
917
  storeHttp.request("GET", "/store/invitations", z.array(invitationSchema)),
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./schemas/auth";
18
18
  export * from "./schemas/category";
19
19
  export * from "./schemas/claim";
20
20
  export * from "./schemas/common";
21
+ export * from "./schemas/customer";
21
22
  export * from "./schemas/misc";
22
23
  export * from "./schemas/order";
23
24
  export * from "./schemas/payment";
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ import { orderItemStatusSchema, pageQueryShape } from "./common";
3
+
4
+ /** 구매자 목록 정렬 */
5
+ export const customerSortSchema = z
6
+ .enum(["joined_desc", "joined_asc", "total_paid_desc", "order_count_desc", "last_ordered_desc"])
7
+ .describe(
8
+ "정렬. `joined_desc`(기본, 최근 가입순)·`joined_asc`·`total_paid_desc`(구매 금액순)·`order_count_desc`(주문 수순)·`last_ordered_desc`(최근 주문순)",
9
+ );
10
+ export type CustomerSort = z.infer<typeof customerSortSchema>;
11
+
12
+ /** `GET /v1/customers` 쿼리 */
13
+ export const customerListQuerySchema = z.object({
14
+ q: z
15
+ .string()
16
+ .optional()
17
+ .describe("검색어 — 이름·이메일(포함). 숫자 4자리면 휴대폰 뒤 4자리로도 찾는다"),
18
+ joinedFrom: z.string().optional().describe("가입일 시작(YYYY-MM-DD, KST, 포함)"),
19
+ joinedTo: z.string().optional().describe("가입일 끝(YYYY-MM-DD, KST, 포함)"),
20
+ marketing: z
21
+ .enum(["agreed", "declined"])
22
+ .optional()
23
+ .describe("마케팅 수신 동의 필터. 생략하면 전체"),
24
+ sort: customerSortSchema.optional(),
25
+ ...pageQueryShape(),
26
+ });
27
+
28
+ /** 구매 통계 — 구매 금액은 취소된 주문 상품을 빼고, 완료된 반품은 수량 비율로 뺀 금액이다 */
29
+ export const customerStatsSchema = z.object({
30
+ orderCount: z.number().int().describe("주문 수(취소 포함)"),
31
+ totalPaid: z.number().int().describe("구매 금액(원) — 취소·완료된 반품 제외"),
32
+ lastOrderedAt: z.string().nullable().describe("최근 주문 시각"),
33
+ });
34
+
35
+ /** 목록 행 — 휴대폰은 가운데를 가린다(`010-****-1234`) */
36
+ export const customerSummarySchema = z
37
+ .object({
38
+ customerId: z.string(),
39
+ name: z.string(),
40
+ email: z.string(),
41
+ maskedPhone: z.string().nullable(),
42
+ joinedAt: z.string(),
43
+ point: z.number().int(),
44
+ marketingAgreed: z.boolean(),
45
+ })
46
+ .extend(customerStatsSchema.shape);
47
+ export type CustomerSummary = z.infer<typeof customerSummarySchema>;
48
+
49
+ export const customerAddressSchema = z.object({
50
+ addressId: z.string(),
51
+ alias: z.string(),
52
+ isDefault: z.boolean(),
53
+ receiverName: z.string(),
54
+ phone: z.string(),
55
+ zipCode: z.string(),
56
+ address1: z.string(),
57
+ address2: z.string().nullable(),
58
+ });
59
+
60
+ /** 구매자 상세 — 휴대폰 전체를 보여 준다 */
61
+ export const customerDetailSchema = z.object({
62
+ customerId: z.string(),
63
+ name: z.string(),
64
+ email: z.string(),
65
+ phone: z.string().nullable(),
66
+ joinedAt: z.string(),
67
+ point: z.number().int(),
68
+ marketingAgreed: z.boolean(),
69
+ loginMethods: z
70
+ .array(z.enum(["password"]))
71
+ .describe("로그인 방법. 지금은 이메일·비밀번호(`password`)뿐이다"),
72
+ addresses: z.array(customerAddressSchema),
73
+ stats: customerStatsSchema.extend({
74
+ canceledOrReturnedCount: z.number().int().describe("취소된 주문 상품 + 완료된 반품 수"),
75
+ reviewCount: z.number().int(),
76
+ inquiryCount: z.number().int().describe("상품 문의 + 1:1 문의"),
77
+ }),
78
+ });
79
+ export type CustomerDetail = z.infer<typeof customerDetailSchema>;
80
+
81
+ /** 구매자의 주문 — 전체 기간 */
82
+ export const customerOrderSchema = z.object({
83
+ orderId: z.string(),
84
+ orderedAt: z.string(),
85
+ totalAmount: z.number().int().describe("결제 금액"),
86
+ testPayment: z.boolean(),
87
+ items: z.array(
88
+ z.object({
89
+ orderItemId: z.string(),
90
+ productName: z.string(),
91
+ optionName: z.string().nullable(),
92
+ quantity: z.number().int(),
93
+ status: orderItemStatusSchema,
94
+ claimStatus: z
95
+ .enum(["CANCEL_REQUESTED", "RETURN_REQUESTED", "EXCHANGE_REQUESTED"])
96
+ .nullable(),
97
+ }),
98
+ ),
99
+ });
100
+ export type CustomerOrder = z.infer<typeof customerOrderSchema>;
101
+
102
+ /** `POST /v1/customers/{id}/withdraw` 본문 */
103
+ export const withdrawCustomerRequestSchema = z.object({
104
+ reason: z
105
+ .enum(["BUYER_REQUEST", "DORMANT", "ABUSE", "OTHER"])
106
+ .describe("처리 사유 — 구매자 요청·장기 미이용·부정 이용·기타. 감사 로그에 남는다"),
107
+ memo: z.string().max(200).optional().describe("사유 보충(선택)"),
108
+ });
109
+ export type WithdrawCustomerRequest = z.infer<typeof withdrawCustomerRequestSchema>;
110
+
111
+ /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프런트 탈퇴와 같다) */
112
+ export const customerWithdrawalBlockedDetailsSchema = z.object({
113
+ inProgressOrderItems: z.number().int(),
114
+ openClaims: z.number().int(),
115
+ pendingPayments: z.number().int(),
116
+ });
117
+ export type CustomerWithdrawalBlockedDetails = z.infer<
118
+ typeof customerWithdrawalBlockedDetailsSchema
119
+ >;
@@ -27,6 +27,7 @@ export const STORE_SCOPE_RESOURCES = {
27
27
  webhook: ["r", "rw"],
28
28
  store: ["r", "rw"],
29
29
  member: ["r", "rw"],
30
+ customer: ["r", "rw"],
30
31
  } as const satisfies Record<string, readonly ScopeAccess[]>;
31
32
 
32
33
  export type StoreScopeResource = keyof typeof STORE_SCOPE_RESOURCES;
@@ -61,6 +62,7 @@ export const STORE_ROLE_SCOPES = {
61
62
  "webhook:rw",
62
63
  "store:rw",
63
64
  "member:rw",
65
+ "customer:rw",
64
66
  ],
65
67
  ADMIN: [
66
68
  "product:rw",
@@ -73,6 +75,15 @@ export const STORE_ROLE_SCOPES = {
73
75
  "webhook:rw",
74
76
  "store:rw",
75
77
  "member:rw",
78
+ "customer:rw",
79
+ ],
80
+ STAFF: [
81
+ "product:rw",
82
+ "category:rw",
83
+ "order:rw",
84
+ "claim:rw",
85
+ "inquiry:rw",
86
+ "review:rw",
87
+ "customer:r",
76
88
  ],
77
- STAFF: ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw"],
78
89
  } as const satisfies Record<StoreRole, readonly string[]>;
@@ -73,6 +73,63 @@ export const storeCodeSchema = z
73
73
  "플랫폼이 쓰는 이름이라 스토어 코드로 쓸 수 없어요",
74
74
  );
75
75
 
76
+ /** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
77
+ export function isWebOrigin(value: string): boolean {
78
+ try {
79
+ const url = new URL(value);
80
+ return (
81
+ (url.protocol === "https:" || url.protocol === "http:") &&
82
+ !url.username &&
83
+ !url.password &&
84
+ url.pathname === "/" &&
85
+ !url.search &&
86
+ !url.hash &&
87
+ !value.includes("*")
88
+ );
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
95
+ export const analyticsOriginSchema = z
96
+ .string()
97
+ .trim()
98
+ .max(255)
99
+ .refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요")
100
+ .describe("스토어프런트 오리진. 예: `https://shop.example.com`");
101
+
102
+ /** 허용 오리진 상한 */
103
+ export const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
104
+
105
+ /** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
106
+ export const updateStoreRequestSchema = z
107
+ .object({
108
+ name: z.string().min(2).max(50).optional(),
109
+ logoUrl: z.url().optional(),
110
+ customerCenterPhone: z.string().optional(),
111
+ businessHours: z.string().max(100).optional(),
112
+ analyticsAllowedOrigins: z
113
+ .array(analyticsOriginSchema)
114
+ .max(MAX_ANALYTICS_ALLOWED_ORIGINS)
115
+ .optional()
116
+ .describe(
117
+ "방문 분석 수집을 받을 스토어프런트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. " +
118
+ "넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다",
119
+ ),
120
+ guestCheckoutEnabled: z
121
+ .boolean()
122
+ .optional()
123
+ .describe(
124
+ "비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. " +
125
+ "장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다",
126
+ ),
127
+ })
128
+ .refine((value) => Object.values(value).some((v) => v !== undefined), {
129
+ message: "최소 1개 필드가 필요합니다",
130
+ });
131
+ export type UpdateStoreRequest = z.infer<typeof updateStoreRequestSchema>;
132
+
76
133
  export const storeSchema = z.object({
77
134
  storeId: z.string(),
78
135
  name: z.string(),
@@ -82,6 +139,10 @@ export const storeSchema = z.object({
82
139
  logoUrl: z.url().nullable(),
83
140
  businessNumber: z.string().nullable(),
84
141
  customerCenterPhone: z.string().nullable(),
142
+ analyticsAllowedOrigins: z
143
+ .array(z.string())
144
+ .describe("방문 분석 수집을 받는 스토어프런트 오리진. 비어 있으면 모든 오리진에서 받는다"),
145
+ guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
85
146
  memberCount: z.number().int(),
86
147
  createdAt: z.string(),
87
148
  });