@sayren/store-sdk 0.34.0 → 0.36.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 +410 -23
- package/dist/index.mjs +266 -13
- package/package.json +1 -1
- package/src/client.ts +116 -0
- package/src/index.ts +2 -0
- package/src/schemas/category.ts +25 -0
- package/src/schemas/collection.ts +25 -0
- package/src/schemas/locale.ts +39 -0
- package/src/schemas/market.ts +24 -0
- package/src/schemas/order.ts +9 -2
- package/src/schemas/product-info.ts +26 -0
- package/src/schemas/product-localization.ts +121 -0
- package/src/schemas/product.ts +13 -1
- package/src/schemas/saved-option-set.ts +62 -0
- package/src/schemas/store.ts +11 -0
package/src/client.ts
CHANGED
|
@@ -63,9 +63,12 @@ import { myAccountSchema, myStoreSchema } from "./schemas/auth";
|
|
|
63
63
|
import {
|
|
64
64
|
type CategoryCreateRequest,
|
|
65
65
|
type CategoryDeleteStrategy,
|
|
66
|
+
type CategoryLocalizationRequest,
|
|
66
67
|
type CategoryPatchRequest,
|
|
67
68
|
categoryAttributeSchema,
|
|
68
69
|
categoryCreateRequestSchema,
|
|
70
|
+
categoryLocalizationRequestSchema,
|
|
71
|
+
categoryLocalizationSchema,
|
|
69
72
|
categoryPatchRequestSchema,
|
|
70
73
|
categorySchema,
|
|
71
74
|
categorySearchResultSchema,
|
|
@@ -84,11 +87,14 @@ import {
|
|
|
84
87
|
import {
|
|
85
88
|
type CollectionCreateRequestInput,
|
|
86
89
|
type CollectionListParams,
|
|
90
|
+
type CollectionLocalizationRequest,
|
|
87
91
|
type CollectionPatchRequest,
|
|
88
92
|
type CollectionPreviewRequest,
|
|
89
93
|
type CollectionProductsAddRequest,
|
|
90
94
|
type CollectionProductsReplaceRequest,
|
|
91
95
|
collectionCreateRequestSchema,
|
|
96
|
+
collectionLocalizationRequestSchema,
|
|
97
|
+
collectionLocalizationSchema,
|
|
92
98
|
collectionPageSchema,
|
|
93
99
|
collectionPatchRequestSchema,
|
|
94
100
|
collectionPreviewRequestSchema,
|
|
@@ -308,18 +314,29 @@ import {
|
|
|
308
314
|
variantStockSchema,
|
|
309
315
|
} from "./schemas/product";
|
|
310
316
|
import {
|
|
317
|
+
type InfoSectionLocalizationRequest,
|
|
311
318
|
type InfoSectionRequestInput,
|
|
312
319
|
infoSchemaSchema,
|
|
320
|
+
infoSectionLocalizationRequestSchema,
|
|
321
|
+
infoSectionLocalizationSchema,
|
|
313
322
|
infoSectionRequestSchema,
|
|
314
323
|
infoSectionSchema,
|
|
315
324
|
platformCodeSchema,
|
|
316
325
|
} from "./schemas/product-info";
|
|
326
|
+
import {
|
|
327
|
+
type ProductLocalizationRequest,
|
|
328
|
+
productLocalizationRequestSchema,
|
|
329
|
+
productLocalizationSchema,
|
|
330
|
+
} from "./schemas/product-localization";
|
|
317
331
|
import {
|
|
318
332
|
type SavedOptionSetFromProductRequestInput,
|
|
319
333
|
type SavedOptionSetListParams,
|
|
334
|
+
type SavedOptionSetLocalizationRequest,
|
|
320
335
|
type SavedOptionSetRequestInput,
|
|
321
336
|
savedOptionSetFromProductRequestSchema,
|
|
322
337
|
savedOptionSetFromProductResultSchema,
|
|
338
|
+
savedOptionSetLocalizationRequestSchema,
|
|
339
|
+
savedOptionSetLocalizationSchema,
|
|
323
340
|
savedOptionSetRequestSchema,
|
|
324
341
|
savedOptionSetSchema,
|
|
325
342
|
} from "./schemas/saved-option-set";
|
|
@@ -592,6 +609,24 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
592
609
|
body: productPatchRequestSchema.parse(body),
|
|
593
610
|
}),
|
|
594
611
|
remove: (productId: string) => storeHttp.requestVoid("DELETE", path`/products/${productId}`),
|
|
612
|
+
/** 번역(이슈 #134) — 언어별 상품명·상세설명·이미지·옵션 이름 */
|
|
613
|
+
localizations: {
|
|
614
|
+
list: (productId: string) =>
|
|
615
|
+
storeHttp.request(
|
|
616
|
+
"GET",
|
|
617
|
+
path`/products/${productId}/localizations`,
|
|
618
|
+
z.array(productLocalizationSchema),
|
|
619
|
+
),
|
|
620
|
+
put: (productId: string, locale: string, body: ProductLocalizationRequest) =>
|
|
621
|
+
storeHttp.request(
|
|
622
|
+
"PUT",
|
|
623
|
+
path`/products/${productId}/localizations/${locale}`,
|
|
624
|
+
productLocalizationSchema,
|
|
625
|
+
{ body: productLocalizationRequestSchema.parse(body) },
|
|
626
|
+
),
|
|
627
|
+
remove: (productId: string, locale: string) =>
|
|
628
|
+
storeHttp.requestVoid("DELETE", path`/products/${productId}/localizations/${locale}`),
|
|
629
|
+
},
|
|
595
630
|
bulkUpdateStatus: (productIds: string[], status: ProductStatus, idempotencyKey?: string) =>
|
|
596
631
|
storeHttp.request("POST", "/products/bulk/status", bulkResultSchema, {
|
|
597
632
|
body: { productIds, status: productStatusSchema.parse(status) },
|
|
@@ -675,6 +710,27 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
675
710
|
savedOptionSetFromProductResultSchema,
|
|
676
711
|
{ body: savedOptionSetFromProductRequestSchema.parse(body) },
|
|
677
712
|
),
|
|
713
|
+
/** 번역(이슈 #134) — 기본 언어 이름이 키다. 불러올 때 상품 번역으로 복사한다 */
|
|
714
|
+
localizations: {
|
|
715
|
+
list: (optionSetId: string) =>
|
|
716
|
+
storeHttp.request(
|
|
717
|
+
"GET",
|
|
718
|
+
path`/saved-option-sets/${optionSetId}/localizations`,
|
|
719
|
+
z.array(savedOptionSetLocalizationSchema),
|
|
720
|
+
),
|
|
721
|
+
put: (optionSetId: string, locale: string, body: SavedOptionSetLocalizationRequest) =>
|
|
722
|
+
storeHttp.request(
|
|
723
|
+
"PUT",
|
|
724
|
+
path`/saved-option-sets/${optionSetId}/localizations/${locale}`,
|
|
725
|
+
savedOptionSetLocalizationSchema,
|
|
726
|
+
{ body: savedOptionSetLocalizationRequestSchema.parse(body) },
|
|
727
|
+
),
|
|
728
|
+
remove: (optionSetId: string, locale: string) =>
|
|
729
|
+
storeHttp.requestVoid(
|
|
730
|
+
"DELETE",
|
|
731
|
+
path`/saved-option-sets/${optionSetId}/localizations/${locale}`,
|
|
732
|
+
),
|
|
733
|
+
},
|
|
678
734
|
},
|
|
679
735
|
|
|
680
736
|
/**
|
|
@@ -711,6 +767,27 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
711
767
|
}),
|
|
712
768
|
remove: (infoSectionId: string) =>
|
|
713
769
|
storeHttp.requestVoid("DELETE", path`/info-sections/${infoSectionId}`),
|
|
770
|
+
/** 번역(이슈 #134) — 언어별 제목·본문 */
|
|
771
|
+
localizations: {
|
|
772
|
+
list: (infoSectionId: string) =>
|
|
773
|
+
storeHttp.request(
|
|
774
|
+
"GET",
|
|
775
|
+
path`/info-sections/${infoSectionId}/localizations`,
|
|
776
|
+
z.array(infoSectionLocalizationSchema),
|
|
777
|
+
),
|
|
778
|
+
put: (infoSectionId: string, locale: string, body: InfoSectionLocalizationRequest) =>
|
|
779
|
+
storeHttp.request(
|
|
780
|
+
"PUT",
|
|
781
|
+
path`/info-sections/${infoSectionId}/localizations/${locale}`,
|
|
782
|
+
infoSectionLocalizationSchema,
|
|
783
|
+
{ body: infoSectionLocalizationRequestSchema.parse(body) },
|
|
784
|
+
),
|
|
785
|
+
remove: (infoSectionId: string, locale: string) =>
|
|
786
|
+
storeHttp.requestVoid(
|
|
787
|
+
"DELETE",
|
|
788
|
+
path`/info-sections/${infoSectionId}/localizations/${locale}`,
|
|
789
|
+
),
|
|
790
|
+
},
|
|
714
791
|
},
|
|
715
792
|
|
|
716
793
|
/**
|
|
@@ -760,6 +837,27 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
760
837
|
storeHttp.request("POST", "/collections/preview", collectionPreviewSchema, {
|
|
761
838
|
body: collectionPreviewRequestSchema.parse(body),
|
|
762
839
|
}),
|
|
840
|
+
/** 번역(이슈 #134) — 언어별 제목·설명·SEO */
|
|
841
|
+
localizations: {
|
|
842
|
+
list: (collectionId: string) =>
|
|
843
|
+
storeHttp.request(
|
|
844
|
+
"GET",
|
|
845
|
+
path`/collections/${collectionId}/localizations`,
|
|
846
|
+
z.array(collectionLocalizationSchema),
|
|
847
|
+
),
|
|
848
|
+
put: (collectionId: string, locale: string, body: CollectionLocalizationRequest) =>
|
|
849
|
+
storeHttp.request(
|
|
850
|
+
"PUT",
|
|
851
|
+
path`/collections/${collectionId}/localizations/${locale}`,
|
|
852
|
+
collectionLocalizationSchema,
|
|
853
|
+
{ body: collectionLocalizationRequestSchema.parse(body) },
|
|
854
|
+
),
|
|
855
|
+
remove: (collectionId: string, locale: string) =>
|
|
856
|
+
storeHttp.requestVoid(
|
|
857
|
+
"DELETE",
|
|
858
|
+
path`/collections/${collectionId}/localizations/${locale}`,
|
|
859
|
+
),
|
|
860
|
+
},
|
|
763
861
|
},
|
|
764
862
|
|
|
765
863
|
categories: {
|
|
@@ -796,6 +894,24 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
796
894
|
storeHttp.requestVoid("DELETE", path`/categories/${categoryId}`, {
|
|
797
895
|
query: (opts?.strategy ? { strategy: opts.strategy } : {}) as Query,
|
|
798
896
|
}),
|
|
897
|
+
/** 번역(이슈 #134) — 언어별 이름·SEO */
|
|
898
|
+
localizations: {
|
|
899
|
+
list: (categoryId: string) =>
|
|
900
|
+
storeHttp.request(
|
|
901
|
+
"GET",
|
|
902
|
+
path`/categories/${categoryId}/localizations`,
|
|
903
|
+
z.array(categoryLocalizationSchema),
|
|
904
|
+
),
|
|
905
|
+
put: (categoryId: string, locale: string, body: CategoryLocalizationRequest) =>
|
|
906
|
+
storeHttp.request(
|
|
907
|
+
"PUT",
|
|
908
|
+
path`/categories/${categoryId}/localizations/${locale}`,
|
|
909
|
+
categoryLocalizationSchema,
|
|
910
|
+
{ body: categoryLocalizationRequestSchema.parse(body) },
|
|
911
|
+
),
|
|
912
|
+
remove: (categoryId: string, locale: string) =>
|
|
913
|
+
storeHttp.requestVoid("DELETE", path`/categories/${categoryId}/localizations/${locale}`),
|
|
914
|
+
},
|
|
799
915
|
},
|
|
800
916
|
|
|
801
917
|
orders: {
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from "./schemas/customer";
|
|
|
36
36
|
export * from "./schemas/customer-delegation";
|
|
37
37
|
export * from "./schemas/fulfillment";
|
|
38
38
|
export * from "./schemas/inventory";
|
|
39
|
+
export * from "./schemas/locale";
|
|
39
40
|
export * from "./schemas/login";
|
|
40
41
|
export * from "./schemas/market";
|
|
41
42
|
export * from "./schemas/misc";
|
|
@@ -44,6 +45,7 @@ export * from "./schemas/payment";
|
|
|
44
45
|
export * from "./schemas/points";
|
|
45
46
|
export * from "./schemas/product";
|
|
46
47
|
export * from "./schemas/product-info";
|
|
48
|
+
export * from "./schemas/product-localization";
|
|
47
49
|
export * from "./schemas/promotion";
|
|
48
50
|
export * from "./schemas/refund-policy";
|
|
49
51
|
export * from "./schemas/saved-option-set";
|
package/src/schemas/category.ts
CHANGED
|
@@ -146,3 +146,28 @@ export const categoryDeleteQuerySchema = z.object({
|
|
|
146
146
|
"`reparent`는 직계 자식을 삭제할 카테고리의 부모로 옮기고, `cascade`는 서브트리를 삭제하고 연결 상품을 미분류로 둔다",
|
|
147
147
|
),
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 카테고리 번역 (이슈 #134) — 언어 하나의 이름·SEO. 카테고리 필드(`name`·`meta*`)는 상점 기본 언어 값이고 번역 레코드가 언어별 값이다.
|
|
152
|
+
* 비운 칸(null)은 스토어프론트가 기본 언어 값으로 대체한다(칸 단위 대체).
|
|
153
|
+
*/
|
|
154
|
+
export const categoryLocalizationSchema = z.object({
|
|
155
|
+
locale: z.string().describe("언어(BCP 47)"),
|
|
156
|
+
name: z.string().nullable().describe("이름. null이면 기본 언어 이름"),
|
|
157
|
+
metaTitle: z.string().nullable(),
|
|
158
|
+
metaDescription: z.string().nullable(),
|
|
159
|
+
metaKeywords: z.string().nullable(),
|
|
160
|
+
updatedAt: z.string(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export type CategoryLocalization = z.infer<typeof categoryLocalizationSchema>;
|
|
164
|
+
|
|
165
|
+
/** `PUT /v1/categories/{categoryId}/localizations/{locale}` 본문 — 레코드 전체를 바꾼다. 빠진 칸은 null(기본 언어 값으로 대체)이다 */
|
|
166
|
+
export const categoryLocalizationRequestSchema = z.strictObject({
|
|
167
|
+
name: z.string().trim().min(1).max(100).nullable().optional(),
|
|
168
|
+
metaTitle: z.string().max(100).nullable().optional(),
|
|
169
|
+
metaDescription: z.string().max(300).nullable().optional(),
|
|
170
|
+
metaKeywords: z.string().max(200).nullable().optional(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export type CategoryLocalizationRequest = z.infer<typeof categoryLocalizationRequestSchema>;
|
|
@@ -587,3 +587,28 @@ export const collectionPreviewSchema = z.object({
|
|
|
587
587
|
});
|
|
588
588
|
|
|
589
589
|
export type CollectionPreview = z.infer<typeof collectionPreviewSchema>;
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 컬렉션 번역 (이슈 #134) — 언어 하나의 제목·설명·SEO. 컬렉션 필드는 상점 기본 언어 값이고 번역 레코드가 언어별 값이다.
|
|
593
|
+
* 비운 칸(null)은 스토어프론트가 기본 언어 값으로 대체한다(칸 단위 대체).
|
|
594
|
+
*/
|
|
595
|
+
export const collectionLocalizationSchema = z.object({
|
|
596
|
+
locale: z.string().describe("언어(BCP 47)"),
|
|
597
|
+
title: z.string().nullable().describe("제목. null이면 기본 언어 제목"),
|
|
598
|
+
description: z.string().nullable(),
|
|
599
|
+
metaTitle: z.string().nullable(),
|
|
600
|
+
metaDescription: z.string().nullable(),
|
|
601
|
+
updatedAt: z.string(),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
export type CollectionLocalization = z.infer<typeof collectionLocalizationSchema>;
|
|
605
|
+
|
|
606
|
+
/** `PUT /v1/collections/{collectionId}/localizations/{locale}` 본문 — 레코드 전체를 바꾼다. 빠진 칸은 null(기본 언어 값으로 대체)이다 */
|
|
607
|
+
export const collectionLocalizationRequestSchema = z.strictObject({
|
|
608
|
+
title: z.string().trim().min(1).max(MAX_COLLECTION_TITLE).nullable().optional(),
|
|
609
|
+
description: z.string().trim().max(MAX_COLLECTION_DESCRIPTION).nullable().optional(),
|
|
610
|
+
metaTitle: z.string().trim().max(60).nullable().optional(),
|
|
611
|
+
metaDescription: z.string().trim().max(160).nullable().optional(),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
export type CollectionLocalizationRequest = z.infer<typeof collectionLocalizationRequestSchema>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 언어 코드 (이슈 #134) — BCP 47의 언어[-문자][-지역]까지 받는다(`ko`·`en`·`en-US`·`zh-Hant-TW`).
|
|
5
|
+
* 대소문자는 표준 표기로 맞춘다(언어 소문자, 문자 첫 글자 대문자, 지역 대문자). 맞지 않으면 null이다.
|
|
6
|
+
*/
|
|
7
|
+
const LOCALE_PATTERN = /^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2}))?$/;
|
|
8
|
+
|
|
9
|
+
export function normalizeLocale(value: string | null | undefined): string | null {
|
|
10
|
+
const match = value?.trim().replace(/_/g, "-").toLowerCase().match(LOCALE_PATTERN);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
const [, language, script, region] = match;
|
|
13
|
+
return [
|
|
14
|
+
language,
|
|
15
|
+
script ? `${script[0]?.toUpperCase()}${script.slice(1)}` : null,
|
|
16
|
+
region ? region.toUpperCase() : null,
|
|
17
|
+
]
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join("-");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 상점 기본 언어 — 옛 상점과 언어를 정하지 않은 상점의 값이다 */
|
|
23
|
+
export const DEFAULT_STORE_LOCALE = "ko";
|
|
24
|
+
|
|
25
|
+
export const localeCodeSchema = z
|
|
26
|
+
.string()
|
|
27
|
+
.max(35)
|
|
28
|
+
.transform((value, ctx) => {
|
|
29
|
+
const normalized = normalizeLocale(value);
|
|
30
|
+
if (normalized === null) {
|
|
31
|
+
ctx.addIssue({
|
|
32
|
+
code: "custom",
|
|
33
|
+
message: "언어는 BCP 47 언어 코드입니다(예: ko·en·en-US)",
|
|
34
|
+
});
|
|
35
|
+
return z.NEVER;
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
})
|
|
39
|
+
.describe("언어(BCP 47, 예: ko·en·en-US·zh-Hant-TW)");
|
package/src/schemas/market.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { currencyCodeSchema } from "./currency";
|
|
3
|
+
import { localeCodeSchema } from "./locale";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 판매 권역 (이슈 #130) — 상점이 어느 국가에 어느 통화로 파는지를 정한다.
|
|
@@ -9,6 +10,8 @@ import { currencyCodeSchema } from "./currency";
|
|
|
9
10
|
*
|
|
10
11
|
* 스토어프론트는 요청마다 도메인(`X-Storefront-Host`) → 구매자 국가(`X-Buyer-Country`) → 기본 권역 순으로 권역을 고르고, 주문서가 그 권역·통화를 굳힌다.
|
|
11
12
|
* 외화 권역은 적립금을 켤 수 없다(적립금은 기본 통화 단위다).
|
|
13
|
+
*
|
|
14
|
+
* 권역은 언어도 정한다(이슈 #134). `locales`는 이 권역 스토어프론트가 내는 언어이고 첫 언어가 대표 언어다. 비어 있으면 상점 기본 언어 하나다.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
const countryCodeSchema = z
|
|
@@ -44,6 +47,13 @@ export const marketSchema = z.object({
|
|
|
44
47
|
.string()
|
|
45
48
|
.nullable()
|
|
46
49
|
.describe("보관 시각. 보관한 권역은 판정에서 빠지고 그 국가·도메인은 풀린다"),
|
|
50
|
+
locales: z
|
|
51
|
+
.array(z.string())
|
|
52
|
+
.optional()
|
|
53
|
+
.default([])
|
|
54
|
+
.describe(
|
|
55
|
+
"이 권역 언어(이슈 #134). 첫 언어가 대표 언어다. 비어 있으면 상점 기본 언어 하나다. 서버는 항상 싣는다",
|
|
56
|
+
),
|
|
47
57
|
});
|
|
48
58
|
|
|
49
59
|
export type Market = z.infer<typeof marketSchema>;
|
|
@@ -55,6 +65,13 @@ export const createMarketRequestSchema = z.strictObject({
|
|
|
55
65
|
domains: z.array(hostnameSchema).max(20).optional(),
|
|
56
66
|
couponsEnabled: z.boolean().optional().describe("기본 false"),
|
|
57
67
|
pointsEnabled: z.boolean().optional().describe("기본 false. 외화 권역은 true로 둘 수 없다"),
|
|
68
|
+
locales: z
|
|
69
|
+
.array(localeCodeSchema)
|
|
70
|
+
.max(20)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe(
|
|
73
|
+
"이 권역 언어(이슈 #134). 주면 목록 전체를 바꾼다. 첫 언어가 대표 언어다. 비우면 상점 기본 언어 하나다",
|
|
74
|
+
),
|
|
58
75
|
});
|
|
59
76
|
|
|
60
77
|
export type CreateMarketRequest = z.infer<typeof createMarketRequestSchema>;
|
|
@@ -70,6 +87,13 @@ export const updateMarketRequestSchema = z.strictObject({
|
|
|
70
87
|
domains: z.array(hostnameSchema).max(20).optional().describe("주면 도메인 목록 전체를 바꾼다"),
|
|
71
88
|
couponsEnabled: z.boolean().optional(),
|
|
72
89
|
pointsEnabled: z.boolean().optional().describe("외화 권역은 true로 둘 수 없다"),
|
|
90
|
+
locales: z
|
|
91
|
+
.array(localeCodeSchema)
|
|
92
|
+
.max(20)
|
|
93
|
+
.optional()
|
|
94
|
+
.describe(
|
|
95
|
+
"이 권역 언어(이슈 #134). 주면 목록 전체를 바꾼다. 첫 언어가 대표 언어다. 비우면 상점 기본 언어 하나다",
|
|
96
|
+
),
|
|
73
97
|
});
|
|
74
98
|
|
|
75
99
|
export type UpdateMarketRequest = z.infer<typeof updateMarketRequestSchema>;
|
package/src/schemas/order.ts
CHANGED
|
@@ -284,6 +284,13 @@ export const orderSchema = z.object({
|
|
|
284
284
|
.describe(
|
|
285
285
|
"테스트 결제(샌드박스 결제) 주문인가. true면 실제로 돈이 오가지 않았습니다. 상품을 발송하지 않도록 주의하십시오",
|
|
286
286
|
),
|
|
287
|
+
locale: z
|
|
288
|
+
.string()
|
|
289
|
+
.nullable()
|
|
290
|
+
.optional()
|
|
291
|
+
.describe(
|
|
292
|
+
"구매 언어(이슈 #134, BCP 47). 주문상품의 상품명·옵션 이름은 구매자가 이 언어로 본 값이다. null이면 상점 기본 언어다(언어 기능 이전 주문 포함)",
|
|
293
|
+
),
|
|
287
294
|
items: z.array(orderItemSchema),
|
|
288
295
|
});
|
|
289
296
|
|
|
@@ -310,7 +317,7 @@ const pageQuery = {
|
|
|
310
317
|
|
|
311
318
|
/**
|
|
312
319
|
* `GET /v1/orders` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열). 검증 규칙은 서버가 적용한다:
|
|
313
|
-
* `orderNo` 없이 `orderedFrom` 누락 400 `MISSING_PARAMETER`, 날짜·`orderNo` 형식 오류 400 `INVALID_PARAMETER`,
|
|
320
|
+
* `orderNo` 없이 `orderedFrom` 누락 400 `MISSING_PARAMETER`, 날짜·`orderNo` 형식 오류 400 `INVALID_PARAMETER`, 366일 초과 400 `RANGE_TOO_WIDE`,
|
|
314
321
|
* `testPayment`가 `true`·`false`가 아니면 400 `INVALID_PARAMETER`
|
|
315
322
|
*/
|
|
316
323
|
export const orderListQuerySchema = z.object({
|
|
@@ -321,7 +328,7 @@ export const orderListQuerySchema = z.object({
|
|
|
321
328
|
orderedTo: z
|
|
322
329
|
.string()
|
|
323
330
|
.optional()
|
|
324
|
-
.describe("조회 끝 시각 (ISO 8601, 포함). 생략하면 orderedFrom부터 31일. 기간은 최대
|
|
331
|
+
.describe("조회 끝 시각 (ISO 8601, 포함). 생략하면 orderedFrom부터 31일. 기간은 최대 366일"),
|
|
325
332
|
status: orderItemStatusSchema
|
|
326
333
|
.optional()
|
|
327
334
|
.describe("주문상품 상태 필터 — 이 상태의 주문상품이 하나라도 있는 주문"),
|
|
@@ -188,3 +188,29 @@ export const infoSectionRequestSchema = z.object({
|
|
|
188
188
|
|
|
189
189
|
export type InfoSectionRequest = z.infer<typeof infoSectionRequestSchema>;
|
|
190
190
|
export type InfoSectionRequestInput = z.input<typeof infoSectionRequestSchema>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 안내 섹션 번역 (이슈 #134) — 언어 하나의 제목·본문. 섹션 필드는 상점 기본 언어 값이다. 비운 칸(null)은 스토어프론트가 기본 언어 값으로 대체한다.
|
|
194
|
+
* 섹션을 연결한 모든 상품이 같은 번역을 보인다.
|
|
195
|
+
*/
|
|
196
|
+
export const infoSectionLocalizationSchema = z.object({
|
|
197
|
+
locale: z.string().describe("언어(BCP 47)"),
|
|
198
|
+
title: z.string().nullable(),
|
|
199
|
+
body: z
|
|
200
|
+
.string()
|
|
201
|
+
.nullable()
|
|
202
|
+
.describe("본문 HTML(상세설명과 같은 정화 규칙). null이면 기본 언어 본문"),
|
|
203
|
+
bodyJson: z.string().nullable(),
|
|
204
|
+
updatedAt: z.string(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export type InfoSectionLocalization = z.infer<typeof infoSectionLocalizationSchema>;
|
|
208
|
+
|
|
209
|
+
/** `PUT /v1/info-sections/{infoSectionId}/localizations/{locale}` 본문 — 레코드 전체를 바꾼다. 빠진 칸은 null이다 */
|
|
210
|
+
export const infoSectionLocalizationRequestSchema = z.strictObject({
|
|
211
|
+
title: z.string().trim().min(1).max(MAX_INFO_SECTION_TITLE).nullable().optional(),
|
|
212
|
+
body: z.string().nullable().optional(),
|
|
213
|
+
bodyJson: z.string().nullable().optional(),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export type InfoSectionLocalizationRequest = z.infer<typeof infoSectionLocalizationRequestSchema>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MAX_PRODUCT_IMAGES, productImageUrlSchema } from "./product";
|
|
3
|
+
import { MAX_NOTICE_VALUE_LENGTH } from "./product-info";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 상품 번역 (이슈 #134) — 언어 하나의 상품명·상세설명·이미지·옵션 이름. 상품 필드는 상점 기본 언어 값이고 번역 레코드가 언어별 값이다.
|
|
7
|
+
* 비운 칸(null)은 스토어프론트가 기본 언어 값으로 대체한다(칸 단위 대체). 이미지는 목록 통째로 대체한다.
|
|
8
|
+
* 옵션은 id가 원천이다 — 값 id·추가금·재고는 언어와 무관하고 이름만 언어별이다.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const translatedOptionValueSchema = z.object({
|
|
12
|
+
valueId: z.string(),
|
|
13
|
+
name: z.string().nullable().describe("값 이름. null이면 기본 언어 이름"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const translatedOptionGroupSchema = z.object({
|
|
17
|
+
groupId: z.string(),
|
|
18
|
+
name: z.string().nullable().describe("옵션명. null이면 기본 언어 이름"),
|
|
19
|
+
values: z.array(translatedOptionValueSchema),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const translatedCustomInputSchema = z.object({
|
|
23
|
+
inputId: z.string(),
|
|
24
|
+
label: z.string().nullable().describe("항목명. null이면 기본 언어 항목명"),
|
|
25
|
+
placeholder: z.string().nullable().describe("입력 안내. null이면 기본 언어 안내"),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const translatedNoticeValueSchema = z.object({
|
|
29
|
+
key: z.string().describe("고시 칸 키"),
|
|
30
|
+
label: z.string().describe("칸 이름(원문)"),
|
|
31
|
+
value: z.string().nullable().describe("셀러 값 번역. null이면 기본 언어 값"),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 조회 응답 — 지금 상품의 옵션명·값·직접 입력 항목, 셀러가 적은 고시 칸을 모두 싣고 번역하지 않은 칸은 null이다.
|
|
36
|
+
* `optionGroups`는 조합 옵션, `addonGroups`는 추가 선택이다(상품 응답과 같은 구분)
|
|
37
|
+
*/
|
|
38
|
+
export const productLocalizationSchema = z.object({
|
|
39
|
+
locale: z.string().describe("언어(BCP 47)"),
|
|
40
|
+
name: z.string().nullable().describe("상품명. null이면 기본 언어 상품명"),
|
|
41
|
+
description: z.string().nullable().describe("상세설명(HTML). null이면 기본 언어 상세설명"),
|
|
42
|
+
descriptionJson: z.string().nullable().describe("상세설명 에디터 상태(재편집 복원용)"),
|
|
43
|
+
images: z.array(z.string()).nullable().describe("이미지 목록. null이면 기본 언어 목록"),
|
|
44
|
+
optionGroups: z.array(translatedOptionGroupSchema),
|
|
45
|
+
addonGroups: z.array(translatedOptionGroupSchema),
|
|
46
|
+
customInputs: z.array(translatedCustomInputSchema),
|
|
47
|
+
notice: z
|
|
48
|
+
.array(translatedNoticeValueSchema)
|
|
49
|
+
.optional()
|
|
50
|
+
.default([])
|
|
51
|
+
.describe(
|
|
52
|
+
"셀러가 적은 상품정보제공 고시 칸의 값 번역(이슈 #134). 출처·법정 문구 칸은 싣지 않는다",
|
|
53
|
+
),
|
|
54
|
+
updatedAt: z.string(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export type ProductLocalization = z.infer<typeof productLocalizationSchema>;
|
|
58
|
+
|
|
59
|
+
const translatedNameSchema = z.string().trim().min(1).max(50).nullable().optional();
|
|
60
|
+
|
|
61
|
+
const translatedOptionGroupRequestSchema = z.strictObject({
|
|
62
|
+
groupId: z.string(),
|
|
63
|
+
name: translatedNameSchema,
|
|
64
|
+
values: z.array(z.strictObject({ valueId: z.string(), name: translatedNameSchema })).optional(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `PUT /v1/products/{productId}/localizations/{locale}` 본문 — 레코드 전체를 바꾼다. 빠진 칸은 null(기본 언어 값으로 대체)이다.
|
|
69
|
+
* 옵션은 번역할 id만 보낸다. 상품에 없는 id는 400 `PRODUCT_LOCALIZATION_TARGET_NOT_FOUND`다.
|
|
70
|
+
* 번역한 옵션명·값 이름도 기본 언어와 같은 규칙(대소문자·앞뒤 공백 무시 유일)이고 겹치면 400 `INVALID_OPTION_MODEL`이다
|
|
71
|
+
*/
|
|
72
|
+
export const productLocalizationRequestSchema = z.strictObject({
|
|
73
|
+
name: z.string().trim().min(1).max(100).nullable().optional(),
|
|
74
|
+
description: z.string().nullable().optional(),
|
|
75
|
+
descriptionJson: z.string().nullable().optional(),
|
|
76
|
+
images: z.array(productImageUrlSchema).min(1).max(MAX_PRODUCT_IMAGES).nullable().optional(),
|
|
77
|
+
optionGroups: z.array(translatedOptionGroupRequestSchema).optional(),
|
|
78
|
+
addonGroups: z.array(translatedOptionGroupRequestSchema).optional(),
|
|
79
|
+
customInputs: z
|
|
80
|
+
.array(
|
|
81
|
+
z.strictObject({
|
|
82
|
+
inputId: z.string(),
|
|
83
|
+
label: z.string().trim().min(1).max(30).nullable().optional(),
|
|
84
|
+
placeholder: z.string().trim().min(1).max(100).nullable().optional(),
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
.optional(),
|
|
88
|
+
notice: z
|
|
89
|
+
.array(
|
|
90
|
+
z.strictObject({
|
|
91
|
+
key: z.string(),
|
|
92
|
+
value: z.string().trim().min(1).max(MAX_NOTICE_VALUE_LENGTH).nullable().optional(),
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
.optional()
|
|
96
|
+
.describe(
|
|
97
|
+
"고시 칸 값 번역. 상품에 셀러 값이 없는 칸 키는 400 `PRODUCT_LOCALIZATION_TARGET_NOT_FOUND`",
|
|
98
|
+
),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export type ProductLocalizationRequest = z.infer<typeof productLocalizationRequestSchema>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 번역할 칸이 비었는가(이슈 #134, 2026-10-18 결정: 완성도 대신 「번역 필요」 표시만) — 상품명, 기본 언어에 본문이 있는 상세설명,
|
|
105
|
+
* 옵션명·값·직접 입력 항목명, 셀러가 적은 고시 칸. 이미지·입력 안내는 기본 언어 값을 그대로 써도 되는 칸이라 세지 않는다.
|
|
106
|
+
* 상점 플랫폼(상품 수정·목록)과 관리 목록 응답이 같은 함수다
|
|
107
|
+
*/
|
|
108
|
+
export function productLocalizationNeedsTranslation(
|
|
109
|
+
localization: Pick<
|
|
110
|
+
ProductLocalization,
|
|
111
|
+
"name" | "description" | "optionGroups" | "addonGroups" | "customInputs"
|
|
112
|
+
> & { notice?: ProductLocalization["notice"] },
|
|
113
|
+
base: { description?: string | null },
|
|
114
|
+
): boolean {
|
|
115
|
+
if (!localization.name) return true;
|
|
116
|
+
if ((base.description ?? "").trim() && !localization.description) return true;
|
|
117
|
+
const groups = [...localization.optionGroups, ...localization.addonGroups];
|
|
118
|
+
if (groups.some((group) => !group.name || group.values.some((value) => !value.name))) return true;
|
|
119
|
+
if (localization.customInputs.some((item) => !item.label)) return true;
|
|
120
|
+
return (localization.notice ?? []).some((entry) => !entry.value);
|
|
121
|
+
}
|
package/src/schemas/product.ts
CHANGED
|
@@ -67,6 +67,18 @@ export const productSummarySchema = z.object({
|
|
|
67
67
|
.describe(
|
|
68
68
|
"상점을 만들 때 넣은 샘플 상품인가(이슈 #70). 상점 플랫폼이 「샘플」 배지를 단다. 스토어프론트에는 싣지 않는다. 서버는 항상 싣는다",
|
|
69
69
|
),
|
|
70
|
+
localizations: z
|
|
71
|
+
.array(
|
|
72
|
+
z.object({
|
|
73
|
+
locale: z.string().describe("언어(BCP 47)"),
|
|
74
|
+
needsTranslation: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.describe("번역할 칸이 비었는가(`productLocalizationNeedsTranslation`과 같은 규칙)"),
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
.optional()
|
|
80
|
+
.default([])
|
|
81
|
+
.describe("번역 레코드가 있는 언어(이슈 #134). 상점 기본 언어는 싣지 않는다"),
|
|
70
82
|
});
|
|
71
83
|
|
|
72
84
|
export type ProductSummary = z.infer<typeof productSummarySchema>;
|
|
@@ -372,7 +384,7 @@ export const discountSchema = z.object({
|
|
|
372
384
|
|
|
373
385
|
export type Discount = z.infer<typeof discountSchema>;
|
|
374
386
|
|
|
375
|
-
export const productSchema = productSummarySchema.extend({
|
|
387
|
+
export const productSchema = productSummarySchema.omit({ localizations: true }).extend({
|
|
376
388
|
prices: z
|
|
377
389
|
.array(storedProductCurrencyPriceSchema)
|
|
378
390
|
.optional()
|
|
@@ -174,3 +174,65 @@ export const savedOptionSetListQuerySchema = z.object({
|
|
|
174
174
|
sort: savedOptionSetSortSchema.optional().describe("정렬 (기본 updatedAt.desc)"),
|
|
175
175
|
...pageQueryShape({ defaultSize: 20, maxSize: 200 }),
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 저장 옵션 번역 (이슈 #134) — 저장 옵션에는 id가 없어 **기본 언어 이름이 키**다(`baseName`·`baseLabel`).
|
|
180
|
+
* 상품이 불러올 때 상품 번역으로 함께 복사하고(상품 저장 트랜잭션), 그 뒤로는 상품 번역이 원천이다 — 저장 옵션 번역을 고쳐도
|
|
181
|
+
* 이미 불러온 상품은 그대로다. 기본 언어 이름을 바꾸면 그 이름의 번역은 읽지 않는다.
|
|
182
|
+
*/
|
|
183
|
+
export const savedOptionSetLocalizationSchema = z.object({
|
|
184
|
+
locale: z.string().describe("언어(BCP 47)"),
|
|
185
|
+
groups: z.array(
|
|
186
|
+
z.object({
|
|
187
|
+
baseName: z.string().describe("기본 언어 옵션명"),
|
|
188
|
+
name: z.string().nullable().describe("번역 옵션명. null이면 기본 언어 이름"),
|
|
189
|
+
values: z.array(
|
|
190
|
+
z.object({
|
|
191
|
+
baseName: z.string(),
|
|
192
|
+
name: z.string().nullable(),
|
|
193
|
+
}),
|
|
194
|
+
),
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
197
|
+
customInputs: z.array(
|
|
198
|
+
z.object({
|
|
199
|
+
baseLabel: z.string().describe("기본 언어 항목명"),
|
|
200
|
+
label: z.string().nullable(),
|
|
201
|
+
placeholder: z.string().nullable(),
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
updatedAt: z.string(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export type SavedOptionSetLocalization = z.infer<typeof savedOptionSetLocalizationSchema>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* `PUT /v1/saved-option-sets/{optionSetId}/localizations/{locale}` 본문 — 레코드 전체를 바꾼다. 번역할 이름만 보낸다.
|
|
211
|
+
* 저장 옵션에 없는 기본 언어 이름은 400 `OPTION_SET_LOCALIZATION_TARGET_NOT_FOUND`, 번역 이름 중복은 상품과 같은 규칙으로 400 `INVALID_OPTION_MODEL`이다
|
|
212
|
+
*/
|
|
213
|
+
export const savedOptionSetLocalizationRequestSchema = z.strictObject({
|
|
214
|
+
groups: z
|
|
215
|
+
.array(
|
|
216
|
+
z.strictObject({
|
|
217
|
+
baseName: z.string(),
|
|
218
|
+
name: nameSchema.nullable().optional(),
|
|
219
|
+
values: z
|
|
220
|
+
.array(z.strictObject({ baseName: z.string(), name: nameSchema.nullable().optional() }))
|
|
221
|
+
.optional(),
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
224
|
+
.optional(),
|
|
225
|
+
customInputs: z
|
|
226
|
+
.array(
|
|
227
|
+
z.strictObject({
|
|
228
|
+
baseLabel: z.string(),
|
|
229
|
+
label: z.string().trim().min(1).max(30).nullable().optional(),
|
|
230
|
+
placeholder: z.string().trim().min(1).max(100).nullable().optional(),
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
.optional(),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
export type SavedOptionSetLocalizationRequest = z.infer<
|
|
237
|
+
typeof savedOptionSetLocalizationRequestSchema
|
|
238
|
+
>;
|
package/src/schemas/store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { storeRoleSchema } from "./auth";
|
|
3
|
+
import { localeCodeSchema } from "./locale";
|
|
3
4
|
import { refundPolicyPatchSchema, refundPolicySchema } from "./refund-policy";
|
|
4
5
|
import { shippingPolicyPatchSchema, shippingPolicySchema } from "./shipping-policy";
|
|
5
6
|
|
|
@@ -307,6 +308,11 @@ export const updateStoreRequestSchema = z
|
|
|
307
308
|
privacyUrl: policyDocumentUrlSchema
|
|
308
309
|
.optional()
|
|
309
310
|
.describe("개인정보 수집이용 동의 문서 주소(https). `null`을 주면 지운다"),
|
|
311
|
+
defaultLocale: localeCodeSchema
|
|
312
|
+
.optional()
|
|
313
|
+
.describe(
|
|
314
|
+
"상점 기본 언어(이슈 #134). 상품·카테고리 필드가 이 언어 값이고 번역이 없는 칸은 이 값으로 보인다. 바꿔도 저장된 값은 그대로다",
|
|
315
|
+
),
|
|
310
316
|
})
|
|
311
317
|
.refine((value) => Object.values(value).some((v) => v !== undefined), {
|
|
312
318
|
message: "최소 1개 필드가 필요합니다",
|
|
@@ -358,6 +364,11 @@ export const storeSchema = z.object({
|
|
|
358
364
|
.describe("이용약관 문서 주소. 정하지 않았으면 null이고 동의 이력에도 주소가 남지 않는다"),
|
|
359
365
|
privacyUrl: z.string().nullable().describe("개인정보 수집이용 동의 문서 주소"),
|
|
360
366
|
memberCount: z.number().int(),
|
|
367
|
+
defaultLocale: z
|
|
368
|
+
.string()
|
|
369
|
+
.optional()
|
|
370
|
+
.default("ko")
|
|
371
|
+
.describe("상점 기본 언어(이슈 #134, BCP 47). 서버는 항상 싣는다"),
|
|
361
372
|
createdAt: z.string(),
|
|
362
373
|
});
|
|
363
374
|
|