@sayren/storefront-sdk 0.15.0 → 0.17.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.
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { easyPayProviderResponseSchema, paymentMethodSchema } from "./checkout";
3
3
  import { orderItemStatusSchema } from "./common";
4
4
  import { myOrderItemFulfillmentSnapshotSchema } from "./fulfillment";
5
+ import { lineSelectionResponseShape } from "./product-options";
5
6
 
6
7
  export const myOrderItemSchema = z.object({
7
8
  orderItemId: z.string(),
@@ -9,6 +10,7 @@ export const myOrderItemSchema = z.object({
9
10
  productName: z.string(),
10
11
  thumbnailUrl: z.url().nullable().describe("주문 시점의 대표 이미지. 없었으면 null"),
11
12
  optionName: z.string().nullable(),
13
+ ...lineSelectionResponseShape,
12
14
  quantity: z.number().int().describe("주문 수량"),
13
15
  // 두 수량은 서버가 언제나 함께 싣는다. `store-sdk`의 `orderItemSchema`와 같은 규칙으로 **둘 다 필수**다 —
14
16
  // 응답 스키마는 OpenAPI 계약이기도 해서 기본값을 두면 선택 필드로 문서화되고, 필드가 빠진 응답이
@@ -32,57 +34,24 @@ export const myOrderItemSchema = z.object({
32
34
  ),
33
35
  status: orderItemStatusSchema,
34
36
  fulfillmentSnapshot: myOrderItemFulfillmentSnapshotSchema.describe(
35
- "주문 시점 이행 스냅샷(이슈 #57). `type`이 주문 시점 상품 유형이다. 배송 상품이 아니면 `DELIVERED`는 제공 완료(방문 수령은 수령 완료)다. " +
36
- "청약철회 제한 주문상품(`download`·`code`의 `withdrawalRestricted`)은 제공이 시작되면(다운로드·열람) 구매자 취소·반품을 받지 않는다",
37
+ "주문 시점 이행 스냅샷(이슈 #57). `type`이 주문 시점 상품 유형이다. 배송 없는 상품(`MANUAL`)이면 `DELIVERED`는 제공 완료다",
37
38
  ),
38
39
  fulfillment: z
39
40
  .object({
40
41
  status: z
41
42
  .string()
42
43
  .describe(
43
- "제공 상태. `FULFILLED`(제공함)·`REVOKED`(환불 등으로 회수함). 값이 추가될 수 있어 문자열로 받는다",
44
+ "제공 상태. `FULFILLED`(제공함)·`REVOKED`(전량 취소·환불로 회수함). 값이 추가될 수 있어 문자열로 받는다",
44
45
  ),
45
46
  fulfilledAt: z.string().describe("제공 시각 (ISO 8601)"),
46
- note: z.string().nullable().describe("셀러가 남긴 이용 안내. 없으면 null"),
47
- codes: z
48
- .array(
49
- z.object({
50
- codeId: z.string(),
51
- hint: z.string().describe("가린 코드 — 끝 4자만 보인다(예: `****AB12`)"),
52
- revealedAt: z.string().nullable().describe("처음 열람한 시각. 열람 전이면 null"),
53
- }),
54
- )
55
- .optional()
56
- .default([])
57
- .describe(
58
- "코드 발급 상품의 코드(가린 값). 평문은 `fulfillment/reveal`로 연다. 환불로 회수된 코드는 싣지 않는다. 서버는 항상 싣는다",
59
- ),
60
- download: z
61
- .object({
62
- count: z.number().int().describe("지금까지 받은 횟수"),
63
- limit: z.number().int().describe("받을 수 있는 횟수"),
64
- expiresAt: z.string().nullable().describe("받을 수 있는 기한 (ISO 8601)"),
65
- files: z.array(
66
- z.object({
67
- assetId: z.string(),
68
- fileName: z.string(),
69
- sizeBytes: z.number().int().nullable(),
70
- }),
71
- ),
72
- })
73
- .nullable()
74
- .optional()
75
- .default(null)
76
- .describe(
77
- "파일 다운로드 상품의 횟수·기한·파일 목록. 파일 주소는 `downloads`로 받는다. 다른 유형은 null. 서버는 항상 싣는다",
78
- ),
47
+ note: z.string().nullable().describe("판매자가 제공 처리 때 남긴 이용 안내. 없으면 null"),
79
48
  })
80
49
  .nullable()
81
50
  .optional()
82
51
  .default(null)
83
52
  .describe(
84
- "비실물 상품(이용권·방문 수령 등)의 제공 정보(이슈 #48). 제공 전이거나 배송 상품이면 null이다. " +
85
- "비실물의 `DELIVERED`는 제공 완료(방문 수령은 수령 완료)로 표시한다. 서버는 항상 싣는다",
53
+ "배송 없는 상품의 제공 기록(이슈 #48). 제공 전이거나 배송 상품이면 null이다. " +
54
+ "배송 없는 상품의 `DELIVERED`는 제공 완료로 표시한다. 서버는 항상 싣는다",
86
55
  ),
87
56
  claimStatus: z.string().nullable(),
88
57
  reviewWritten: z.boolean(),
@@ -165,14 +134,6 @@ export const myOrderSchema = z.object({
165
134
  paidAt: z.string().nullable(),
166
135
  receiptUrl: z.url().nullable(),
167
136
  }),
168
- withdrawalAgreedAt: z
169
- .string()
170
- .nullable()
171
- .optional()
172
- .default(null)
173
- .describe(
174
- "청약철회 제한 안내에 동의한 시각(이슈 #48). 제한 상품이 없던 주문은 null. 서버는 항상 싣는다",
175
- ),
176
137
  testPayment: z
177
138
  .boolean()
178
139
  .describe(
@@ -239,43 +200,3 @@ export const purchaseDecisionResultSchema = z.object({
239
200
  });
240
201
 
241
202
  export type PurchaseDecisionResult = z.infer<typeof purchaseDecisionResultSchema>;
242
-
243
- /**
244
- * 코드 열람 응답 (이슈 #48) — `POST /storefront/v1/me/order-items/{orderItemId}/fulfillment/reveal`(회원)·
245
- * `POST /storefront/v1/guest/orders/{orderId}/order-items/{orderItemId}/fulfillment/reveal`(비회원).
246
- * 평문 코드는 이 응답에만 있다. 화면에 보인 뒤 저장하지 않는다(응답은 `Cache-Control: no-store`).
247
- * 첫 열람 시각은 청약철회 판단의 근거(제공 개시)로 남는다.
248
- */
249
- export const fulfillmentRevealSchema = z.object({
250
- orderItemId: z.string(),
251
- codes: z.array(
252
- z.object({
253
- codeId: z.string(),
254
- code: z.string().describe("코드 평문"),
255
- revealedAt: z.string().describe("처음 열람한 시각"),
256
- }),
257
- ),
258
- });
259
-
260
- export type FulfillmentReveal = z.infer<typeof fulfillmentRevealSchema>;
261
-
262
- /**
263
- * 다운로드 요청 (이슈 #48) — `POST /storefront/v1/me/order-items/{orderItemId}/downloads`(회원)·
264
- * `POST /storefront/v1/guest/orders/{orderId}/order-items/{orderItemId}/downloads`(비회원, 연락처·비밀번호를 함께 보낸다).
265
- * 파일이 하나면 `assetId`를 생략할 수 있다.
266
- */
267
- export const downloadRequestSchema = z.object({
268
- assetId: z.string().min(1).max(64).optional().describe("받을 파일. 파일이 여러 개면 필수다"),
269
- });
270
-
271
- export type DownloadRequest = z.infer<typeof downloadRequestSchema>;
272
-
273
- /** 다운로드 응답 — 수명 300초의 서명 주소. 받을 때마다 횟수가 하나 준다 */
274
- export const downloadLinkSchema = z.object({
275
- url: z.url().describe("서명 주소. 300초 뒤 만료된다 — 받아서 바로 쓰고 저장하지 않는다"),
276
- expiresAt: z.string().describe("서명 주소 만료 시각"),
277
- fileName: z.string(),
278
- remaining: z.number().int().describe("남은 다운로드 횟수"),
279
- });
280
-
281
- export type DownloadLink = z.infer<typeof downloadLinkSchema>;
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * 추가 선택·직접 입력 (이슈 #59) — 상품 상세의 옵션 표시, 장바구니 담기·바로 구매 요청, 줄 스냅샷의 공통 모양.
5
+ *
6
+ * 추가 선택은 재고가 없는 옵션이다. 구매자가 조합(옵션이 없으면 상품)을 고른 뒤 옵션명마다 값 하나를 더 고르고, 값의 추가금이
7
+ * 단가에 더해진다. 직접 입력은 구매자가 글자를 적는 항목(각인 문구 등)이다. 추가 선택 값이나 직접 입력값이 다르면 장바구니에서 다른 줄이다.
8
+ */
9
+
10
+ export const addonGroupViewSchema = z.object({
11
+ groupId: z.string(),
12
+ name: z.string(),
13
+ required: z.boolean().describe("꼭 골라야 하는가. false면 고르지 않아도 된다(「선택 안 함」)"),
14
+ values: z
15
+ .array(
16
+ z.object({
17
+ valueId: z.string(),
18
+ name: z.string(),
19
+ additionalPrice: z.number().int().describe("이 값을 고르면 단가에 더하는 금액(원, 0 이상)"),
20
+ }),
21
+ )
22
+ .describe("고를 수 있는 값(셀러가 끈 값은 빠진다)"),
23
+ });
24
+
25
+ export type AddonGroupView = z.infer<typeof addonGroupViewSchema>;
26
+
27
+ export const customInputViewSchema = z.object({
28
+ inputId: z.string(),
29
+ label: z.string().describe("항목명"),
30
+ placeholder: z.string().nullable().describe("입력 안내. 없으면 null"),
31
+ maxLength: z.number().int().describe("최대 글자 수"),
32
+ required: z.boolean(),
33
+ });
34
+
35
+ export type CustomInputView = z.infer<typeof customInputViewSchema>;
36
+
37
+ /**
38
+ * 직접 입력값 정규화 — NFC로 모으고, 제어 문자(C0·C1·줄바꿈·줄·문단 구분자)는 공백으로, 제로폭·양방향 제어 문자는 지운 뒤
39
+ * 앞뒤 공백을 지운다. 서버가 저장하는 값이 이것이다. 빈 문자열이면 입력하지 않은 것으로 본다.
40
+ * (양방향 제어 문자를 두면 셀러 주문 화면의 각인 문구가 뒤집혀 보이게 할 수 있다.)
41
+ */
42
+ export function normalizeCustomInputValue(value: string): string {
43
+ return (
44
+ value
45
+ .normalize("NFC")
46
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: 제어 문자를 지우려는 정규식이다
47
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ")
48
+ .replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g, "")
49
+ .trim()
50
+ );
51
+ }
52
+
53
+ /**
54
+ * 직접 입력값의 글자 수 — 정규화한 값의 **코드 포인트 수**다(이모지 하나가 1자). 서버의 최대 글자 수 판정(`CUSTOM_INPUT_TOO_LONG`)과
55
+ * 스토어프론트·콘솔의 글자 수 표시가 이 함수 하나를 쓴다. `String.length`(UTF-16)로 세면 이모지가 2자로 세어 판정이 갈린다.
56
+ */
57
+ export function customInputLength(value: string): number {
58
+ return [...normalizeCustomInputValue(value)].length;
59
+ }
60
+
61
+ /** 담기·바로 구매 요청의 직접 입력값 한 칸. 앞뒤 공백을 지우고, 빈 값은 입력하지 않은 것으로 본다 */
62
+ export const customInputEntrySchema = z.object({
63
+ inputId: z.string(),
64
+ value: z.string().max(1000),
65
+ });
66
+
67
+ export type CustomInputEntry = z.infer<typeof customInputEntrySchema>;
68
+
69
+ /** 고른 추가 선택 한 칸 — 옵션명(`groupId`)과 그 옵션명의 값(`valueId`) */
70
+ export const addonSelectionSchema = z.object({
71
+ groupId: z.string(),
72
+ valueId: z.string(),
73
+ });
74
+
75
+ export type AddonSelection = z.infer<typeof addonSelectionSchema>;
76
+
77
+ /** 요청의 추가 선택·직접 입력 필드 — 담기·장바구니 수정·바로 구매가 같은 이름을 쓴다 */
78
+ export const lineSelectionRequestShape = {
79
+ addons: z
80
+ .array(addonSelectionSchema)
81
+ .max(20)
82
+ .optional()
83
+ .describe(
84
+ "고른 추가 선택(옵션명마다 하나, 고르지 않은 옵션명은 빼고 보낸다). 필수 옵션명을 빼면 400 `ADDON_OPTION_REQUIRED`, 없거나 꺼진 값이거나 한 옵션명에서 둘을 고르면 400 `ADDON_OPTION_NOT_FOUND`",
85
+ ),
86
+ customInputs: z
87
+ .array(customInputEntrySchema)
88
+ .max(20)
89
+ .optional()
90
+ .describe(
91
+ "직접 입력값. 필수 항목을 비우면 400 `CUSTOM_INPUT_REQUIRED`, 최대 글자 수를 넘으면 400 `CUSTOM_INPUT_TOO_LONG`, 없는 항목이면 400 `CUSTOM_INPUT_NOT_FOUND`",
92
+ ),
93
+ };
94
+
95
+ /** 줄의 옵션 선택 스냅샷 한 칸 — 조합 옵션은 옵션명마다 한 칸이고 추가금이 0이다(조합 추가금은 단가에 들어 있다) */
96
+ export const optionSelectionViewSchema = z.object({
97
+ kind: z
98
+ .string()
99
+ .describe(
100
+ "옵션명 방식. 현재 값은 `combination`·`addon`이고 값이 추가될 수 있어 문자열로 받는다",
101
+ ),
102
+ groupName: z.string(),
103
+ valueName: z.string(),
104
+ additionalPrice: z.number().int(),
105
+ });
106
+
107
+ export type OptionSelectionView = z.infer<typeof optionSelectionViewSchema>;
108
+
109
+ export const customInputValueViewSchema = z.object({
110
+ label: z.string().describe("항목명"),
111
+ value: z.string().describe("구매자가 적은 값"),
112
+ });
113
+
114
+ export type CustomInputValueView = z.infer<typeof customInputValueViewSchema>;
115
+
116
+ /** 응답 줄의 표시 필드 — 장바구니·주문서·주문상품이 같은 이름을 쓴다 */
117
+ export const lineSelectionResponseShape = {
118
+ optionSelections: z
119
+ .array(optionSelectionViewSchema)
120
+ .optional()
121
+ .default([])
122
+ .describe("옵션 선택(조합 옵션 + 추가 선택). 옵션이 없으면 빈 배열. 서버는 항상 싣는다"),
123
+ customInputs: z
124
+ .array(customInputValueViewSchema)
125
+ .optional()
126
+ .default([])
127
+ .describe("직접 입력값(항목명과 값). 없으면 빈 배열. 서버는 항상 싣는다"),
128
+ };