@zalkera/client 0.6.1
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/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/index.cjs +247 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +861 -0
- package/dist/index.d.ts +861 -0
- package/dist/index.js +244 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +408 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Credium Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# @zalkera/client
|
|
2
|
+
|
|
3
|
+
잘커라(zalkera) 헤드리스 CMS·커머스 백엔드의 공개 API를 감싼 타입 안전 클라이언트.
|
|
4
|
+
|
|
5
|
+
- 대상: 잘커라를 데이터로 쓰는 **테넌트 사이트**(쇼핑몰·예약·콘텐츠).
|
|
6
|
+
- 성격: 얇은 `fetch` 래퍼 — **런타임 의존성 0**, ESM·CJS + 타입 선언 동봉.
|
|
7
|
+
- 동작: 응답 껍데기(`ApiResponse<T>`)를 벗겨 `data`만 반환, 실패는 `ZalkeraError`로 throw.
|
|
8
|
+
- 전제: **서버 사이드 전용**(RSC·route handler·server action). 브라우저 직접 호출 미지원 ([서버 사이드 전용](#서버-사이드-전용-중요) 참고).
|
|
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
|
+
**환경변수 이름(`ONEQUE_API_BASE`·`ONEQUE_TENANT`·`ONEQUE_STOREFRONT_KEY`)은 그대로다** — 서빙 인프라가
|
|
32
|
+
주입하는 키라 사이트 코드만 바꿔서는 안 된다. 동적 SSR 정당화 마커도 `zalkera-allow-dynamic` 이 새 이름이지만
|
|
33
|
+
기존 `oneque-`/`oneq-` 마커를 계속 수용한다.
|
|
34
|
+
|
|
35
|
+
기능·API 동작은 동일하다 — 이름만 바뀌었다.
|
|
36
|
+
|
|
37
|
+
## 설치
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @zalkera/client
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- 버전 못 박기: tarball을 `vendor/`에 넣어 써도 결과는 npm 설치와 동일.
|
|
44
|
+
- ❌ 디렉터리 심링크(`file:<dir>`) 금지 — Next 16 Turbopack이 프로젝트 밖을 못 읽어 깨짐.
|
|
45
|
+
|
|
46
|
+
## 사용법
|
|
47
|
+
|
|
48
|
+
### 초기화
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// lib/zalkera.ts — 서버 전용 싱글턴
|
|
52
|
+
import {createZalkeraClient} from "@zalkera/client";
|
|
53
|
+
|
|
54
|
+
export const zalkera = createZalkeraClient({
|
|
55
|
+
baseUrl: process.env.ONEQUE_API_BASE!, // 예: http://localhost:8100 — /api는 안 붙인다
|
|
56
|
+
tenant: process.env.ONEQUE_TENANT!, // 모든 요청에 X-Tenant로 실린다
|
|
57
|
+
secretKey: process.env.ONEQUE_STOREFRONT_KEY, // (선택) 서버 시크릿 — "시크릿 키" 참고
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### 옵션 (`ZalkeraClientOptions`)
|
|
62
|
+
|
|
63
|
+
| 옵션 | 타입 | 필수 | 설명 |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `baseUrl` | `string` | ✅ | 백엔드 베이스 URL. `/api`는 붙이지 않는다(클라이언트가 붙임). |
|
|
66
|
+
| `tenant` | `string` | ✅ | 테넌트 코드. 전 요청 `X-Tenant` 헤더. |
|
|
67
|
+
| `secretKey` | `string` | — | 스토어프론트 서버 시크릿(`oqsk_…`). 있으면 전 요청 `X-Storefront-Key` 헤더. |
|
|
68
|
+
| `fetch` | `typeof fetch` | — | `fetch` 구현 주입. 기본 전역 `fetch`(Node 18+). 캐시 래퍼·테스트용. |
|
|
69
|
+
| `headers` | `Record<string,string>` | — | 전 요청에 추가할 헤더. |
|
|
70
|
+
| `timeoutMs` | `number` | — | 요청 타임아웃. 기본 10초. |
|
|
71
|
+
|
|
72
|
+
### 예시
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const posts = await zalkera.listPosts({size: 10, sort: "publishedAt,desc"});
|
|
76
|
+
const post = await zalkera.getPost("hello-world");
|
|
77
|
+
const menus = await zalkera.listMenus();
|
|
78
|
+
const product = await zalkera.getProduct("tee", {tags: ["product:tee"]});
|
|
79
|
+
await zalkera.submitInquiry({name, email, subject, message}, {clientIp});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## AX 도구 (`llms.txt`)
|
|
83
|
+
|
|
84
|
+
- 이 패키지는 AI가 스토어프론트를 짓는 것을 전제로 설계됨.
|
|
85
|
+
- `llms.txt` 한 파일에 API 표면·서버 호출 규칙·결제 흐름·게스트 장바구니·주문 조회 전부 수록.
|
|
86
|
+
- 설치 후 경로: `node_modules/@zalkera/client/llms.txt`.
|
|
87
|
+
- 사용법: 이 파일을 AI 컨텍스트로 주고 "상품·장바구니·결제 페이지를 만들어줘".
|
|
88
|
+
|
|
89
|
+
## API 스펙 (메서드)
|
|
90
|
+
|
|
91
|
+
### 콘텐츠·공개(무인증)
|
|
92
|
+
|
|
93
|
+
| 메서드 | 엔드포인트 | 반환 |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `getSiteConfig(options?)` | `GET /api/public/site-config` | `SiteConfig` |
|
|
96
|
+
| `listCategories()` | `GET /api/public/categories` | `Category[]` |
|
|
97
|
+
| `listPosts(params?)` | `GET /api/public/posts` | `Paginated<PostSummary>` |
|
|
98
|
+
| `getPost(slug)` | `GET /api/public/posts/{slug}` | `PostDetail` |
|
|
99
|
+
| `recordPostView(slug, ctx?)` | `POST /api/public/posts/{slug}/view` | `boolean` |
|
|
100
|
+
| `getPage(slug, options?)` | `GET /api/public/pages/{slug}` | `PageContent` |
|
|
101
|
+
| `listMenus(options?)` | `GET /api/public/menus` | `Menu[]` |
|
|
102
|
+
| `getMediaUrl(id)` | `GET /api/public/media/{id}/url` | `MediaUrl` |
|
|
103
|
+
| `submitInquiry(input, ctx?)` | `POST /api/public/inquiries` | `InquiryCreated` |
|
|
104
|
+
| `submitLead(input, ctx?)` | `POST /api/public/leads` | `LeadCreated` |
|
|
105
|
+
|
|
106
|
+
### 커머스 카탈로그·후기(공개)
|
|
107
|
+
|
|
108
|
+
| 메서드 | 엔드포인트 | 반환 |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `getProduct(slug, options?)` | `GET /api/public/products/{slug}` | `ProductDetail` |
|
|
111
|
+
| `listProducts(params?, options?)` | `GET /api/public/products` | `Paginated<ProductSummary>` |
|
|
112
|
+
| `listProductCategories(options?)` | `GET /api/public/product-categories` | `ProductCategory[]` |
|
|
113
|
+
| `listProductReviews(productId, params?, options?)` | `GET /api/public/products/{id}/reviews` | `Paginated<Review>` |
|
|
114
|
+
| `getProductReviewSummary(productId, options?)` | `GET /api/public/products/{id}/reviews/summary` | `RatingSummary` |
|
|
115
|
+
| `createProductReview(productId, input, accessToken)` | `POST /api/shop/products/{id}/reviews` | `Review` |
|
|
116
|
+
|
|
117
|
+
- 후기 조회는 **숫자 `productId`**(slug 아님) — `getProduct(slug).id`로 획득.
|
|
118
|
+
- 후기 작성은 로그인 필수 + 구매검증(본인·배송완료 이상·상품 일치).
|
|
119
|
+
|
|
120
|
+
### 고객 인증(소셜·토큰)
|
|
121
|
+
|
|
122
|
+
| 메서드 | 엔드포인트 | 반환 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `socialLogin(input)` | `POST /api/shop/auth/social` | `AuthTokens` |
|
|
125
|
+
| `refreshSession(refreshToken)` | `POST /api/shop/auth/refresh` | `AuthTokens` |
|
|
126
|
+
| `logout(accessToken)` | `POST /api/shop/auth/logout` | `string` |
|
|
127
|
+
| `getMe(accessToken)` | `GET /api/shop/me` | `CustomerSummary` |
|
|
128
|
+
| `getConsents(accessToken)` | `GET /api/shop/consents` | `ConsentStatus[]` |
|
|
129
|
+
| `updateConsents(accessToken, consents)` | `POST /api/shop/consents` | `string` |
|
|
130
|
+
|
|
131
|
+
### 예약(SERVICE 상품)
|
|
132
|
+
|
|
133
|
+
| 메서드 | 엔드포인트 | 반환 |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `availability(params, options?)` | `GET /api/public/booking/availability` | `AvailabilitySlot[]` |
|
|
136
|
+
| `createBooking(accessToken, input)` | `POST /api/shop/booking/bookings` | `Booking` |
|
|
137
|
+
| `myBookings(accessToken)` | `GET /api/shop/booking/bookings` | `Booking[]` |
|
|
138
|
+
| `cancelBooking(accessToken, bookingCode)` | `POST /api/shop/booking/bookings/{code}/cancel` | `string` |
|
|
139
|
+
| `rescheduleBooking(accessToken, bookingCode, newSlotId)` | `POST /api/shop/booking/bookings/{code}/reschedule` | `Booking` |
|
|
140
|
+
|
|
141
|
+
- 가용 판정은 `AvailabilitySlot.availableCount`로 (카탈로그 `inStock` 아님 — 슬롯마다 정원 별도).
|
|
142
|
+
- 예약은 **카트에 담기지 않는다**(SERVICE는 `addToCart` 400 거부). 유료 예약은 `Booking.orderNo`로 결제 흐름을 탄다.
|
|
143
|
+
|
|
144
|
+
### 장바구니
|
|
145
|
+
|
|
146
|
+
| 메서드 | 엔드포인트 | 반환 |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `getCart(session)` | `GET /api/shop/cart` | `Cart` |
|
|
149
|
+
| `addToCart(variantId, quantity, session)` | `POST /api/shop/cart/items` | `Cart` |
|
|
150
|
+
| `updateCartItem(variantId, quantity, session)` | `PATCH /api/shop/cart/items/{variantId}` | `Cart` |
|
|
151
|
+
| `removeFromCart(variantId, session)` | `DELETE /api/shop/cart/items/{variantId}` | `Cart` |
|
|
152
|
+
| `clearCart(session)` | `DELETE /api/shop/cart` | `Cart` |
|
|
153
|
+
|
|
154
|
+
- `ShopSession` = 로그인 `{accessToken}` 또는 게스트 `{cartSessionKey}`.
|
|
155
|
+
|
|
156
|
+
### 결제·주문·배송
|
|
157
|
+
|
|
158
|
+
| 메서드 | 엔드포인트 | 반환 |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `checkout(input, session, idempotencyKey?)` | `POST /api/shop/checkout` | `OrderDetail` |
|
|
161
|
+
| `startPayment(orderNo, access)` | `POST /api/shop/orders/{orderNo}/payment/session` | `PaymentSession` |
|
|
162
|
+
| `confirmPayment(orderNo, providerParams, access)` | `POST /api/shop/orders/{orderNo}/payment/confirm` | `void` |
|
|
163
|
+
| `getOrder(orderNo, access)` | `GET /api/shop/orders/{orderNo}` | `OrderDetail` |
|
|
164
|
+
| `listMyOrders(accessToken, params?)` | `GET /api/shop/orders` | `Paginated<OrderSummary>` |
|
|
165
|
+
| `cancelOrder(orderNo, access)` | `POST /api/shop/orders/{orderNo}/cancel` | `OrderDetail` |
|
|
166
|
+
| `completeOrder(orderNo, access)` | `POST /api/shop/orders/{orderNo}/complete` | `OrderDetail` |
|
|
167
|
+
| `getShipment(orderNo, access)` | `GET /api/shop/orders/{orderNo}/shipment` | `ShipmentInfo` |
|
|
168
|
+
|
|
169
|
+
- `OrderAccess` = 로그인 `{accessToken}` 또는 게스트 `{phone}`(주문번호 + 연락처로 소유확인).
|
|
170
|
+
- `checkout`은 스냅샷 동결 + 재고 차감(상태 PENDING_PAYMENT). `idempotencyKey`로 재시도 안전(같은 키·다른 입력 = 409).
|
|
171
|
+
- `confirmPayment`는 위젯형 벤더(토스 등) 승인 확정 — 금액을 넘겨도 서버는 저장 금액으로 PG에 직접 확인.
|
|
172
|
+
|
|
173
|
+
### ISR 캐시 태그 (`ReadOptions`)
|
|
174
|
+
|
|
175
|
+
- 읽기 메서드에 `{tags: [...]}`를 넘기면 그 fetch에 `next.tags`가 실려 백엔드가 `revalidateTag`로 온디맨드 무효화.
|
|
176
|
+
- 태그 컨벤션: `site-config`·`products`·`product:{slug}`.
|
|
177
|
+
- 안 넘기면 세그먼트 기본 캐시만 적용(하위호환).
|
|
178
|
+
|
|
179
|
+
## 서버 사이드 전용 (중요)
|
|
180
|
+
|
|
181
|
+
- 공개 API는 `X-Tenant`를 그대로 믿는다(비인증) → 브라우저 직접 호출 시 `baseUrl` 노출 + CORS 개방 필요.
|
|
182
|
+
- 그래서 **RSC / route handler(BFF) / server action**에서만 호출한다.
|
|
183
|
+
- 사용자별 상태(장바구니·결제)는 route handler로 프록시 (브라우저 ↔ route handler ↔ 잘커라), 토큰은 httpOnly 쿠키.
|
|
184
|
+
- ❌ 클라이언트 컴포넌트에서 `@zalkera/client` import 금지 — `baseUrl`·`secretKey` 노출.
|
|
185
|
+
|
|
186
|
+
### IP 민감 호출은 방문자 IP를 넘긴다
|
|
187
|
+
|
|
188
|
+
- 대상: `submitInquiry`·`submitLead`·`recordPostView`.
|
|
189
|
+
- 이유: 백엔드 IP 레이트리밋·조회 dedup은 `X-Forwarded-For` 첫 홉을 본다. 안 넘기면 테넌트 서버 IP로 뭉쳐 방문자 전원이 429.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// route handler 안
|
|
193
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
|
|
194
|
+
await zalkera.submitInquiry(input, {clientIp: ip});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 시크릿 키 (`secretKey`)
|
|
198
|
+
|
|
199
|
+
- 역할: `X-Tenant` 무인증 신뢰의 **보안 승격** — 키가 테넌트 신원을 증명(백엔드에선 키가 정본).
|
|
200
|
+
- 형식: `oqsk_…`. 파트너 콘솔에서 발급(원문 1회 노출·재조회 불가·분실 시 회전).
|
|
201
|
+
- 하위호환: 안 주면 종전대로 `tenant`(X-Tenant)만으로 동작. 둘 다 보내도 무방(일치해야 함).
|
|
202
|
+
|
|
203
|
+
### 🔒 비밀 취급 규칙
|
|
204
|
+
|
|
205
|
+
- ✅ **서버 `.env`에만** 둔다(예: `ONEQUE_STOREFRONT_KEY`).
|
|
206
|
+
- ❌ `NEXT_PUBLIC_*` 접두사 금지 — 클라이언트 번들에 박힌다.
|
|
207
|
+
- ❌ 클라이언트 컴포넌트 import 금지.
|
|
208
|
+
- 유출 시: 콘솔에서 revoke → 재발급 → 서버 env 교체 배포.
|
|
209
|
+
|
|
210
|
+
## 에러 처리 (`ZalkeraError`)
|
|
211
|
+
|
|
212
|
+
- 실패는 전부 `ZalkeraError` throw. **원인 분기는 `code`로** (같은 status에 여러 원인 — `message`는 사람용 한국어, 문자열 매칭 금지).
|
|
213
|
+
|
|
214
|
+
| 멤버 | 설명 |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `status` | HTTP 상태. 네트워크 실패·비JSON이면 `0`. |
|
|
217
|
+
| `code` | 기계 판독 에러코드(백엔드 `ErrorCode`). 구버전은 HTTP 사유구로 폴백. |
|
|
218
|
+
| `validationErrors` | 400 시 필드별 메시지. |
|
|
219
|
+
| `body` | 파싱된 원본 에러 본문. |
|
|
220
|
+
| `isRateLimited` | `status === 429`. |
|
|
221
|
+
| `isStorefrontKeyError` | `STOREFRONT_KEY_REQUIRED` 또는 `TENANT_MISMATCH`. |
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import {ZalkeraError} from "@zalkera/client";
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await zalkera.submitInquiry(form);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error instanceof ZalkeraError) {
|
|
230
|
+
if (error.isRateLimited) return "잠시 후 다시 시도해 주세요."; // 429
|
|
231
|
+
if (error.isStorefrontKeyError) throw error; // secretKey 오배선 — 서버 설정 점검
|
|
232
|
+
if (error.status === 400) return error.validationErrors; // 필드별 메시지
|
|
233
|
+
if (error.code === "OUT_OF_STOCK") return "품절된 상품이 있습니다.";
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 시크릿 키 관련 에러
|
|
240
|
+
|
|
241
|
+
- `401 STOREFRONT_KEY_REQUIRED` — 백엔드 `required` 모드인데 키 없음/무효. `secretKey` 설정 필요.
|
|
242
|
+
- `403 TENANT_MISMATCH` — `secretKey`가 `tenant`와 다른 테넌트의 키. 두 값 정합 확인.
|
|
243
|
+
- 둘 다 `ZalkeraError.isStorefrontKeyError === true`이며 안내 메시지가 매핑돼 있다.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
잘커라(zalkera) — AX · Headless Commerce · AEO
|
|
248
|
+
|
|
249
|
+
**Speak it, Ship it.** 말하면, 만들어진다.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var OnequeError = class _OnequeError extends Error {
|
|
5
|
+
/** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
|
|
6
|
+
status;
|
|
7
|
+
/**
|
|
8
|
+
* **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).
|
|
9
|
+
* 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.
|
|
10
|
+
*
|
|
11
|
+
* 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, "Conflict")로 **폴백**한다 —
|
|
12
|
+
* 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.
|
|
13
|
+
* 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).
|
|
14
|
+
*/
|
|
15
|
+
code;
|
|
16
|
+
/** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */
|
|
17
|
+
validationErrors;
|
|
18
|
+
/** 파싱된 원본 에러 본문(있으면). */
|
|
19
|
+
body;
|
|
20
|
+
constructor(message, options) {
|
|
21
|
+
super(message, { cause: options.cause });
|
|
22
|
+
this.name = "OnequeError";
|
|
23
|
+
this.status = options.status;
|
|
24
|
+
this.code = options.code ?? null;
|
|
25
|
+
this.validationErrors = options.validationErrors ?? [];
|
|
26
|
+
this.body = options.body ?? null;
|
|
27
|
+
}
|
|
28
|
+
/** IP 레이트리밋(429) — 문의 폼에서 "잠시 후 다시" 를 띄울 때 쓴다. */
|
|
29
|
+
get isRateLimited() {
|
|
30
|
+
return this.status === 429;
|
|
31
|
+
}
|
|
32
|
+
/** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
|
|
33
|
+
static fromBody(status, body) {
|
|
34
|
+
if (isApiErrorBody(body)) {
|
|
35
|
+
return new _OnequeError(body.message, {
|
|
36
|
+
// errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.
|
|
37
|
+
status,
|
|
38
|
+
code: body.errorCode ?? body.error,
|
|
39
|
+
validationErrors: body.errors ?? [],
|
|
40
|
+
body
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const gateway = status === 502 || status === 503 || status === 504;
|
|
44
|
+
return new _OnequeError(
|
|
45
|
+
gateway ? "\uC11C\uBE44\uC2A4\uC5D0 \uC77C\uC2DC\uC801\uC778 \uBB38\uC81C\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694." : `\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (HTTP ${status})`,
|
|
46
|
+
{ status, code: gateway ? "UPSTREAM_UNAVAILABLE" : null }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function isApiErrorBody(value) {
|
|
51
|
+
return typeof value === "object" && value !== null && "message" in value && typeof value.message === "string";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/client.ts
|
|
55
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
56
|
+
function createOnequeClient(options) {
|
|
57
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
58
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
59
|
+
if (typeof fetchImpl !== "function") {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"@oneque/client: \uC804\uC5ED fetch \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. Node 18+ \uB97C \uC4F0\uAC70\uB098 options.fetch \uB97C \uC8FC\uC785\uD558\uC138\uC694."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
65
|
+
async function request(path, init) {
|
|
66
|
+
const url = new URL(`${baseUrl}/api${path}`);
|
|
67
|
+
if (init?.query) {
|
|
68
|
+
for (const [key, value] of Object.entries(init.query)) {
|
|
69
|
+
if (value != null) url.searchParams.set(key, String(value));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const headers = {
|
|
73
|
+
"X-Tenant": options.tenant,
|
|
74
|
+
Accept: "application/json",
|
|
75
|
+
...options.headers
|
|
76
|
+
};
|
|
77
|
+
if (init?.body != null) headers["Content-Type"] = "application/json";
|
|
78
|
+
if (init?.context?.clientIp) headers["X-Forwarded-For"] = init.context.clientIp;
|
|
79
|
+
if (init?.bearer) headers["Authorization"] = `Bearer ${init.bearer}`;
|
|
80
|
+
if (init?.cartSession) headers["X-Cart-Session"] = init.cartSession;
|
|
81
|
+
if (init?.idempotencyKey) headers["Idempotency-Key"] = init.idempotencyKey;
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
84
|
+
let response;
|
|
85
|
+
try {
|
|
86
|
+
const fetchInit = {
|
|
87
|
+
method: init?.method ?? "GET",
|
|
88
|
+
headers,
|
|
89
|
+
body: init?.body != null ? JSON.stringify(init.body) : void 0,
|
|
90
|
+
signal: controller.signal
|
|
91
|
+
};
|
|
92
|
+
if (init?.next) fetchInit.next = init.next;
|
|
93
|
+
response = await fetchImpl(url.toString(), fetchInit);
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
const aborted = cause instanceof Error && cause.name === "AbortError";
|
|
96
|
+
throw new OnequeError(
|
|
97
|
+
aborted ? `\uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (${timeoutMs}ms)` : "\uB124\uD2B8\uC6CC\uD06C \uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.",
|
|
98
|
+
{ status: 0, cause }
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
const parsed = text ? safeJsonParse(text) : null;
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw OnequeError.fromBody(response.status, parsed);
|
|
107
|
+
}
|
|
108
|
+
if (!text) return void 0;
|
|
109
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
110
|
+
throw new OnequeError(
|
|
111
|
+
`\uC11C\uBC84 \uC751\uB2F5 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694. (HTTP ${response.status})`,
|
|
112
|
+
{ status: 502, code: "UPSTREAM_NON_JSON" }
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return parsed.data;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
getSiteConfig: (options2) => request("/public/site-config", nextInit(options2)),
|
|
119
|
+
listCategories: () => request("/public/categories"),
|
|
120
|
+
listPosts: (params) => request("/public/posts", {
|
|
121
|
+
query: {
|
|
122
|
+
category: params?.category,
|
|
123
|
+
page: params?.page,
|
|
124
|
+
size: params?.size,
|
|
125
|
+
sort: params?.sort
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
getPost: (slug) => request(`/public/posts/${encodeURIComponent(slug)}`),
|
|
129
|
+
recordPostView: (slug, context) => request(`/public/posts/${encodeURIComponent(slug)}/view`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
context
|
|
132
|
+
}),
|
|
133
|
+
getPage: (slug, options2) => request(`/public/pages/${encodeURIComponent(slug)}`, nextInit(options2)),
|
|
134
|
+
listMenus: (options2) => request("/public/menus", { next: options2?.tags ? { tags: options2.tags } : void 0 }),
|
|
135
|
+
getMediaUrl: (id) => request(`/public/media/${id}/url`),
|
|
136
|
+
submitInquiry: (input, context) => request("/public/inquiries", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
body: input,
|
|
139
|
+
context
|
|
140
|
+
}),
|
|
141
|
+
submitLead: (input, context) => request("/public/leads", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: input,
|
|
144
|
+
context
|
|
145
|
+
}),
|
|
146
|
+
// ── 커머스: 카탈로그 ────────────────────────────────────
|
|
147
|
+
getProduct: (slug, options2) => request(`/public/products/${encodeURIComponent(slug)}`, nextInit(options2)),
|
|
148
|
+
listProducts: (params, options2) => request("/public/products", {
|
|
149
|
+
query: {
|
|
150
|
+
productType: params?.productType,
|
|
151
|
+
keyword: params?.keyword,
|
|
152
|
+
page: params?.page,
|
|
153
|
+
size: params?.size,
|
|
154
|
+
sort: params?.sort
|
|
155
|
+
},
|
|
156
|
+
...nextInit(options2)
|
|
157
|
+
}),
|
|
158
|
+
listProductCategories: (options2) => request("/public/product-categories", nextInit(options2)),
|
|
159
|
+
// ── 커머스: 상품후기(공개) ──────────────────────────────
|
|
160
|
+
listProductReviews: (productId, params, options2) => request(`/public/products/${productId}/reviews`, {
|
|
161
|
+
query: { page: params?.page, size: params?.size },
|
|
162
|
+
next: options2?.tags ? { tags: options2.tags } : void 0
|
|
163
|
+
}),
|
|
164
|
+
getProductReviewSummary: (productId, options2) => request(`/public/products/${productId}/reviews/summary`, {
|
|
165
|
+
next: options2?.tags ? { tags: options2.tags } : void 0
|
|
166
|
+
}),
|
|
167
|
+
createProductReview: (productId, input, accessToken) => request(`/shop/products/${productId}/reviews`, { method: "POST", body: input, bearer: accessToken }),
|
|
168
|
+
// ── 커머스: 고객 인증 ───────────────────────────────────
|
|
169
|
+
socialLogin: (input) => request("/shop/auth/social", { method: "POST", body: input }),
|
|
170
|
+
refreshSession: (refreshToken) => request("/shop/auth/refresh", { method: "POST", body: { refreshToken } }),
|
|
171
|
+
logout: (accessToken) => request("/shop/auth/logout", { method: "POST", bearer: accessToken }),
|
|
172
|
+
getMe: (accessToken) => request("/shop/me", { bearer: accessToken }),
|
|
173
|
+
getConsents: (accessToken) => request("/shop/consents", { bearer: accessToken }),
|
|
174
|
+
updateConsents: (accessToken, consents) => request("/shop/consents", { method: "POST", bearer: accessToken, body: { consents } }),
|
|
175
|
+
// ── 예약(SERVICE 상품) ───────────────────────────────────
|
|
176
|
+
availability: (params, options2) => request("/public/booking/availability", {
|
|
177
|
+
query: { productId: params.productId, from: params.from, to: params.to },
|
|
178
|
+
next: options2?.tags ? { tags: options2.tags } : void 0
|
|
179
|
+
}),
|
|
180
|
+
createBooking: (accessToken, input) => request("/shop/booking/bookings", { method: "POST", body: input, bearer: accessToken }),
|
|
181
|
+
myBookings: (accessToken) => request("/shop/booking/bookings", { bearer: accessToken }),
|
|
182
|
+
cancelBooking: (accessToken, bookingCode) => request(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/cancel`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
bearer: accessToken
|
|
185
|
+
}),
|
|
186
|
+
rescheduleBooking: (accessToken, bookingCode, newSlotId) => request(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/reschedule`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
body: { newSlotId },
|
|
189
|
+
bearer: accessToken
|
|
190
|
+
}),
|
|
191
|
+
// ── 커머스: 장바구니 ────────────────────────────────────
|
|
192
|
+
getCart: (session) => request("/shop/cart", shopInit(session)),
|
|
193
|
+
addToCart: (variantId, quantity, session) => request("/shop/cart/items", shopInit(session, { method: "POST", body: { variantId, quantity } })),
|
|
194
|
+
updateCartItem: (variantId, quantity, session) => request(`/shop/cart/items/${variantId}`, shopInit(session, { method: "PATCH", body: { quantity } })),
|
|
195
|
+
removeFromCart: (variantId, session) => request(`/shop/cart/items/${variantId}`, shopInit(session, { method: "DELETE" })),
|
|
196
|
+
clearCart: (session) => request("/shop/cart", shopInit(session, { method: "DELETE" })),
|
|
197
|
+
// ── 커머스: 결제·주문·배송 ──────────────────────────────
|
|
198
|
+
checkout: (input, session, idempotencyKey) => request("/shop/checkout", shopInit(session, { method: "POST", body: input, idempotencyKey })),
|
|
199
|
+
confirmPayment: (orderNo, providerParams, access) => request(
|
|
200
|
+
`/shop/orders/${encodeURIComponent(orderNo)}/payment/confirm`,
|
|
201
|
+
accessInit(access, { method: "POST", body: providerParams })
|
|
202
|
+
),
|
|
203
|
+
startPayment: (orderNo, access) => request(
|
|
204
|
+
`/shop/orders/${encodeURIComponent(orderNo)}/payment/session`,
|
|
205
|
+
accessInit(access, { method: "POST" })
|
|
206
|
+
),
|
|
207
|
+
getOrder: (orderNo, access) => request(`/shop/orders/${encodeURIComponent(orderNo)}`, accessInit(access)),
|
|
208
|
+
listMyOrders: (accessToken, params) => request("/shop/orders", {
|
|
209
|
+
bearer: accessToken,
|
|
210
|
+
query: { page: params?.page, size: params?.size }
|
|
211
|
+
}),
|
|
212
|
+
cancelOrder: (orderNo, access) => request(
|
|
213
|
+
`/shop/orders/${encodeURIComponent(orderNo)}/cancel`,
|
|
214
|
+
accessInit(access, { method: "POST" })
|
|
215
|
+
),
|
|
216
|
+
completeOrder: (orderNo, access) => request(
|
|
217
|
+
`/shop/orders/${encodeURIComponent(orderNo)}/complete`,
|
|
218
|
+
accessInit(access, { method: "POST" })
|
|
219
|
+
),
|
|
220
|
+
getShipment: (orderNo, access) => request(`/shop/orders/${encodeURIComponent(orderNo)}/shipment`, accessInit(access))
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function nextInit(options) {
|
|
224
|
+
return options?.tags && options.tags.length > 0 ? { next: { tags: options.tags } } : {};
|
|
225
|
+
}
|
|
226
|
+
function shopInit(session, extra) {
|
|
227
|
+
return { ...extra, bearer: session.accessToken, cartSession: session.cartSessionKey };
|
|
228
|
+
}
|
|
229
|
+
function accessInit(access, extra) {
|
|
230
|
+
return {
|
|
231
|
+
...extra,
|
|
232
|
+
bearer: access.accessToken,
|
|
233
|
+
query: access.phone ? { phone: access.phone } : void 0
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function safeJsonParse(text) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(text);
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
exports.OnequeError = OnequeError;
|
|
245
|
+
exports.createOnequeClient = createOnequeClient;
|
|
246
|
+
//# sourceMappingURL=index.cjs.map
|
|
247
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":["options"],"mappings":"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","file":"index.cjs","sourcesContent":["import type {ApiErrorBody, ValidationError} from \"./types\";\n\n/**\n * Oneque API 호출 실패.\n *\n * 백엔드의 `ErrorResponse` 를 그대로 담는다.\n *\n * **원인 분기는 [code] 로 한다** — 한 엔드포인트가 같은 상태코드로 여러 원인을 낸다(checkout 의 409 =\n * `OUT_OF_STOCK`·`ITEM_NOT_PURCHASABLE`·`IDEMPOTENCY_CONFLICT` — 처방이 전부 다르다). `message` 는\n * 사람용 한국어라 **문자열 매칭 금지**.\n *\n * `status` 는 거친 분류에만:\n * - `400` + [validationErrors] — 입력값 문제(폼 필드별 메시지 노출)\n * - `404` — 없는 slug/리소스\n * - `429` — IP 레이트리밋(문의 남발). [isRateLimited] 로 편히 판별\n * - `5xx` — 서버 오류\n *\n * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.\n */\nexport class OnequeError extends Error {\n /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */\n readonly status: number;\n /**\n * **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).\n * 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.\n *\n * 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, \"Conflict\")로 **폴백**한다 —\n * 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.\n * 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).\n */\n readonly code: string | null;\n /** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */\n readonly validationErrors: ValidationError[];\n /** 파싱된 원본 에러 본문(있으면). */\n readonly body: ApiErrorBody | null;\n\n constructor(\n message: string,\n options: {\n status: number;\n code?: string | null;\n validationErrors?: ValidationError[];\n body?: ApiErrorBody | null;\n cause?: unknown;\n },\n ) {\n super(message, {cause: options.cause});\n this.name = \"OnequeError\";\n this.status = options.status;\n this.code = options.code ?? null;\n this.validationErrors = options.validationErrors ?? [];\n this.body = options.body ?? null;\n }\n\n /** IP 레이트리밋(429) — 문의 폼에서 \"잠시 후 다시\" 를 띄울 때 쓴다. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n\n /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */\n static fromBody(status: number, body: unknown): OnequeError {\n if (isApiErrorBody(body)) {\n return new OnequeError(body.message, {\n // errorCode(기계 판독)를 우선, 없으면 error(HTTP 사유구)로 폴백 — 구버전 백엔드 안전.\n status,\n code: body.errorCode ?? body.error,\n validationErrors: body.errors ?? [],\n body,\n });\n }\n // 비JSON 에러 바디(게이트웨이 HTML·빈 바디). 502/503/504 는 상류 장애로 안내 메시지 + 코드 부여.\n const gateway = status === 502 || status === 503 || status === 504;\n return new OnequeError(\n gateway ? \"서비스에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.\" : `요청이 실패했습니다 (HTTP ${status})`,\n {status, code: gateway ? \"UPSTREAM_UNAVAILABLE\" : null},\n );\n }\n}\n\nfunction isApiErrorBody(value: unknown): value is ApiErrorBody {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"message\" in value &&\n typeof (value as {message: unknown}).message === \"string\"\n );\n}\n","import {OnequeError} from \"./errors\";\nimport type {\n ApiResponse,\n AuthTokens,\n AvailabilityParams,\n AvailabilitySlot,\n Booking,\n Cart,\n Category,\n CheckoutInput,\n CreateBookingInput,\n CreateReviewInput,\n ConsentInput,\n ConsentStatus,\n CustomerSummary,\n InquiryCreated,\n InquiryInput,\n LeadCreated,\n LeadInput,\n ListPostsParams,\n ListReviewsParams,\n MediaUrl,\n Menu,\n OrderDetail,\n OrderSummary,\n PageContent,\n ListProductsParams,\n Paginated,\n PaymentSession,\n PostDetail,\n PostSummary,\n ProductCategory,\n ProductDetail,\n ProductSummary,\n RatingSummary,\n Review,\n ShipmentInfo,\n SiteConfig,\n SocialLoginInput,\n} from \"./types\";\n\n/**\n * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면\n * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.\n * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).\n * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.\n */\nexport interface ReadOptions {\n /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */\n tags?: string[];\n}\n\nexport interface OnequeClientOptions {\n /**\n * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.\n * 클라이언트가 경로에 `/api/public/...` 를 붙인다.\n */\n baseUrl: string;\n /**\n * 테넌트 코드. 모든 요청에 `X-Tenant` 헤더로 실린다.\n *\n * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**\n * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면\n * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.\n */\n tenant: string;\n /**\n * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·\n * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.\n */\n fetch?: typeof fetch;\n /** 모든 요청에 추가할 헤더(선택). */\n headers?: Record<string, string>;\n /** 요청 타임아웃(ms). 기본 10초. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Oneque 공개 API 클라이언트.\n *\n * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.\n * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.\n *\n * ```ts\n * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: \"credium\" });\n * const posts = await cms.listPosts({ size: 10, sort: \"publishedAt,desc\" });\n * await cms.submitInquiry({ name, email, subject, message });\n * ```\n *\n * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.\n */\nexport interface OnequeClient {\n /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */\n getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;\n /** 살아 있는 카테고리 전부(페이징 없음). */\n listCategories(): Promise<Category[]>;\n /** 발행된 글 목록(페이징). */\n listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;\n /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */\n getPost(slug: string): Promise<PostDetail>;\n /**\n * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).\n * 화면 렌더를 막지 않도록 fire-and-forget 으로 부르길 권한다(실패해도 페이지는 살아야 한다).\n *\n * 조회 dedup 은 뷰어 IP·UA 를 쓴다. 서버 사이드에서 부를 때는 [RequestContext.clientIp] 로\n * 원 방문자 IP 를 넘겨야 방문자별로 집계된다 — 안 넘기면 전부 서버 IP 하나로 뭉친다.\n */\n recordPostView(slug: string, context?: RequestContext): Promise<boolean>;\n /** slug 로 고정 페이지. */\n /**\n * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].\n * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만\n * 콕 집어 revalidate 한다.\n */\n getPage(slug: string, options?: ReadOptions): Promise<PageContent>;\n /** 메뉴 트리(HEADER·FOOTER). */\n listMenus(options?: ReadOptions): Promise<Menu[]>;\n /** 미디어 presigned 다운로드 URL(만료 있음). */\n getMediaUrl(id: number): Promise<MediaUrl>;\n /**\n * 문의 접수. 성공 시 생성된 문의 id. 레이트리밋이면 429 → `error.isRateLimited`.\n *\n * ⚠️ 서버 사이드(테넌트 route handler)에서 부를 때는 [RequestContext.clientIp] 로 **원 방문자\n * IP 를 반드시 넘겨야 한다.** 백엔드의 문의 레이트리밋·IP 기록은 `X-Forwarded-For` 첫 홉을\n * 쓰는데, 안 넘기면 백엔드가 테넌트 서버 IP 하나만 보고 몇 건 뒤 **모든 방문자**를 429 로 막는다.\n */\n submitInquiry(input: InquiryInput, context?: RequestContext): Promise<InquiryCreated>;\n /**\n * 광고 리드 접수. 문의와 달리 이메일이 선택이고 UTM 추적을 함께 보낸다.\n *\n * ⚠️ [submitInquiry] 와 같은 이유로 서버 사이드에서는 [RequestContext.clientIp] 로 원 방문자\n * IP 를 넘겨야 한다 — 백엔드 리드 레이트리밋·IP 기록이 그 값을 본다.\n */\n submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;\n\n // ── 커머스: 카탈로그(공개) ──────────────────────────────────\n /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */\n getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;\n /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */\n listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;\n /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */\n listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────────\n /**\n * 상품 후기 목록(공개·VISIBLE 만, 최신순, 페이징).\n *\n * 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `getProduct(slug).id` 로 획득한다(0.4.0).\n */\n listProductReviews(productId: number, params?: ListReviewsParams, options?: ReadOptions): Promise<Paginated<Review>>;\n /** 상품 평점 요약(후기 수·평균 별점). 후기 없으면 count=0·avg=0. 목록 별점 배지에 값싸게 쓴다. */\n getProductReviewSummary(productId: number, options?: ReadOptions): Promise<RatingSummary>;\n /**\n * 후기 작성 — **로그인 필수 + 구매검증**. `input.orderItemId` 는 [OrderItemLine.id](주문 상세에서 획득).\n * 서버가 본인 주문·배송완료(DELIVERED) 이상·상품 일치를 3중 확인한다. 라인당 1개(재작성 시 409 `ALREADY_REVIEWED`).\n */\n createProductReview(productId: number, input: CreateReviewInput, accessToken: string): Promise<Review>;\n\n // ── 커머스: 고객 인증 ───────────────────────────────────────\n /** 소셜 로그인 — authorization code 를 백엔드가 교환하고 고객 토큰을 발급. */\n socialLogin(input: SocialLoginInput): Promise<AuthTokens>;\n /** refresh 토큰으로 새 토큰 회전(옛 refresh 는 폐기). */\n refreshSession(refreshToken: string): Promise<AuthTokens>;\n /** 로그아웃 — 이 고객의 모든 세션 폐기. */\n logout(accessToken: string): Promise<string>;\n /** 내 정보. */\n getMe(accessToken: string): Promise<CustomerSummary>;\n /** 현재 동의 상태 조회(로그인 필수) — 마케팅 수신 토글 화면 등에서 초기값으로 쓴다. */\n getConsents(accessToken: string): Promise<ConsentStatus[]>;\n /**\n * 동의 갱신(로그인 필수, append) — 마케팅 수신 on/off 등. 넘긴 항목만 반영되고 나머지는 유지된다.\n * 성공 시 백엔드 확인 메시지를 돌려준다(최신 상태가 필요하면 [getConsents] 로 다시 읽는다).\n */\n updateConsents(accessToken: string, consents: ConsentInput[]): Promise<string>;\n\n // ── 예약(SERVICE 상품) ──────────────────────────────────────\n /**\n * 예약 가능한 슬롯 조회 — **비로그인 가능**(상품 상세에서 시간표를 보여주는 자리).\n *\n * 미래·잔여정원 있는 OPEN 슬롯만 온다. 가용 판정은 [AvailabilitySlot.availableCount] 로 하고,\n * 카탈로그의 `inStock`/`available` 을 쓰지 마라 — 예약은 슬롯마다 정원이 따로다.\n */\n availability(params: AvailabilityParams, options?: ReadOptions): Promise<AvailabilitySlot[]>;\n /**\n * 예약 생성 — 슬롯 정원을 원자적으로 소비한다. **로그인 필수**(게스트 예약 없음).\n *\n * 무료 예약은 즉시 `CONFIRMED`. **유료·예약금이면 `status=PENDING` + [Booking.orderNo] 가 실려 오고**,\n * 그 orderNo 로 [startPayment] 를 불러 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).\n *\n * 정원 소진은 `SLOT_FULL` — 슬롯 목록이 stale 했다는 뜻이니 [availability] 를 다시 읽어 보여줘라.\n */\n createBooking(accessToken: string, input: CreateBookingInput): Promise<Booking>;\n /** 내 예약 목록(최신순). 로그인 필수. */\n myBookings(accessToken: string): Promise<Booking[]>;\n /** 예약 취소 — 시작 전·무료 예약만. 정원이 복원된다. 로그인 필수. */\n cancelBooking(accessToken: string, bookingCode: string): Promise<string>;\n /** 같은 상품의 다른 슬롯으로 이동(신 슬롯 소비·구 슬롯 복원). 로그인 필수. */\n rescheduleBooking(accessToken: string, bookingCode: string, newSlotId: number): Promise<Booking>;\n\n // ── 커머스: 장바구니 ────────────────────────────────────────\n /**\n * 장바구니 조회. 로그인은 accessToken, 게스트는 cartSessionKey 로 식별.\n *\n * ⚠️ **예약(SERVICE) 상품은 장바구니에 담기지 않는다** — [addToCart] 가 400 으로 거부한다.\n * 예약은 [createBooking] 으로 별도 흐름을 탄다(카트에 슬롯을 실으면 체크아웃이 오염된다).\n */\n getCart(session: ShopSession): Promise<Cart>;\n /**\n * 담기(같은 variant 는 수량 누적).\n *\n * ⚠️ **예약(SERVICE) 상품은 400 으로 거부된다** — 예약은 [createBooking] 이다. 상품 상세에서\n * `productType === \"SERVICE\"` 면 담기 버튼 대신 슬롯 선택을 그려라.\n */\n addToCart(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 수량 변경. */\n updateCartItem(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;\n /** 항목 삭제. */\n removeFromCart(variantId: number, session: ShopSession): Promise<Cart>;\n /** 비우기. */\n clearCart(session: ShopSession): Promise<Cart>;\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────────\n /**\n * 결제(주문 생성) — 스냅샷 동결 + 재고 차감. 상태=PENDING_PAYMENT.\n *\n * [idempotencyKey] 를 주면 **같은 키·같은 입력의 재요청이 새 주문을 만들지 않고 원주문을 그대로\n * 반환**한다(더블클릭·네트워크 재시도 안전). 같은 키로 **다른 입력**을 보내면 409.\n *\n * **키는 \"이 체크아웃 1건\"에 고정된 값이어야 한다** — 호출마다 새로 만들면 재시도가 서로 다른\n * 키를 들고 가서 아무것도 막지 못한다(그래서 SDK 가 자동 생성해 주지 않는다). 안정적인 소스를\n * 쓴다: 게스트는 **장바구니 세션 키**(카트 1개 → 주문 1건이라 의미가 정확히 맞는다),\n * 그 외에는 결제 시도 시작 시 한 번 만들어 재시도 동안 보관한 값.\n *\n * 안 주면 종전 동작 — 중복 제출은 카트가 이미 소비돼 404(`CART_NOT_FOUND`)로 떨어진다.\n */\n checkout(input: CheckoutInput, session: ShopSession, idempotencyKey?: string): Promise<OrderDetail>;\n /**\n * 결제 시작. **벤더에 따라 두 갈래**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):\n * `session.widget` 이 있으면 위젯형(내 사이트에서 결제창 → [confirmPayment] 로 승인 확정),\n * 없으면 리다이렉트형(`session.paymentUrl` 로 보내면 끝).\n */\n startPayment(orderNo: string, access: OrderAccess): Promise<PaymentSession>;\n /**\n * **위젯형 벤더의 승인 확정**(토스 등). 결제창이 성공 URL 로 돌려준 파라미터를 그대로 넘긴다\n * (토스: `{paymentKey, orderId, amount}` — URL 쿼리에서 읽어 통째로).\n *\n * **금액을 넘겨도 서버는 안 믿는다** — 서버가 저장한 주문 금액으로 PG 에 직접 묻는다. 그래서\n * 브라우저에서 금액을 조작해도 승인되지 않는다.\n *\n * 이미 승인된 주문(웹훅 선착)이면 조용히 통과한다 — 재호출이 안전하다.\n * 리다이렉트형 벤더에 부르면 404(`PG_CONFIG_NOT_FOUND`).\n */\n confirmPayment(orderNo: string, providerParams: Record<string, string>, access: OrderAccess): Promise<void>;\n /** 주문 조회 — 본인(accessToken) 또는 게스트(phone). */\n getOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 내 주문 목록(로그인 필수). */\n listMyOrders(accessToken: string, params?: {page?: number; size?: number}): Promise<Paginated<OrderSummary>>;\n /** 주문 취소(미결제만) — 본인 또는 게스트(phone). */\n cancelOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 구매 확정(배송완료 후) — 본인 또는 게스트(phone). */\n completeOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;\n /** 배송 조회 — 본인 또는 게스트(phone). */\n getShipment(orderNo: string, access: OrderAccess): Promise<ShipmentInfo>;\n}\n\n/**\n * 장바구니·결제의 사용자 식별. 로그인 고객은 [accessToken], 게스트는 [cartSessionKey]\n * (브라우저가 만든 안정적 익명 키)를 넘긴다. 로그인 시 게스트 카트→고객 카트 병합은 백엔드 몫.\n */\nexport interface ShopSession {\n accessToken?: string;\n cartSessionKey?: string;\n}\n\n/**\n * 주문 조회·취소·배송조회의 접근 크리덴셜. 로그인 고객은 [accessToken] 으로 소유권을,\n * 게스트는 주문 시 남긴 [phone] 으로 확인한다(주문번호 + 연락처).\n */\nexport interface OrderAccess {\n accessToken?: string;\n phone?: string;\n}\n\n/**\n * IP 민감 엔드포인트(문의·조회 비콘)에서 원 방문자를 백엔드에 알리는 컨텍스트.\n *\n * 테넌트 사이트는 서버 사이드에서 이 클라이언트를 부르므로, 백엔드가 보는 소스 IP 는 방문자가\n * 아니라 테넌트 서버다. 방문자 IP 를 `X-Forwarded-For` 로 실어 백엔드가 방문자별로 판단하게 한다.\n */\nexport interface RequestContext {\n /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */\n clientIp?: string;\n}\n\nexport function createOnequeClient(options: OnequeClientOptions): OnequeClient {\n const baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new Error(\n \"@oneque/client: 전역 fetch 가 없습니다. Node 18+ 를 쓰거나 options.fetch 를 주입하세요.\",\n );\n }\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n async function request<T>(\n path: string,\n init?: {\n method?: string;\n body?: unknown;\n query?: Record<string, unknown>;\n context?: RequestContext;\n /** 고객 access 토큰 → Authorization: Bearer. */\n bearer?: string;\n /** 게스트 장바구니 키 → X-Cart-Session. */\n cartSession?: string;\n /** 멱등키 → Idempotency-Key. 같은 키·같은 본문의 재요청은 원주문을 재생한다. */\n idempotencyKey?: string;\n /** Next.js ISR 캐시 옵션 — RSC/ISR fetch 에 `next.tags` 를 실어 온디맨드 revalidate 대상으로 만든다. */\n next?: {tags?: string[]};\n },\n ): Promise<T> {\n const url = new URL(`${baseUrl}/api${path}`);\n if (init?.query) {\n for (const [key, value] of Object.entries(init.query)) {\n if (value != null) url.searchParams.set(key, String(value));\n }\n }\n\n const headers: Record<string, string> = {\n \"X-Tenant\": options.tenant,\n Accept: \"application/json\",\n ...options.headers,\n };\n if (init?.body != null) headers[\"Content-Type\"] = \"application/json\";\n // 원 방문자 IP 를 백엔드에 전달 — 문의 레이트리밋·조회 dedup·IP 기록이 이걸 본다.\n if (init?.context?.clientIp) headers[\"X-Forwarded-For\"] = init.context.clientIp;\n // 커머스 인증 — 로그인 고객은 Bearer, 게스트 장바구니는 세션 키.\n if (init?.bearer) headers[\"Authorization\"] = `Bearer ${init.bearer}`;\n if (init?.cartSession) headers[\"X-Cart-Session\"] = init.cartSession;\n if (init?.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n\n // 타임아웃 — 행 걸린 연결이 페이지 렌더를 무한정 붙들지 않게.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n // Next.js 는 표준 RequestInit 에 비표준 `next`(캐시 태그/주기)를 확장한다. 표준 DOM 타입엔\n // 없으므로 교차 타입으로 선언해 (있을 때만) 실어 보낸다 — 일반 fetch 는 이 필드를 무시한다.\n const fetchInit: RequestInit & {next?: {tags?: string[]}} = {\n method: init?.method ?? \"GET\",\n headers,\n body: init?.body != null ? JSON.stringify(init.body) : undefined,\n signal: controller.signal,\n };\n if (init?.next) fetchInit.next = init.next;\n response = await fetchImpl(url.toString(), fetchInit);\n } catch (cause) {\n const aborted = cause instanceof Error && cause.name === \"AbortError\";\n throw new OnequeError(\n aborted ? `요청 시간이 초과되었습니다 (${timeoutMs}ms)` : \"네트워크 요청이 실패했습니다.\",\n {status: 0, cause},\n );\n } finally {\n clearTimeout(timer);\n }\n\n // 204 등 본문 없는 응답은 파싱하지 않는다.\n const text = await response.text();\n const parsed = text ? safeJsonParse(text) : null;\n\n if (!response.ok) {\n throw OnequeError.fromBody(response.status, parsed);\n }\n // 빈 바디(204·void 엔드포인트 예: confirmPayment)는 정상 — data 접근 없이 통과.\n if (!text) return undefined as T;\n // 성공(2xx)인데 envelope 가 아니면(HTML·비객체 JSON) 게이트웨이/프록시 오염이다.\n // (null).data TypeError 로 새지 않게 구조화 에러로 변환 — BFF errorResponse 가 502 로 서빙(memo 57 §11-4).\n if (parsed === null || typeof parsed !== \"object\") {\n throw new OnequeError(\n `서버 응답 형식이 올바르지 않습니다. 잠시 후 다시 시도해주세요. (HTTP ${response.status})`,\n {status: 502, code: \"UPSTREAM_NON_JSON\"},\n );\n }\n return (parsed as ApiResponse<T>).data;\n }\n\n return {\n getSiteConfig: (options) => request<SiteConfig>(\"/public/site-config\", nextInit(options)),\n listCategories: () => request<Category[]>(\"/public/categories\"),\n listPosts: (params) =>\n request<Paginated<PostSummary>>(\"/public/posts\", {\n query: {\n category: params?.category,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n }),\n getPost: (slug) => request<PostDetail>(`/public/posts/${encodeURIComponent(slug)}`),\n recordPostView: (slug, context) =>\n request<boolean>(`/public/posts/${encodeURIComponent(slug)}/view`, {\n method: \"POST\",\n context,\n }),\n getPage: (slug, options) =>\n request<PageContent>(`/public/pages/${encodeURIComponent(slug)}`, nextInit(options)),\n listMenus: (options) =>\n request<Menu[]>(\"/public/menus\", {next: options?.tags ? {tags: options.tags} : undefined}),\n getMediaUrl: (id) => request<MediaUrl>(`/public/media/${id}/url`),\n submitInquiry: (input, context) =>\n request<InquiryCreated>(\"/public/inquiries\", {\n method: \"POST\",\n body: input,\n context,\n }),\n submitLead: (input, context) =>\n request<LeadCreated>(\"/public/leads\", {\n method: \"POST\",\n body: input,\n context,\n }),\n\n // ── 커머스: 카탈로그 ────────────────────────────────────\n getProduct: (slug, options) =>\n request<ProductDetail>(`/public/products/${encodeURIComponent(slug)}`, nextInit(options)),\n listProducts: (params, options) =>\n request<Paginated<ProductSummary>>(\"/public/products\", {\n query: {\n productType: params?.productType,\n keyword: params?.keyword,\n page: params?.page,\n size: params?.size,\n sort: params?.sort,\n },\n ...nextInit(options),\n }),\n listProductCategories: (options) =>\n request<ProductCategory[]>(\"/public/product-categories\", nextInit(options)),\n\n // ── 커머스: 상품후기(공개) ──────────────────────────────\n listProductReviews: (productId, params, options) =>\n request<Paginated<Review>>(`/public/products/${productId}/reviews`, {\n query: {page: params?.page, size: params?.size},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n getProductReviewSummary: (productId, options) =>\n request<RatingSummary>(`/public/products/${productId}/reviews/summary`, {\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createProductReview: (productId, input, accessToken) =>\n request<Review>(`/shop/products/${productId}/reviews`, {method: \"POST\", body: input, bearer: accessToken}),\n\n // ── 커머스: 고객 인증 ───────────────────────────────────\n socialLogin: (input) => request<AuthTokens>(\"/shop/auth/social\", {method: \"POST\", body: input}),\n refreshSession: (refreshToken) =>\n request<AuthTokens>(\"/shop/auth/refresh\", {method: \"POST\", body: {refreshToken}}),\n logout: (accessToken) => request<string>(\"/shop/auth/logout\", {method: \"POST\", bearer: accessToken}),\n getMe: (accessToken) => request<CustomerSummary>(\"/shop/me\", {bearer: accessToken}),\n getConsents: (accessToken) => request<ConsentStatus[]>(\"/shop/consents\", {bearer: accessToken}),\n updateConsents: (accessToken, consents) =>\n request<string>(\"/shop/consents\", {method: \"POST\", bearer: accessToken, body: {consents}}),\n\n // ── 예약(SERVICE 상품) ───────────────────────────────────\n availability: (params, options) =>\n request<AvailabilitySlot[]>(\"/public/booking/availability\", {\n query: {productId: params.productId, from: params.from, to: params.to},\n next: options?.tags ? {tags: options.tags} : undefined,\n }),\n createBooking: (accessToken, input) =>\n request<Booking>(\"/shop/booking/bookings\", {method: \"POST\", body: input, bearer: accessToken}),\n myBookings: (accessToken) => request<Booking[]>(\"/shop/booking/bookings\", {bearer: accessToken}),\n cancelBooking: (accessToken, bookingCode) =>\n request<string>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/cancel`, {\n method: \"POST\",\n bearer: accessToken,\n }),\n rescheduleBooking: (accessToken, bookingCode, newSlotId) =>\n request<Booking>(`/shop/booking/bookings/${encodeURIComponent(bookingCode)}/reschedule`, {\n method: \"POST\",\n body: {newSlotId},\n bearer: accessToken,\n }),\n\n // ── 커머스: 장바구니 ────────────────────────────────────\n getCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session)),\n addToCart: (variantId, quantity, session) =>\n request<Cart>(\"/shop/cart/items\", shopInit(session, {method: \"POST\", body: {variantId, quantity}})),\n updateCartItem: (variantId, quantity, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"PATCH\", body: {quantity}})),\n removeFromCart: (variantId, session) =>\n request<Cart>(`/shop/cart/items/${variantId}`, shopInit(session, {method: \"DELETE\"})),\n clearCart: (session) => request<Cart>(\"/shop/cart\", shopInit(session, {method: \"DELETE\"})),\n\n // ── 커머스: 결제·주문·배송 ──────────────────────────────\n checkout: (input, session, idempotencyKey) =>\n request<OrderDetail>(\"/shop/checkout\", shopInit(session, {method: \"POST\", body: input, idempotencyKey})),\n confirmPayment: (orderNo, providerParams, access) =>\n request<void>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/confirm`,\n accessInit(access, {method: \"POST\", body: providerParams}),\n ),\n startPayment: (orderNo, access) =>\n request<PaymentSession>(\n `/shop/orders/${encodeURIComponent(orderNo)}/payment/session`,\n accessInit(access, {method: \"POST\"}),\n ),\n getOrder: (orderNo, access) =>\n request<OrderDetail>(`/shop/orders/${encodeURIComponent(orderNo)}`, accessInit(access)),\n listMyOrders: (accessToken, params) =>\n request<Paginated<OrderSummary>>(\"/shop/orders\", {\n bearer: accessToken,\n query: {page: params?.page, size: params?.size},\n }),\n cancelOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/cancel`,\n accessInit(access, {method: \"POST\"}),\n ),\n completeOrder: (orderNo, access) =>\n request<OrderDetail>(\n `/shop/orders/${encodeURIComponent(orderNo)}/complete`,\n accessInit(access, {method: \"POST\"}),\n ),\n getShipment: (orderNo, access) =>\n request<ShipmentInfo>(`/shop/orders/${encodeURIComponent(orderNo)}/shipment`, accessInit(access)),\n };\n}\n\n/** [ReadOptions] 를 request init 의 Next 캐시 옵션으로 변환. 태그 없으면 빈 객체(하위호환). */\nfunction nextInit(options?: ReadOptions): {next?: {tags?: string[]}} {\n return options?.tags && options.tags.length > 0 ? {next: {tags: options.tags}} : {};\n}\n\n/** 장바구니·결제 요청의 사용자 식별을 request init 으로 변환. */\nfunction shopInit(\n session: ShopSession,\n extra?: {method?: string; body?: unknown; idempotencyKey?: string},\n): {method?: string; body?: unknown; bearer?: string; cartSession?: string; idempotencyKey?: string} {\n return {...extra, bearer: session.accessToken, cartSession: session.cartSessionKey};\n}\n\n/** 주문 접근(본인 토큰 또는 게스트 phone)을 request init 으로 변환. */\nfunction accessInit(\n access: OrderAccess,\n extra?: {method?: string; body?: unknown},\n): {method?: string; body?: unknown; bearer?: string; query?: Record<string, unknown>} {\n return {\n ...extra,\n bearer: access.accessToken,\n query: access.phone ? {phone: access.phone} : undefined,\n };\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n"]}
|