@zalkera/client 0.7.0 → 0.8.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/README.md +16 -37
- package/dist/index.cjs +47 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +47 -21
- package/dist/index.js.map +1 -1
- package/llms.txt +93 -12
- package/package.json +48 -47
package/README.md
CHANGED
|
@@ -7,43 +7,6 @@
|
|
|
7
7
|
- 동작: 응답 껍데기(`ApiResponse<T>`)를 벗겨 `data`만 반환, 실패는 `ZalkeraError`로 throw.
|
|
8
8
|
- 전제: **서버 사이드 전용**(RSC·route handler·server action). 브라우저 직접 호출 미지원 ([서버 사이드 전용](#서버-사이드-전용-중요) 참고).
|
|
9
9
|
|
|
10
|
-
## `@oneque/client` 에서 옮겨오기
|
|
11
|
-
|
|
12
|
-
패키지 이름과 공개 심볼이 함께 바뀌었다(브랜드 개명). 옛 패키지는 발행 상태로 남지만 더는 갱신되지 않는다.
|
|
13
|
-
|
|
14
|
-
```diff
|
|
15
|
-
- "@oneque/client": "^0.6.0"
|
|
16
|
-
+ "@zalkera/client": "^0.6.1"
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
```diff
|
|
20
|
-
- import {createOnequeClient, OnequeError} from "@oneque/client";
|
|
21
|
-
+ import {createZalkeraClient, ZalkeraError} from "@zalkera/client";
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
| 옛 이름 | 새 이름 |
|
|
25
|
-
|---|---|
|
|
26
|
-
| `createOnequeClient` | `createZalkeraClient` |
|
|
27
|
-
| `OnequeError` | `ZalkeraError` |
|
|
28
|
-
| `OnequeClient` | `ZalkeraClient` |
|
|
29
|
-
| `OnequeClientOptions` | `ZalkeraClientOptions` |
|
|
30
|
-
|
|
31
|
-
**환경변수 이름도 `ZALKERA_*` 로 바뀌었다**(2026-07-26 컷오버). 관리형 서빙(잘커라가 띄우는 사이트)은
|
|
32
|
-
인프라가 새 이름으로 주입하므로 할 일이 없다. **자기 인프라로 돌리는 BYO 사이트는 `.env` 의 이름을 직접
|
|
33
|
-
바꿔야 한다** — 구 이름 폴백은 제거됐다:
|
|
34
|
-
|
|
35
|
-
| 옛 이름 | 새 이름 |
|
|
36
|
-
|---|---|
|
|
37
|
-
| `ONEQUE_API_BASE` | `ZALKERA_API_BASE` |
|
|
38
|
-
| `ONEQUE_TENANT` | `ZALKERA_TENANT` |
|
|
39
|
-
| `ONEQUE_STOREFRONT_KEY` | `ZALKERA_STOREFRONT_KEY` |
|
|
40
|
-
| `ONEQUE_SITE_URL` | `ZALKERA_SITE_URL` |
|
|
41
|
-
|
|
42
|
-
동적 SSR 정당화 마커도 `zalkera-allow-dynamic` 이 새 이름이지만 기존 `oneque-`/`oneq-` 마커를 계속 수용한다.
|
|
43
|
-
`x-oneque-revalidate-secret` **헤더 이름은 그대로다** — 백엔드가 보내는 와이어 이름이라 별도 전환이 필요하다.
|
|
44
|
-
|
|
45
|
-
기능·API 동작은 동일하다 — 이름만 바뀌었다.
|
|
46
|
-
|
|
47
10
|
## 설치
|
|
48
11
|
|
|
49
12
|
```bash
|
|
@@ -108,6 +71,7 @@ await zalkera.submitInquiry({name, email, subject, message}, {clientIp});
|
|
|
108
71
|
| `getPost(slug)` | `GET /api/public/posts/{slug}` | `PostDetail` |
|
|
109
72
|
| `recordPostView(slug, ctx?)` | `POST /api/public/posts/{slug}/view` | `boolean` |
|
|
110
73
|
| `getPage(slug, options?)` | `GET /api/public/pages/{slug}` | `PageContent` |
|
|
74
|
+
| `listPages(params?, options?)` | `GET /api/public/pages` | `Paginated<PageSummary>` |
|
|
111
75
|
| `listMenus(options?)` | `GET /api/public/menus` | `Menu[]` |
|
|
112
76
|
| `getMediaUrl(id)` | `GET /api/public/media/{id}/url` | `MediaUrl` |
|
|
113
77
|
| `submitInquiry(input, ctx?)` | `POST /api/public/inquiries` | `InquiryCreated` |
|
|
@@ -124,6 +88,7 @@ await zalkera.submitInquiry({name, email, subject, message}, {clientIp});
|
|
|
124
88
|
| `getProductReviewSummary(productId, options?)` | `GET /api/public/products/{id}/reviews/summary` | `RatingSummary` |
|
|
125
89
|
| `createProductReview(productId, input, accessToken)` | `POST /api/shop/products/{id}/reviews` | `Review` |
|
|
126
90
|
|
|
91
|
+
- `listPages`는 **열거 전용**(본문 없음 — sitemap용). `size` 상한 100.
|
|
127
92
|
- 후기 조회는 **숫자 `productId`**(slug 아님) — `getProduct(slug).id`로 획득.
|
|
128
93
|
- 후기 작성은 로그인 필수 + 구매검증(본인·배송완료 이상·상품 일치).
|
|
129
94
|
|
|
@@ -246,6 +211,20 @@ try {
|
|
|
246
211
|
}
|
|
247
212
|
```
|
|
248
213
|
|
|
214
|
+
### SDK 가 직접 만드는 에러 코드
|
|
215
|
+
|
|
216
|
+
백엔드 `ErrorCode`가 아니라 클라이언트가 붙이는 코드다. 전부 `status === 502`이고 **배선·상류 문제**라 재시도로 풀리지 않는다.
|
|
217
|
+
|
|
218
|
+
| `code` | 뜻 / 처방 |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `UPSTREAM_REDIRECT` | 백엔드가 3xx로 응답했다. **클라이언트는 리다이렉트를 따라가지 않는다** — 따라가면 `X-Storefront-Key`·`X-Tenant`·`X-Cart-Session`이 제3자 오리진에 그대로 전달되기 때문이다. `baseUrl`이 **최종 오리진**인지 확인하라(http↔https 승격·www 유무·프록시 경로 리라이트). 우회 옵션은 없다(보안 불변식). |
|
|
221
|
+
| `UPSTREAM_NON_JSON` | 2xx인데 본문이 envelope가 아니다(게이트웨이 HTML 등). |
|
|
222
|
+
| `UPSTREAM_UNAVAILABLE` | 502·503·504에 비JSON 본문. 상류 장애. |
|
|
223
|
+
|
|
224
|
+
### 경로 파라미터
|
|
225
|
+
|
|
226
|
+
- slug·주문번호·id 등 **경로 파라미터는 라이브러리가 인코딩한다**. 넘기기 전에 `encodeURIComponent`를 직접 씌우지 마라 — 이중 인코딩된다.
|
|
227
|
+
|
|
249
228
|
### 시크릿 키 관련 에러
|
|
250
229
|
|
|
251
230
|
- `401 STOREFRONT_KEY_REQUIRED` — 백엔드 `required` 모드인데 키 없음/무효. `secretKey` 설정 필요.
|
package/dist/index.cjs
CHANGED
|
@@ -62,13 +62,18 @@ var ZalkeraError = class _ZalkeraError extends Error {
|
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
64
|
var STOREFRONT_KEY_MESSAGES = {
|
|
65
|
-
STOREFRONT_KEY_REQUIRED: "\uC2A4\uD1A0\uC5B4\uD504\uB860\uD2B8 \uC2DC\uD06C\uB9BF \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. createZalkeraClient \uC758 secretKey \uC635\uC158\uC744 \uC124\uC815\uD558\uC138\uC694 (\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09 \u2192 \uC11C\uBC84 .env \uC758
|
|
65
|
+
STOREFRONT_KEY_REQUIRED: "\uC2A4\uD1A0\uC5B4\uD504\uB860\uD2B8 \uC2DC\uD06C\uB9BF \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. createZalkeraClient \uC758 secretKey \uC635\uC158\uC744 \uC124\uC815\uD558\uC138\uC694 (\uD30C\uD2B8\uB108 \uCF58\uC194\uC5D0\uC11C \uBC1C\uAE09 \u2192 \uC11C\uBC84 .env \uC758 ZALKERA_STOREFRONT_KEY). \uBE0C\uB77C\uC6B0\uC800 \uBC88\uB4E4\uC5D0 \uB123\uC9C0 \uB9C8\uC138\uC694.",
|
|
66
66
|
TENANT_MISMATCH: "secretKey \uAC00 tenant \uC635\uC158\uACFC \uB2E4\uB978 \uD14C\uB10C\uD2B8\uC758 \uD0A4\uC785\uB2C8\uB2E4. \uB450 \uAC12\uC774 \uAC19\uC740 \uD14C\uB10C\uD2B8\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694."
|
|
67
67
|
};
|
|
68
68
|
function isApiErrorBody(value) {
|
|
69
69
|
return typeof value === "object" && value !== null && "message" in value && typeof value.message === "string";
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// src/pathSegment.ts
|
|
73
|
+
function seg(v) {
|
|
74
|
+
return encodeURIComponent(String(v));
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
// src/client.ts
|
|
73
78
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
74
79
|
function createZalkeraClient(options) {
|
|
@@ -81,6 +86,9 @@ function createZalkeraClient(options) {
|
|
|
81
86
|
}
|
|
82
87
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
83
88
|
async function request(path, init) {
|
|
89
|
+
if (/(^|\/)\.{1,2}(\/|$)/.test(path)) {
|
|
90
|
+
throw new ZalkeraError(`\uACBD\uB85C\uC5D0 \uC0C1\uB300 \uC138\uADF8\uBA3C\uD2B8\uAC00 \uC788\uC2B5\uB2C8\uB2E4: ${path}`, { status: 400, code: "INVALID_PATH" });
|
|
91
|
+
}
|
|
84
92
|
const url = new URL(`${baseUrl}/api${path}`);
|
|
85
93
|
if (init?.query) {
|
|
86
94
|
for (const [key, value] of Object.entries(init.query)) {
|
|
@@ -106,7 +114,14 @@ function createZalkeraClient(options) {
|
|
|
106
114
|
method: init?.method ?? "GET",
|
|
107
115
|
headers,
|
|
108
116
|
body: init?.body != null ? JSON.stringify(init.body) : void 0,
|
|
109
|
-
signal: controller.signal
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
// 리다이렉트를 따라가지 않는다 — **보안 불변식이라 스위치를 두지 않는다**(옵션으로 열면
|
|
119
|
+
// 그 옵션이 곧 사고 경로가 된다). 기본값 `follow` 로 크로스오리진 3xx 를 따라가면
|
|
120
|
+
// `X-Storefront-Key`(테넌트 시크릿)·`X-Tenant`·`X-Cart-Session`·`Idempotency-Key` 가
|
|
121
|
+
// 제3자 오리진에 그대로 전달된다 — undici 는 커스텀 헤더를 벗기지 않는다(실측).
|
|
122
|
+
// `"error"` 를 쓰지 않는 이유: undici 에서 일반 `TypeError: fetch failed` 로 떨어져
|
|
123
|
+
// 아래 네트워크 분기에 흡수되고, 그러면 원인(대개 baseUrl 오배선)이 사라진다.
|
|
124
|
+
redirect: "manual"
|
|
110
125
|
};
|
|
111
126
|
if (init?.next) fetchInit.next = init.next;
|
|
112
127
|
response = await fetchImpl(url.toString(), fetchInit);
|
|
@@ -119,6 +134,14 @@ function createZalkeraClient(options) {
|
|
|
119
134
|
} finally {
|
|
120
135
|
clearTimeout(timer);
|
|
121
136
|
}
|
|
137
|
+
if (isRedirectResponse(response)) {
|
|
138
|
+
response.body?.cancel().catch(() => {
|
|
139
|
+
});
|
|
140
|
+
throw new ZalkeraError(
|
|
141
|
+
`\uBC31\uC5D4\uB4DC\uAC00 \uB9AC\uB2E4\uC774\uB809\uD2B8\uB85C \uC751\uB2F5\uD588\uC2B5\uB2C8\uB2E4 (HTTP ${response.status}). \uBCF4\uC548\uC0C1 \uB530\uB77C\uAC00\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 \u2014 \uB530\uB77C\uAC00\uBA74 \uC694\uCCAD \uD5E4\uB354(\uD14C\uB10C\uD2B8 \uC2DC\uD06C\uB9BF \uD0A4\xB7\uC7A5\uBC14\uAD6C\uB2C8 \uC138\uC158)\uAC00 \uB2E4\uB978 \uC624\uB9AC\uC9C4\uC73C\uB85C \uC0D9\uB2C8\uB2E4. baseUrl \uC774 \uCD5C\uC885 \uC624\uB9AC\uC9C4\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694(http\u2194https \uC2B9\uACA9, www \uC720\uBB34, \uD504\uB85D\uC2DC\uC758 \uACBD\uB85C \uB9AC\uB77C\uC774\uD2B8).`,
|
|
142
|
+
{ status: 502, code: "UPSTREAM_REDIRECT" }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
122
145
|
const text = await response.text();
|
|
123
146
|
const parsed = text ? safeJsonParse(text) : null;
|
|
124
147
|
if (!response.ok) {
|
|
@@ -144,18 +167,18 @@ function createZalkeraClient(options) {
|
|
|
144
167
|
sort: params?.sort
|
|
145
168
|
}
|
|
146
169
|
}),
|
|
147
|
-
getPost: (slug) => request(`/public/posts/${
|
|
148
|
-
recordPostView: (slug, context) => request(`/public/posts/${
|
|
170
|
+
getPost: (slug) => request(`/public/posts/${seg(slug)}`),
|
|
171
|
+
recordPostView: (slug, context) => request(`/public/posts/${seg(slug)}/view`, {
|
|
149
172
|
method: "POST",
|
|
150
173
|
context
|
|
151
174
|
}),
|
|
152
|
-
getPage: (slug, options2) => request(`/public/pages/${
|
|
175
|
+
getPage: (slug, options2) => request(`/public/pages/${seg(slug)}`, nextInit(options2)),
|
|
153
176
|
listPages: (params, options2) => request("/public/pages", {
|
|
154
177
|
query: { page: params?.page, size: params?.size },
|
|
155
178
|
...nextInit(options2)
|
|
156
179
|
}),
|
|
157
180
|
listMenus: (options2) => request("/public/menus", { next: options2?.tags ? { tags: options2.tags } : void 0 }),
|
|
158
|
-
getMediaUrl: (id) => request(`/public/media/${id}/url`),
|
|
181
|
+
getMediaUrl: (id) => request(`/public/media/${seg(id)}/url`),
|
|
159
182
|
submitInquiry: (input, context) => request("/public/inquiries", {
|
|
160
183
|
method: "POST",
|
|
161
184
|
body: input,
|
|
@@ -167,7 +190,7 @@ function createZalkeraClient(options) {
|
|
|
167
190
|
context
|
|
168
191
|
}),
|
|
169
192
|
// ── 커머스: 카탈로그 ────────────────────────────────────
|
|
170
|
-
getProduct: (slug, options2) => request(`/public/products/${
|
|
193
|
+
getProduct: (slug, options2) => request(`/public/products/${seg(slug)}`, nextInit(options2)),
|
|
171
194
|
listProducts: (params, options2) => request("/public/products", {
|
|
172
195
|
query: {
|
|
173
196
|
productType: params?.productType,
|
|
@@ -180,14 +203,14 @@ function createZalkeraClient(options) {
|
|
|
180
203
|
}),
|
|
181
204
|
listProductCategories: (options2) => request("/public/product-categories", nextInit(options2)),
|
|
182
205
|
// ── 커머스: 상품후기(공개) ──────────────────────────────
|
|
183
|
-
listProductReviews: (productId, params, options2) => request(`/public/products/${productId}/reviews`, {
|
|
206
|
+
listProductReviews: (productId, params, options2) => request(`/public/products/${seg(productId)}/reviews`, {
|
|
184
207
|
query: { page: params?.page, size: params?.size },
|
|
185
208
|
next: options2?.tags ? { tags: options2.tags } : void 0
|
|
186
209
|
}),
|
|
187
|
-
getProductReviewSummary: (productId, options2) => request(`/public/products/${productId}/reviews/summary`, {
|
|
210
|
+
getProductReviewSummary: (productId, options2) => request(`/public/products/${seg(productId)}/reviews/summary`, {
|
|
188
211
|
next: options2?.tags ? { tags: options2.tags } : void 0
|
|
189
212
|
}),
|
|
190
|
-
createProductReview: (productId, input, accessToken) => request(`/shop/products/${productId}/reviews`, { method: "POST", body: input, bearer: accessToken }),
|
|
213
|
+
createProductReview: (productId, input, accessToken) => request(`/shop/products/${seg(productId)}/reviews`, { method: "POST", body: input, bearer: accessToken }),
|
|
191
214
|
// ── 커머스: 고객 인증 ───────────────────────────────────
|
|
192
215
|
socialLogin: (input) => request("/shop/auth/social", { method: "POST", body: input }),
|
|
193
216
|
refreshSession: (refreshToken) => request("/shop/auth/refresh", { method: "POST", body: { refreshToken } }),
|
|
@@ -202,11 +225,11 @@ function createZalkeraClient(options) {
|
|
|
202
225
|
}),
|
|
203
226
|
createBooking: (accessToken, input) => request("/shop/booking/bookings", { method: "POST", body: input, bearer: accessToken }),
|
|
204
227
|
myBookings: (accessToken) => request("/shop/booking/bookings", { bearer: accessToken }),
|
|
205
|
-
cancelBooking: (accessToken, bookingCode) => request(`/shop/booking/bookings/${
|
|
228
|
+
cancelBooking: (accessToken, bookingCode) => request(`/shop/booking/bookings/${seg(bookingCode)}/cancel`, {
|
|
206
229
|
method: "POST",
|
|
207
230
|
bearer: accessToken
|
|
208
231
|
}),
|
|
209
|
-
rescheduleBooking: (accessToken, bookingCode, newSlotId) => request(`/shop/booking/bookings/${
|
|
232
|
+
rescheduleBooking: (accessToken, bookingCode, newSlotId) => request(`/shop/booking/bookings/${seg(bookingCode)}/reschedule`, {
|
|
210
233
|
method: "POST",
|
|
211
234
|
body: { newSlotId },
|
|
212
235
|
bearer: accessToken
|
|
@@ -214,33 +237,33 @@ function createZalkeraClient(options) {
|
|
|
214
237
|
// ── 커머스: 장바구니 ────────────────────────────────────
|
|
215
238
|
getCart: (session) => request("/shop/cart", shopInit(session)),
|
|
216
239
|
addToCart: (variantId, quantity, session) => request("/shop/cart/items", shopInit(session, { method: "POST", body: { variantId, quantity } })),
|
|
217
|
-
updateCartItem: (variantId, quantity, session) => request(`/shop/cart/items/${variantId}`, shopInit(session, { method: "PATCH", body: { quantity } })),
|
|
218
|
-
removeFromCart: (variantId, session) => request(`/shop/cart/items/${variantId}`, shopInit(session, { method: "DELETE" })),
|
|
240
|
+
updateCartItem: (variantId, quantity, session) => request(`/shop/cart/items/${seg(variantId)}`, shopInit(session, { method: "PATCH", body: { quantity } })),
|
|
241
|
+
removeFromCart: (variantId, session) => request(`/shop/cart/items/${seg(variantId)}`, shopInit(session, { method: "DELETE" })),
|
|
219
242
|
clearCart: (session) => request("/shop/cart", shopInit(session, { method: "DELETE" })),
|
|
220
243
|
// ── 커머스: 결제·주문·배송 ──────────────────────────────
|
|
221
244
|
checkout: (input, session, idempotencyKey) => request("/shop/checkout", shopInit(session, { method: "POST", body: input, idempotencyKey })),
|
|
222
245
|
confirmPayment: (orderNo, providerParams, access) => request(
|
|
223
|
-
`/shop/orders/${
|
|
246
|
+
`/shop/orders/${seg(orderNo)}/payment/confirm`,
|
|
224
247
|
accessInit(access, { method: "POST", body: providerParams })
|
|
225
248
|
),
|
|
226
249
|
startPayment: (orderNo, access) => request(
|
|
227
|
-
`/shop/orders/${
|
|
250
|
+
`/shop/orders/${seg(orderNo)}/payment/session`,
|
|
228
251
|
accessInit(access, { method: "POST" })
|
|
229
252
|
),
|
|
230
|
-
getOrder: (orderNo, access) => request(`/shop/orders/${
|
|
253
|
+
getOrder: (orderNo, access) => request(`/shop/orders/${seg(orderNo)}`, accessInit(access)),
|
|
231
254
|
listMyOrders: (accessToken, params) => request("/shop/orders", {
|
|
232
255
|
bearer: accessToken,
|
|
233
256
|
query: { page: params?.page, size: params?.size }
|
|
234
257
|
}),
|
|
235
258
|
cancelOrder: (orderNo, access) => request(
|
|
236
|
-
`/shop/orders/${
|
|
259
|
+
`/shop/orders/${seg(orderNo)}/cancel`,
|
|
237
260
|
accessInit(access, { method: "POST" })
|
|
238
261
|
),
|
|
239
262
|
completeOrder: (orderNo, access) => request(
|
|
240
|
-
`/shop/orders/${
|
|
263
|
+
`/shop/orders/${seg(orderNo)}/complete`,
|
|
241
264
|
accessInit(access, { method: "POST" })
|
|
242
265
|
),
|
|
243
|
-
getShipment: (orderNo, access) => request(`/shop/orders/${
|
|
266
|
+
getShipment: (orderNo, access) => request(`/shop/orders/${seg(orderNo)}/shipment`, accessInit(access))
|
|
244
267
|
};
|
|
245
268
|
}
|
|
246
269
|
function nextInit(options) {
|
|
@@ -256,6 +279,9 @@ function accessInit(access, extra) {
|
|
|
256
279
|
query: access.phone ? { phone: access.phone } : void 0
|
|
257
280
|
};
|
|
258
281
|
}
|
|
282
|
+
function isRedirectResponse(response) {
|
|
283
|
+
return response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
|
|
284
|
+
}
|
|
259
285
|
function safeJsonParse(text) {
|
|
260
286
|
try {
|
|
261
287
|
return JSON.parse(text);
|
|
@@ -327,7 +353,7 @@ function asId(value) {
|
|
|
327
353
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
328
354
|
}
|
|
329
355
|
function mediaSrc(assetId) {
|
|
330
|
-
return `/media/${assetId}`;
|
|
356
|
+
return `/media/${seg(assetId)}`;
|
|
331
357
|
}
|
|
332
358
|
|
|
333
359
|
// src/theme.ts
|