@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.
@@ -0,0 +1,861 @@
1
+ /** 페이징 목록 — `ApiResponse.data` 자리에 들어간다. Spring `Pageable` 기반(page 는 0-based). */
2
+ interface Paginated<T> {
3
+ content: T[];
4
+ page: number;
5
+ size: number;
6
+ totalElements: number;
7
+ totalPages: number;
8
+ first: boolean;
9
+ last: boolean;
10
+ }
11
+ /** 에러 응답 — `ErrorResponse`. 검증 실패면 `errors` 가 채워진다. */
12
+ interface ApiErrorBody {
13
+ timestamp: number;
14
+ status: number;
15
+ /** HTTP 사유구("Conflict"). 사람용 — 분기엔 [errorCode] 를 쓴다. */
16
+ error: string;
17
+ /**
18
+ * 기계 판독 코드(`OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`…). 같은 상태코드의 여러 원인을 가른다.
19
+ * 구버전 백엔드 응답엔 없다 — `OnequeError.code` 가 `error` 로 폴백한다.
20
+ */
21
+ errorCode?: string;
22
+ message: string;
23
+ path: string;
24
+ errors?: ValidationError[];
25
+ }
26
+ interface ValidationError {
27
+ field: string;
28
+ message: string;
29
+ }
30
+ /** `SiteConfigResponse` — 회사 정보·테마·SEO 기본값. JSON 컬럼(themeColors 등)은 문자열로 온다. */
31
+ interface SiteConfig {
32
+ companyName: string;
33
+ ceoName: string | null;
34
+ bizRegNo: string | null;
35
+ address: string | null;
36
+ tel: string | null;
37
+ fax: string | null;
38
+ email: string | null;
39
+ logoAssetId: number | null;
40
+ /** JSON 문자열. 파싱은 소비자 몫(테넌트마다 스키마가 다를 수 있다). */
41
+ themeColors: string | null;
42
+ /**
43
+ * 사이트 기본 SEO JSON 문자열(스키마리스 — 백엔드가 검증하지 않는다). 미설정이면 null.
44
+ * 권장 구조: `{"title": "…", "description": "…"}` — 페이지·상품의 [PageContent.seo]·
45
+ * [ProductDetail.seo] 와 같은 모양이다.
46
+ *
47
+ * `title` 은 자체 제목이 없는 문서(홈 등)의 기본 제목, `description` 은 기본 meta description.
48
+ * 둘 다 없으면 소비자가 상호(`companyName`)로 강하한다. **파싱 실패해도 죽으면 안 된다** —
49
+ * 패스스루라 어떤 값이든 올 수 있다.
50
+ */
51
+ seoDefaults: string | null;
52
+ snsLinks: string | null;
53
+ /**
54
+ * 환불·교환·배송·A/S 정책 JSON 문자열(스키마리스 — 테넌트가 채운다). 미설정이면 null.
55
+ * 권장 구조: `{returns:{windowDays,notes}, exchange:{notes}, shipping:{notes}, as:{notes}}`.
56
+ *
57
+ * **금액은 여기 없다** — 반품 배송비는 [defaultReturnShippingFee] 가 단일 출처다(표시=실제 보장).
58
+ */
59
+ commercePolicies: string | null;
60
+ /**
61
+ * 업종. 스토어프론트가 schema.org 타입을 좁힐 때 쓴다(`BEAUTY` → `BeautySalon`, null → `Organization`).
62
+ * **실제 그 업태일 때만 서버가 채운다** — 테마 선택에서 유도한 값이 아니다.
63
+ */
64
+ businessType: BusinessType | null;
65
+ /**
66
+ * 반품 배송비(원). **실제 환불에서 차감되는 운영 값**이지 표시용 사본이 아니다 —
67
+ * 정책 문구와 이 값이 갈리지 않도록 금액은 항상 이 필드를 쓴다.
68
+ */
69
+ defaultReturnShippingFee: number | null;
70
+ }
71
+ /** 테넌트 업종. 서버가 값을 추가할 수 있다 — 모르는 값은 일반(Organization)으로 흘려보내라. */
72
+ type BusinessType = "BEAUTY";
73
+ /** `PublicCategoryResponse`. */
74
+ interface Category {
75
+ id: number;
76
+ slug: string;
77
+ name: string;
78
+ sortOrder: number;
79
+ }
80
+ /** `PublicPostResponse` — 목록 카드용(본문 없음). */
81
+ interface PostSummary {
82
+ id: number;
83
+ slug: string;
84
+ title: string;
85
+ summary: string | null;
86
+ categoryName: string | null;
87
+ coverAssetId: number | null;
88
+ publishedAt: string | null;
89
+ viewCount: number;
90
+ }
91
+ /** `PublicPostDetailResponse` — 상세(본문 포함). */
92
+ interface PostDetail {
93
+ id: number;
94
+ slug: string;
95
+ title: string;
96
+ summary: string | null;
97
+ content: string | null;
98
+ categoryId: number | null;
99
+ coverAssetId: number | null;
100
+ publishedAt: string | null;
101
+ viewCount: number;
102
+ /**
103
+ * SEO 오버라이드 JSON 문자열(스키마리스 — 백엔드가 검증하지 않는다). 미설정이면 null.
104
+ * 권장 구조: `{"title": "…", "description": "…"}` — 사이트 기본값 [SiteConfig.seoDefaults] 와
105
+ * 같은 모양이다. `title` 없으면 소비자가 자연 제목(페이지 제목·상품명)으로 강하한다.
106
+ * **파싱 실패해도 죽으면 안 된다** — 패스스루라 어떤 값이든 올 수 있다.
107
+ */
108
+ seo: string | null;
109
+ }
110
+ /** 글 목록 질의. `category` 는 카테고리 slug. page 는 0-based. */
111
+ interface ListPostsParams {
112
+ category?: string;
113
+ page?: number;
114
+ size?: number;
115
+ /** Spring 정렬 표현. 예: `"publishedAt,desc"`. */
116
+ sort?: string;
117
+ }
118
+ /**
119
+ * 페이지 섹션 — 구조화된 페이지 구성 요소.
120
+ *
121
+ * `type` 은 **문자열 그대로 둔다**(union 아님). 백엔드 `SectionType` 이 append-only 계약이라
122
+ * union 으로 못박으면 새 타입이 추가되는 순간 기존 스토어프론트가 타입 에러로 깨진다.
123
+ * 아는 값만 [KnownSectionType] 로 따로 제공한다 — **모르는 타입은 조용히 건너뛰는 게 계약이다.**
124
+ *
125
+ * `config` 는 **파싱하지 않은 raw JSON 문자열**이다(`seo`·`themeColors` 와 같은 사상). 백엔드도
126
+ * 검증하지 않는다 — 사이트 하나 고칠 때마다 백엔드를 배포하지 않으려는 의도다. 파싱은 소비자
127
+ * 몫이고, **깨진 config 는 그 섹션만 건너뛰어야지 페이지를 죽이면 안 된다.**
128
+ *
129
+ * 타입별 config 형상(정본은 백엔드 `SectionType` enum KDoc — 바뀌면 세 곳을 같이 고친다):
130
+ * - `SERVICE_MENU` — `{ productIds: number[] }` (`{ categorySlug }` 변형은 공개 상품 API 에
131
+ * 카테고리 필터가 아직 없어 미지원)
132
+ * - `BEFORE_AFTER_GALLERY` — `{ items: [{ beforeAssetId, afterAssetId, caption }] }`
133
+ * - `BOOKING_CTA` — `{ productId, label }`
134
+ * - `DOCTOR_INTRO` — `{ name, title, photoAssetId, bio }`
135
+ */
136
+ interface PageSection {
137
+ type: string;
138
+ sortOrder: number;
139
+ config: string | null;
140
+ }
141
+ /** 지금 렌더러가 아는 섹션 타입. 이 밖의 값이 와도 정상이다(스킵). */
142
+ type KnownSectionType = "SERVICE_MENU" | "BEFORE_AFTER_GALLERY" | "BOOKING_CTA" | "DOCTOR_INTRO";
143
+ /** `PublicPageResponse` — 회사 소개 같은 고정 페이지. */
144
+ interface PageContent {
145
+ id: number;
146
+ slug: string;
147
+ title: string;
148
+ /** 레거시 마크다운 본문. [sections] 가 있으면 무시된다. */
149
+ content: string | null;
150
+ publishedAt: string | null;
151
+ /**
152
+ * SEO 오버라이드 JSON 문자열(스키마리스 — 백엔드가 검증하지 않는다). 미설정이면 null.
153
+ * 권장 구조: `{"title": "…", "description": "…"}` — 사이트 기본값 [SiteConfig.seoDefaults] 와
154
+ * 같은 모양이다. `title` 없으면 소비자가 자연 제목(페이지 제목·상품명)으로 강하한다.
155
+ * **파싱 실패해도 죽으면 안 된다** — 패스스루라 어떤 값이든 올 수 있다.
156
+ */
157
+ seo: string | null;
158
+ /** 있으면 이걸 렌더한다(content 는 무시). 비었으면 [content] 폴백. 순서는 서버가 정렬해 준다. */
159
+ sections: PageSection[];
160
+ }
161
+ type MenuPosition = "HEADER" | "FOOTER";
162
+ /** `PublicMenuResponse` — 재귀 트리(children). */
163
+ interface Menu {
164
+ id: number;
165
+ position: MenuPosition;
166
+ label: string;
167
+ url: string;
168
+ sortOrder: number;
169
+ children: Menu[];
170
+ }
171
+ /** `PublicMediaUrlResponse` — presigned 다운로드 URL. `expiresAt` 전까지만 유효하다. */
172
+ interface MediaUrl {
173
+ url: string;
174
+ expiresAt: string;
175
+ }
176
+ /**
177
+ * 예약 상태. `PENDING`(유료 예약의 결제 대기) → `CONFIRMED`(확정) → `COMPLETED`(방문 완료).
178
+ * `CANCELLED`·`NO_SHOW` 도 종단이다. 서버가 값을 추가할 수 있다 — 모르는 값은 그대로 표시만 하라.
179
+ */
180
+ type BookingStatus = "PENDING" | "CONFIRMED" | "CANCELLED" | "COMPLETED" | "NO_SHOW";
181
+ /**
182
+ * 예약 가능한 슬롯 한 칸 — `AvailabilitySlotResponse`.
183
+ *
184
+ * **가용 판정은 [availableCount] 로 한다** — 상품 카탈로그의 `inStock`/`available` 이 아니다.
185
+ * 예약은 슬롯마다 정원이 따로라, 상품 단위 재고 개념이 성립하지 않는다.
186
+ */
187
+ interface AvailabilitySlot {
188
+ slotId: number;
189
+ resourceId: number;
190
+ /** ISO-8601(UTC). 표시할 때 현지 시간대로 변환하는 건 소비자 몫이다. */
191
+ startAt: string;
192
+ endAt: string;
193
+ /** 이 슬롯의 남은 정원. 0이면 못 잡는다(서버는 애초에 0인 슬롯을 안 준다). */
194
+ availableCount: number;
195
+ }
196
+ /** 예약 생성 입력 — `CreateBookingRequest`. [slotId] 는 [AvailabilitySlot.slotId]. */
197
+ interface CreateBookingInput {
198
+ slotId: number;
199
+ /** 인원·수량. 생략하면 1. 슬롯 정원을 이만큼 소비한다. */
200
+ quantity?: number;
201
+ /** 연락처 — 생략하면 로그인 고객 정보를 쓴다. */
202
+ contactName?: string | null;
203
+ contactPhone?: string | null;
204
+ }
205
+ /**
206
+ * 내 예약 한 건 — `MyBookingResponse`.
207
+ *
208
+ * **[orderNo] 가 핵심 분기다**: 유료 예약(예약금 포함)이면 결제할 주문번호가 실려 오고, 그때는
209
+ * `startPayment(orderNo, ...)` 로 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).
210
+ * 무료 예약이면 null 이고 생성 즉시 `CONFIRMED` 다.
211
+ */
212
+ interface Booking {
213
+ /** 외부 노출 식별자. 취소·변경은 이 코드로 한다(숫자 id 를 쓰지 않는다). */
214
+ bookingCode: string;
215
+ productId: number;
216
+ slotId: number;
217
+ startAt: string | null;
218
+ endAt: string | null;
219
+ quantity: number;
220
+ status: BookingStatus;
221
+ /** 유료 예약이면 결제할 주문번호. 무료면 null. */
222
+ orderNo: string | null;
223
+ createdAt: string | null;
224
+ }
225
+ /** 가용 슬롯 조회 질의. [from]·[to] 는 ISO-8601(UTC) — 조회 창을 넓게 잡으면 응답이 커진다. */
226
+ interface AvailabilityParams {
227
+ productId: number;
228
+ from: string;
229
+ to: string;
230
+ }
231
+ /**
232
+ * 문의 접수 입력 — `InquiryCreateRequest`.
233
+ *
234
+ * 길이 제약은 백엔드가 최종 검증한다(name≤100, email≤255, phone≤30, company≤100,
235
+ * subject≤255, message≤5000). 초과 시 400 + `ValidationError[]` 로 온다.
236
+ */
237
+ interface InquiryInput {
238
+ name: string;
239
+ email: string;
240
+ phone?: string | null;
241
+ company?: string | null;
242
+ subject: string;
243
+ message: string;
244
+ }
245
+ /** `InquiryCreatedResponse`. */
246
+ interface InquiryCreated {
247
+ id: number;
248
+ }
249
+ /** 광고 유입 추적. 전부 선택 — 자연 유입이면 비어 있다. */
250
+ interface LeadTracking {
251
+ utmSource?: string | null;
252
+ utmMedium?: string | null;
253
+ utmCampaign?: string | null;
254
+ utmAdgroup?: string | null;
255
+ utmContent?: string | null;
256
+ fbclid?: string | null;
257
+ gclid?: string | null;
258
+ nclid?: string | null;
259
+ }
260
+ /**
261
+ * 광고 리드 접수 입력 — `LeadCreateRequest`.
262
+ *
263
+ * 문의(InquiryInput)와 달리 **이메일이 선택**이고 연락처가 필수다. UTM·클릭ID 등 광고 유입 추적을
264
+ * `tracking` 으로 함께 보낸다. 길이 제약은 백엔드가 최종 검증한다(name≤100, phone≤30,
265
+ * email≤255, interest≤100, message≤5000).
266
+ */
267
+ interface LeadInput {
268
+ name: string;
269
+ phone: string;
270
+ email?: string | null;
271
+ /** 관심 분야/문의 유형 (예: 치과 진료과목). */
272
+ interest?: string | null;
273
+ message?: string | null;
274
+ /** 빠른 상담 신청 여부. */
275
+ isQuick?: boolean;
276
+ consentMarketing?: boolean;
277
+ tracking?: LeadTracking;
278
+ }
279
+ /** `LeadCreatedResponse`. */
280
+ interface LeadCreated {
281
+ id: number;
282
+ }
283
+ /** 상품 이행 유형. PHYSICAL=배송, DIGITAL=즉시전달, SERVICE=시간·장소 예약. */
284
+ type ProductType = "PHYSICAL" | "DIGITAL" | "SERVICE";
285
+ /** `PublicProductResponse.PublicVariant` — 스토어프론트 노출 variant. */
286
+ interface ProductVariant {
287
+ id: number;
288
+ sku: string | null;
289
+ /** 옵션 조합("레드/XL"). 단순 상품은 빈 문자열. */
290
+ optionSignature: string;
291
+ price: number;
292
+ compareAtPrice: number | null;
293
+ currency: string;
294
+ inStock: boolean;
295
+ /** 추적 재고의 가용수량. 무한재고면 null. */
296
+ available: number | null;
297
+ }
298
+ /** `PublicProductResponse` — 공개 상품 상세(ACTIVE 상품·ACTIVE variant 만). */
299
+ interface ProductDetail {
300
+ /** 숫자 상품 id — 후기 조회(`listProductReviews(id)`·`getProductReviewSummary(id)`) 등 id 기반 API 의 연결 키. */
301
+ id: number;
302
+ slug: string;
303
+ name: string;
304
+ description: string | null;
305
+ productType: ProductType;
306
+ coverAssetId: number | null;
307
+ /**
308
+ * SEO 오버라이드 JSON 문자열(스키마리스 — 백엔드가 검증하지 않는다). 미설정이면 null.
309
+ * 권장 구조: `{"title": "…", "description": "…"}` — 사이트 기본값 [SiteConfig.seoDefaults] 와
310
+ * 같은 모양이다. `title` 없으면 소비자가 자연 제목(페이지 제목·상품명)으로 강하한다.
311
+ * **파싱 실패해도 죽으면 안 된다** — 패스스루라 어떤 값이든 올 수 있다.
312
+ */
313
+ seo: string | null;
314
+ variants: ProductVariant[];
315
+ }
316
+ /** `PublicProductCategoryResponse`. */
317
+ /** `PublicProductSummaryResponse` — 공개 상품 목록 카드(최저가·구매가능 요약). */
318
+ interface ProductSummary {
319
+ /** 숫자 상품 id — 카드 평점 요약(`getProductReviewSummary(id)`) 연결 키. */
320
+ id: number;
321
+ slug: string;
322
+ name: string;
323
+ productType: ProductType;
324
+ coverAssetId: number | null;
325
+ /** 최저 판매가(ACTIVE variant 중). 없으면 null. */
326
+ priceFrom: number | null;
327
+ currency: string;
328
+ inStock: boolean;
329
+ }
330
+ /** `listProducts` 질의. category 필터는 상품↔카테고리 매핑이 채워진 뒤 지원. */
331
+ interface ListProductsParams {
332
+ productType?: ProductType;
333
+ keyword?: string;
334
+ page?: number;
335
+ size?: number;
336
+ sort?: string;
337
+ }
338
+ interface ProductCategory {
339
+ id: number;
340
+ parentId: number | null;
341
+ slug: string;
342
+ name: string;
343
+ sortOrder: number;
344
+ }
345
+ /**
346
+ * `ReviewResponse` — 공개 후기 한 건(VISIBLE 만 노출). 작성자 신원은 공개하지 않는다(v1).
347
+ *
348
+ * 후기는 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `ProductDetail.id` 로 획득한다
349
+ * (`getProduct(slug)` → `.id` → `listProductReviews(id)`). 0.4.0 에서 ProductDetail 에 id 를 노출해 봉합됨.
350
+ */
351
+ interface Review {
352
+ id: number;
353
+ productId: number;
354
+ /** 별점 1~5. */
355
+ rating: number;
356
+ title: string | null;
357
+ content: string;
358
+ /** 사진 후기의 media asset id 배열. 사진이 없으면 빈 배열. `getMediaUrl(id)` 로 URL 화. */
359
+ photos: number[];
360
+ /** 작성 시각(ISO-8601 문자열). */
361
+ createdAt: string | null;
362
+ }
363
+ /**
364
+ * `RatingSummaryResponse` — 상품 평점 요약. 평균 = 별점합 / 후기수(후기가 없으면 count·avg 모두 0).
365
+ * product 의 비정규화 집계에서 읽으므로 목록 렌더에 값싸게 쓸 수 있다.
366
+ */
367
+ interface RatingSummary {
368
+ productId: number;
369
+ reviewCount: number;
370
+ averageRating: number;
371
+ }
372
+ /**
373
+ * `listProductReviews` 질의. page 는 0-based. **정렬은 백엔드가 항상 최신순(id 내림차순)으로 고정**하므로
374
+ * sort 파라미터는 없다.
375
+ */
376
+ interface ListReviewsParams {
377
+ page?: number;
378
+ size?: number;
379
+ }
380
+ /** `CartResponse.CartLine` — 가격은 항상 현재가로 재계산된다(주문 시에만 동결). */
381
+ interface CartLine {
382
+ variantId: number;
383
+ productName: string | null;
384
+ variantLabel: string | null;
385
+ sku: string | null;
386
+ unitPrice: number | null;
387
+ quantity: number;
388
+ lineTotal: number | null;
389
+ /** 판매가능(ACTIVE variant + 재고). false 면 결제에서 제외/차단된다. */
390
+ available: boolean;
391
+ }
392
+ /** `CartResponse`. */
393
+ interface Cart {
394
+ items: CartLine[];
395
+ subtotal: number;
396
+ currency: string;
397
+ }
398
+ type SocialProvider = "KAKAO" | "NAVER" | "GOOGLE";
399
+ /**
400
+ * 약관·정책 동의 종류.
401
+ * - 필수(신규가입에 셋 다 granted=true 여야 함): `TERMS`(이용약관)·`PRIVACY`(개인정보 수집·이용)·`OVER_14`(만 14세 이상)
402
+ * - 선택: `MARKETING_EMAIL`·`MARKETING_SMS`(마케팅 수신)
403
+ */
404
+ type ConsentType = "TERMS" | "PRIVACY" | "OVER_14" | "MARKETING_EMAIL" | "MARKETING_SMS";
405
+ /**
406
+ * 동의 한 건 — 어떤 정책의 어느 버전에 동의(또는 거부)했는지.
407
+ * `policyVersion` 은 동의받은 약관 문서의 버전 식별자(예: `"v1"`)로, 프론트가 관리한다.
408
+ */
409
+ interface ConsentInput {
410
+ consentType: ConsentType;
411
+ policyVersion: string;
412
+ granted: boolean;
413
+ }
414
+ /**
415
+ * 현재 동의 상태 — `GET /api/shop/consents` 응답 행. 입력(`ConsentInput`)에 마지막 반영 시각을 더한 것.
416
+ * `occurredAt` 은 백엔드가 채워 주는 ISO-8601 문자열(동의/철회가 기록된 시점).
417
+ */
418
+ interface ConsentStatus {
419
+ consentType: ConsentType;
420
+ policyVersion: string;
421
+ granted: boolean;
422
+ occurredAt?: string | null;
423
+ }
424
+ /**
425
+ * `SocialLoginRequest` — 소셜 리다이렉트에서 받은 authorization code 를 백엔드가 교환한다.
426
+ *
427
+ * ⚠️ 백엔드는 **신규 고객 가입 시 필수 동의를 강제**한다: `consents` 에 `TERMS`·`PRIVACY`·`OVER_14`
428
+ * 가 모두 `granted=true` 로 있어야 한다. 없으면 400 (`code: CONSENT_REQUIRED`) 로 가입이 거부된다.
429
+ * 기존 회원 재로그인은 `consents` 없이도 통과한다.
430
+ */
431
+ interface SocialLoginInput {
432
+ provider: SocialProvider;
433
+ code: string;
434
+ redirectUri?: string;
435
+ /** 약관 동의 목록. 신규 가입 시 필수 3종이 granted=true 여야 백엔드가 가입을 허용한다. */
436
+ consents?: ConsentInput[];
437
+ }
438
+ /** `CustomerSummaryResponse` — PK 대신 customerKey(UUID)만 노출. */
439
+ interface CustomerSummary {
440
+ customerKey: string;
441
+ name: string | null;
442
+ email: string | null;
443
+ phone: string | null;
444
+ emailVerified: boolean;
445
+ phoneVerified: boolean;
446
+ }
447
+ /** `AuthTokenResponse` — 로그인·갱신 성공. accessToken 은 15분, refresh 는 매 교환마다 회전. */
448
+ interface AuthTokens {
449
+ accessToken: string;
450
+ refreshToken: string;
451
+ tokenType: string;
452
+ expiresIn: number;
453
+ customer: CustomerSummary;
454
+ }
455
+ type OrderStatus = "PENDING_PAYMENT" | "PAID" | "SHIPPED" | "DELIVERED" | "COMPLETED" | "CANCELED" | "REFUNDED";
456
+ interface ShipToInput {
457
+ name?: string;
458
+ phone?: string;
459
+ zipcode?: string;
460
+ address1?: string;
461
+ address2?: string;
462
+ }
463
+ /** `CheckoutRequest` — 구매자 연락처는 게스트 주문 조회 크리덴셜이라 필수. */
464
+ interface CheckoutInput {
465
+ buyerName: string;
466
+ buyerEmail?: string;
467
+ buyerPhone: string;
468
+ shipTo?: ShipToInput;
469
+ }
470
+ interface OrderItemLine {
471
+ /** 주문 라인 id — **후기 작성 키**. `createProductReview` 의 `orderItemId` 로 넘긴다. */
472
+ id: number;
473
+ /** 이 라인 상품의 id. 후기 작성 경로(`/shop/products/{productId}/reviews`)에 쓴다. 예약금 라인이면 null. */
474
+ productId: number | null;
475
+ productName: string;
476
+ variantLabel: string | null;
477
+ sku: string | null;
478
+ unitPrice: number;
479
+ quantity: number;
480
+ lineTotal: number;
481
+ }
482
+ /**
483
+ * 후기 작성 입력 — `CreateReviewRequest`.
484
+ *
485
+ * **구매검증이 걸린다**: `orderItemId` 는 [OrderItemLine.id](주문 상세에서 얻는다)이고, 서버가
486
+ * (본인 주문 + 배송완료(DELIVERED) 이상 + 상품 일치)를 3중 확인한다. 라인당 후기 1개(재작성 불가).
487
+ */
488
+ interface CreateReviewInput {
489
+ /** [OrderItemLine.id]. 주문 상세 응답에서 획득한다 — 다른 데서 못 얻는다. */
490
+ orderItemId: number;
491
+ /** 별점 1~5. */
492
+ rating: number;
493
+ title?: string | null;
494
+ content: string;
495
+ /** media asset id 배열(사진 후기). 고객 업로드 표면이 아직 없어 실질 생략. */
496
+ photos?: number[];
497
+ }
498
+ interface OrderHistoryEntry {
499
+ fromStatus: OrderStatus | null;
500
+ toStatus: OrderStatus;
501
+ actor: "SYSTEM" | "CUSTOMER" | "OPERATOR" | "PAYONEQ";
502
+ reason: string | null;
503
+ occurredAt: string;
504
+ }
505
+ /** `OrderResponse` — 주문 상세(스냅샷 동결). */
506
+ interface OrderDetail {
507
+ orderNo: string;
508
+ status: OrderStatus;
509
+ currency: string;
510
+ subtotalAmount: number;
511
+ shippingAmount: number;
512
+ discountAmount: number;
513
+ totalAmount: number;
514
+ buyerName: string | null;
515
+ buyerEmail: string | null;
516
+ buyerPhone: string | null;
517
+ shipToName: string | null;
518
+ shipToPhone: string | null;
519
+ shipToZipcode: string | null;
520
+ shipToAddress1: string | null;
521
+ shipToAddress2: string | null;
522
+ placedAt: string | null;
523
+ items: OrderItemLine[];
524
+ history: OrderHistoryEntry[];
525
+ }
526
+ /** `OrderSummaryResponse` — 내 주문 목록 행. */
527
+ interface OrderSummary {
528
+ orderNo: string;
529
+ status: OrderStatus;
530
+ totalAmount: number;
531
+ currency: string;
532
+ placedAt: string | null;
533
+ }
534
+ /**
535
+ * `PaymentSessionResponse` — 결제 시작 결과.
536
+ *
537
+ * **벤더가 둘 중 하나다**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
538
+ * - **리다이렉트형**(PayOneQ 등): [widget] 이 없다 → [paymentUrl] 로 고객을 보내면 끝.
539
+ * - **위젯형**(토스): [widget] 값으로 **내 사이트에서** 결제창을 띄우고, 성공 콜백의 파라미터를
540
+ * [OnequeClient.confirmPayment] 로 넘겨 승인을 확정한다.
541
+ *
542
+ * 분기는 `session.widget ? 위젯 : redirect(session.paymentUrl)` 한 줄이면 된다.
543
+ */
544
+ interface PaymentSession {
545
+ paymentUrl: string;
546
+ /** PG 결제 식별자(대사 키). 위젯형은 승인 전이라 주문번호가 들어온다. */
547
+ pgPaymentId: string;
548
+ /**
549
+ * 위젯형 벤더가 결제창을 띄우는 데 필요한 값. 리다이렉트형이면 없다(null/undefined).
550
+ *
551
+ * 토스: `{vendor:"TOSS_PAYMENTS", clientKey, orderId, amount, orderName}`.
552
+ * **브라우저 노출이 전제된 값만 담긴다** — clientKey 는 본디 프론트 공개값이고 secretKey 는 없다.
553
+ * 키 구성은 벤더마다 다르므로 `vendor` 로 분기해 읽는다.
554
+ */
555
+ widget?: Record<string, string> | null;
556
+ }
557
+ type ShipmentStatus = "READY" | "IN_TRANSIT" | "OUT_FOR_DELIVERY" | "DELIVERED" | "FAILED";
558
+ interface ShipmentEvent {
559
+ status: ShipmentStatus;
560
+ location: string | null;
561
+ description: string | null;
562
+ occurredAt: string;
563
+ }
564
+ /** `ShipmentResponse`. */
565
+ interface ShipmentInfo {
566
+ carrierCode: string | null;
567
+ trackingNo: string | null;
568
+ status: ShipmentStatus;
569
+ shippedAt: string | null;
570
+ deliveredAt: string | null;
571
+ lastSyncedAt: string | null;
572
+ events: ShipmentEvent[];
573
+ }
574
+
575
+ /**
576
+ * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면
577
+ * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.
578
+ * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).
579
+ * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.
580
+ */
581
+ interface ReadOptions {
582
+ /** Next ISR 캐시 태그. 이 태그로 백엔드가 온디맨드 revalidate 한다. */
583
+ tags?: string[];
584
+ }
585
+ interface OnequeClientOptions {
586
+ /**
587
+ * 백엔드 베이스 URL — `/api` 접두사는 붙이지 않는다. 예: `http://localhost:8100`.
588
+ * 클라이언트가 경로에 `/api/public/...` 를 붙인다.
589
+ */
590
+ baseUrl: string;
591
+ /**
592
+ * 테넌트 코드. 모든 요청에 `X-Tenant` 헤더로 실린다.
593
+ *
594
+ * 공개 API 는 이 헤더를 그대로 믿는다(비인증). 그래서 이 클라이언트는 **서버 사이드**
595
+ * (RSC·route handler·server action)에서 쓰는 것을 전제로 한다 — 브라우저에서 직접 부르면
596
+ * baseUrl 이 노출되고 CORS 를 열어야 한다. README 참고.
597
+ */
598
+ tenant: string;
599
+ /**
600
+ * `fetch` 구현 주입(선택). 기본은 전역 `fetch`(Node 18+·브라우저). 테스트·커스텀 에이전트·
601
+ * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.
602
+ */
603
+ fetch?: typeof fetch;
604
+ /** 모든 요청에 추가할 헤더(선택). */
605
+ headers?: Record<string, string>;
606
+ /** 요청 타임아웃(ms). 기본 10초. */
607
+ timeoutMs?: number;
608
+ }
609
+ /**
610
+ * Oneque 공개 API 클라이언트.
611
+ *
612
+ * 테넌트 사이트(credium 등)가 백엔드의 공개 엔드포인트를 타입 안전하게 부르기 위한 얇은 래퍼다.
613
+ * 런타임 의존성이 없다 — 전역 `fetch` 만 쓴다.
614
+ *
615
+ * ```ts
616
+ * const cms = createOnequeClient({ baseUrl: process.env.API_BASE_URL!, tenant: "credium" });
617
+ * const posts = await cms.listPosts({ size: 10, sort: "publishedAt,desc" });
618
+ * await cms.submitInquiry({ name, email, subject, message });
619
+ * ```
620
+ *
621
+ * 모든 메서드는 성공 시 envelope 안쪽 `data` 를 돌려주고, 실패 시 [OnequeError] 를 던진다.
622
+ */
623
+ interface OnequeClient {
624
+ /** 회사 정보·테마·SEO 기본값. ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다. */
625
+ getSiteConfig(options?: ReadOptions): Promise<SiteConfig>;
626
+ /** 살아 있는 카테고리 전부(페이징 없음). */
627
+ listCategories(): Promise<Category[]>;
628
+ /** 발행된 글 목록(페이징). */
629
+ listPosts(params?: ListPostsParams): Promise<Paginated<PostSummary>>;
630
+ /** slug 로 글 상세(본문 포함). 없으면 404 → [OnequeError]. */
631
+ getPost(slug: string): Promise<PostDetail>;
632
+ /**
633
+ * 조회 비콘 — 같은 뷰어의 같은 날 재조회는 집계되지 않는다(반환 false).
634
+ * 화면 렌더를 막지 않도록 fire-and-forget 으로 부르길 권한다(실패해도 페이지는 살아야 한다).
635
+ *
636
+ * 조회 dedup 은 뷰어 IP·UA 를 쓴다. 서버 사이드에서 부를 때는 [RequestContext.clientIp] 로
637
+ * 원 방문자 IP 를 넘겨야 방문자별로 집계된다 — 안 넘기면 전부 서버 IP 하나로 뭉친다.
638
+ */
639
+ recordPostView(slug: string, context?: RequestContext): Promise<boolean>;
640
+ /** slug 로 고정 페이지. */
641
+ /**
642
+ * slug 로 고정 페이지(섹션 포함). 없으면 404 → [OnequeError].
643
+ * ISR 페이지는 [ReadOptions.tags] 로 캐시 태그를 실을 수 있다 — 백엔드가 발행 시 그 태그만
644
+ * 콕 집어 revalidate 한다.
645
+ */
646
+ getPage(slug: string, options?: ReadOptions): Promise<PageContent>;
647
+ /** 메뉴 트리(HEADER·FOOTER). */
648
+ listMenus(options?: ReadOptions): Promise<Menu[]>;
649
+ /** 미디어 presigned 다운로드 URL(만료 있음). */
650
+ getMediaUrl(id: number): Promise<MediaUrl>;
651
+ /**
652
+ * 문의 접수. 성공 시 생성된 문의 id. 레이트리밋이면 429 → `error.isRateLimited`.
653
+ *
654
+ * ⚠️ 서버 사이드(테넌트 route handler)에서 부를 때는 [RequestContext.clientIp] 로 **원 방문자
655
+ * IP 를 반드시 넘겨야 한다.** 백엔드의 문의 레이트리밋·IP 기록은 `X-Forwarded-For` 첫 홉을
656
+ * 쓰는데, 안 넘기면 백엔드가 테넌트 서버 IP 하나만 보고 몇 건 뒤 **모든 방문자**를 429 로 막는다.
657
+ */
658
+ submitInquiry(input: InquiryInput, context?: RequestContext): Promise<InquiryCreated>;
659
+ /**
660
+ * 광고 리드 접수. 문의와 달리 이메일이 선택이고 UTM 추적을 함께 보낸다.
661
+ *
662
+ * ⚠️ [submitInquiry] 와 같은 이유로 서버 사이드에서는 [RequestContext.clientIp] 로 원 방문자
663
+ * IP 를 넘겨야 한다 — 백엔드 리드 레이트리밋·IP 기록이 그 값을 본다.
664
+ */
665
+ submitLead(input: LeadInput, context?: RequestContext): Promise<LeadCreated>;
666
+ /** slug 로 공개 상품(ACTIVE) 상세 — variant·재고 가용여부 포함. 없으면 404. ISR 태그는 [ReadOptions]. */
667
+ getProduct(slug: string, options?: ReadOptions): Promise<ProductDetail>;
668
+ /** 공개 상품 목록(ACTIVE) — 카드용 요약(최저가·재고). ISR 태그는 [ReadOptions]. */
669
+ listProducts(params?: ListProductsParams, options?: ReadOptions): Promise<Paginated<ProductSummary>>;
670
+ /** 커머스 카테고리 목록(노출 순서). ISR 태그는 [ReadOptions]. */
671
+ listProductCategories(options?: ReadOptions): Promise<ProductCategory[]>;
672
+ /**
673
+ * 상품 후기 목록(공개·VISIBLE 만, 최신순, 페이징).
674
+ *
675
+ * 상품의 **숫자 id(productId)** 로 조회한다 — slug 가 아니다. `getProduct(slug).id` 로 획득한다(0.4.0).
676
+ */
677
+ listProductReviews(productId: number, params?: ListReviewsParams, options?: ReadOptions): Promise<Paginated<Review>>;
678
+ /** 상품 평점 요약(후기 수·평균 별점). 후기 없으면 count=0·avg=0. 목록 별점 배지에 값싸게 쓴다. */
679
+ getProductReviewSummary(productId: number, options?: ReadOptions): Promise<RatingSummary>;
680
+ /**
681
+ * 후기 작성 — **로그인 필수 + 구매검증**. `input.orderItemId` 는 [OrderItemLine.id](주문 상세에서 획득).
682
+ * 서버가 본인 주문·배송완료(DELIVERED) 이상·상품 일치를 3중 확인한다. 라인당 1개(재작성 시 409 `ALREADY_REVIEWED`).
683
+ */
684
+ createProductReview(productId: number, input: CreateReviewInput, accessToken: string): Promise<Review>;
685
+ /** 소셜 로그인 — authorization code 를 백엔드가 교환하고 고객 토큰을 발급. */
686
+ socialLogin(input: SocialLoginInput): Promise<AuthTokens>;
687
+ /** refresh 토큰으로 새 토큰 회전(옛 refresh 는 폐기). */
688
+ refreshSession(refreshToken: string): Promise<AuthTokens>;
689
+ /** 로그아웃 — 이 고객의 모든 세션 폐기. */
690
+ logout(accessToken: string): Promise<string>;
691
+ /** 내 정보. */
692
+ getMe(accessToken: string): Promise<CustomerSummary>;
693
+ /** 현재 동의 상태 조회(로그인 필수) — 마케팅 수신 토글 화면 등에서 초기값으로 쓴다. */
694
+ getConsents(accessToken: string): Promise<ConsentStatus[]>;
695
+ /**
696
+ * 동의 갱신(로그인 필수, append) — 마케팅 수신 on/off 등. 넘긴 항목만 반영되고 나머지는 유지된다.
697
+ * 성공 시 백엔드 확인 메시지를 돌려준다(최신 상태가 필요하면 [getConsents] 로 다시 읽는다).
698
+ */
699
+ updateConsents(accessToken: string, consents: ConsentInput[]): Promise<string>;
700
+ /**
701
+ * 예약 가능한 슬롯 조회 — **비로그인 가능**(상품 상세에서 시간표를 보여주는 자리).
702
+ *
703
+ * 미래·잔여정원 있는 OPEN 슬롯만 온다. 가용 판정은 [AvailabilitySlot.availableCount] 로 하고,
704
+ * 카탈로그의 `inStock`/`available` 을 쓰지 마라 — 예약은 슬롯마다 정원이 따로다.
705
+ */
706
+ availability(params: AvailabilityParams, options?: ReadOptions): Promise<AvailabilitySlot[]>;
707
+ /**
708
+ * 예약 생성 — 슬롯 정원을 원자적으로 소비한다. **로그인 필수**(게스트 예약 없음).
709
+ *
710
+ * 무료 예약은 즉시 `CONFIRMED`. **유료·예약금이면 `status=PENDING` + [Booking.orderNo] 가 실려 오고**,
711
+ * 그 orderNo 로 [startPayment] 를 불러 **기존 결제 흐름을 그대로 탄다**(예약 전용 결제 API 는 없다).
712
+ *
713
+ * 정원 소진은 `SLOT_FULL` — 슬롯 목록이 stale 했다는 뜻이니 [availability] 를 다시 읽어 보여줘라.
714
+ */
715
+ createBooking(accessToken: string, input: CreateBookingInput): Promise<Booking>;
716
+ /** 내 예약 목록(최신순). 로그인 필수. */
717
+ myBookings(accessToken: string): Promise<Booking[]>;
718
+ /** 예약 취소 — 시작 전·무료 예약만. 정원이 복원된다. 로그인 필수. */
719
+ cancelBooking(accessToken: string, bookingCode: string): Promise<string>;
720
+ /** 같은 상품의 다른 슬롯으로 이동(신 슬롯 소비·구 슬롯 복원). 로그인 필수. */
721
+ rescheduleBooking(accessToken: string, bookingCode: string, newSlotId: number): Promise<Booking>;
722
+ /**
723
+ * 장바구니 조회. 로그인은 accessToken, 게스트는 cartSessionKey 로 식별.
724
+ *
725
+ * ⚠️ **예약(SERVICE) 상품은 장바구니에 담기지 않는다** — [addToCart] 가 400 으로 거부한다.
726
+ * 예약은 [createBooking] 으로 별도 흐름을 탄다(카트에 슬롯을 실으면 체크아웃이 오염된다).
727
+ */
728
+ getCart(session: ShopSession): Promise<Cart>;
729
+ /**
730
+ * 담기(같은 variant 는 수량 누적).
731
+ *
732
+ * ⚠️ **예약(SERVICE) 상품은 400 으로 거부된다** — 예약은 [createBooking] 이다. 상품 상세에서
733
+ * `productType === "SERVICE"` 면 담기 버튼 대신 슬롯 선택을 그려라.
734
+ */
735
+ addToCart(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;
736
+ /** 수량 변경. */
737
+ updateCartItem(variantId: number, quantity: number, session: ShopSession): Promise<Cart>;
738
+ /** 항목 삭제. */
739
+ removeFromCart(variantId: number, session: ShopSession): Promise<Cart>;
740
+ /** 비우기. */
741
+ clearCart(session: ShopSession): Promise<Cart>;
742
+ /**
743
+ * 결제(주문 생성) — 스냅샷 동결 + 재고 차감. 상태=PENDING_PAYMENT.
744
+ *
745
+ * [idempotencyKey] 를 주면 **같은 키·같은 입력의 재요청이 새 주문을 만들지 않고 원주문을 그대로
746
+ * 반환**한다(더블클릭·네트워크 재시도 안전). 같은 키로 **다른 입력**을 보내면 409.
747
+ *
748
+ * **키는 "이 체크아웃 1건"에 고정된 값이어야 한다** — 호출마다 새로 만들면 재시도가 서로 다른
749
+ * 키를 들고 가서 아무것도 막지 못한다(그래서 SDK 가 자동 생성해 주지 않는다). 안정적인 소스를
750
+ * 쓴다: 게스트는 **장바구니 세션 키**(카트 1개 → 주문 1건이라 의미가 정확히 맞는다),
751
+ * 그 외에는 결제 시도 시작 시 한 번 만들어 재시도 동안 보관한 값.
752
+ *
753
+ * 안 주면 종전 동작 — 중복 제출은 카트가 이미 소비돼 404(`CART_NOT_FOUND`)로 떨어진다.
754
+ */
755
+ checkout(input: CheckoutInput, session: ShopSession, idempotencyKey?: string): Promise<OrderDetail>;
756
+ /**
757
+ * 결제 시작. **벤더에 따라 두 갈래**(테넌트가 자기 PG 를 고른다 — 기본 TOSS):
758
+ * `session.widget` 이 있으면 위젯형(내 사이트에서 결제창 → [confirmPayment] 로 승인 확정),
759
+ * 없으면 리다이렉트형(`session.paymentUrl` 로 보내면 끝).
760
+ */
761
+ startPayment(orderNo: string, access: OrderAccess): Promise<PaymentSession>;
762
+ /**
763
+ * **위젯형 벤더의 승인 확정**(토스 등). 결제창이 성공 URL 로 돌려준 파라미터를 그대로 넘긴다
764
+ * (토스: `{paymentKey, orderId, amount}` — URL 쿼리에서 읽어 통째로).
765
+ *
766
+ * **금액을 넘겨도 서버는 안 믿는다** — 서버가 저장한 주문 금액으로 PG 에 직접 묻는다. 그래서
767
+ * 브라우저에서 금액을 조작해도 승인되지 않는다.
768
+ *
769
+ * 이미 승인된 주문(웹훅 선착)이면 조용히 통과한다 — 재호출이 안전하다.
770
+ * 리다이렉트형 벤더에 부르면 404(`PG_CONFIG_NOT_FOUND`).
771
+ */
772
+ confirmPayment(orderNo: string, providerParams: Record<string, string>, access: OrderAccess): Promise<void>;
773
+ /** 주문 조회 — 본인(accessToken) 또는 게스트(phone). */
774
+ getOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;
775
+ /** 내 주문 목록(로그인 필수). */
776
+ listMyOrders(accessToken: string, params?: {
777
+ page?: number;
778
+ size?: number;
779
+ }): Promise<Paginated<OrderSummary>>;
780
+ /** 주문 취소(미결제만) — 본인 또는 게스트(phone). */
781
+ cancelOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;
782
+ /** 구매 확정(배송완료 후) — 본인 또는 게스트(phone). */
783
+ completeOrder(orderNo: string, access: OrderAccess): Promise<OrderDetail>;
784
+ /** 배송 조회 — 본인 또는 게스트(phone). */
785
+ getShipment(orderNo: string, access: OrderAccess): Promise<ShipmentInfo>;
786
+ }
787
+ /**
788
+ * 장바구니·결제의 사용자 식별. 로그인 고객은 [accessToken], 게스트는 [cartSessionKey]
789
+ * (브라우저가 만든 안정적 익명 키)를 넘긴다. 로그인 시 게스트 카트→고객 카트 병합은 백엔드 몫.
790
+ */
791
+ interface ShopSession {
792
+ accessToken?: string;
793
+ cartSessionKey?: string;
794
+ }
795
+ /**
796
+ * 주문 조회·취소·배송조회의 접근 크리덴셜. 로그인 고객은 [accessToken] 으로 소유권을,
797
+ * 게스트는 주문 시 남긴 [phone] 으로 확인한다(주문번호 + 연락처).
798
+ */
799
+ interface OrderAccess {
800
+ accessToken?: string;
801
+ phone?: string;
802
+ }
803
+ /**
804
+ * IP 민감 엔드포인트(문의·조회 비콘)에서 원 방문자를 백엔드에 알리는 컨텍스트.
805
+ *
806
+ * 테넌트 사이트는 서버 사이드에서 이 클라이언트를 부르므로, 백엔드가 보는 소스 IP 는 방문자가
807
+ * 아니라 테넌트 서버다. 방문자 IP 를 `X-Forwarded-For` 로 실어 백엔드가 방문자별로 판단하게 한다.
808
+ */
809
+ interface RequestContext {
810
+ /** 원 방문자 IP — route handler 에서 요청 헤더(x-forwarded-for·x-real-ip)로 뽑아 넘긴다. */
811
+ clientIp?: string;
812
+ }
813
+ declare function createOnequeClient(options: OnequeClientOptions): OnequeClient;
814
+
815
+ /**
816
+ * Oneque API 호출 실패.
817
+ *
818
+ * 백엔드의 `ErrorResponse` 를 그대로 담는다.
819
+ *
820
+ * **원인 분기는 [code] 로 한다** — 한 엔드포인트가 같은 상태코드로 여러 원인을 낸다(checkout 의 409 =
821
+ * `OUT_OF_STOCK`·`ITEM_NOT_PURCHASABLE`·`IDEMPOTENCY_CONFLICT` — 처방이 전부 다르다). `message` 는
822
+ * 사람용 한국어라 **문자열 매칭 금지**.
823
+ *
824
+ * `status` 는 거친 분류에만:
825
+ * - `400` + [validationErrors] — 입력값 문제(폼 필드별 메시지 노출)
826
+ * - `404` — 없는 slug/리소스
827
+ * - `429` — IP 레이트리밋(문의 남발). [isRateLimited] 로 편히 판별
828
+ * - `5xx` — 서버 오류
829
+ *
830
+ * 네트워크 자체가 실패했거나 응답이 JSON 이 아니면 [status] 가 0 이고 [body] 가 null 이다.
831
+ */
832
+ declare class OnequeError extends Error {
833
+ /** HTTP 상태. 네트워크 실패·비JSON 응답이면 0. */
834
+ readonly status: number;
835
+ /**
836
+ * **기계 판독 에러 코드** — `ErrorResponse.errorCode`(백엔드 `ErrorCode` enum 이름).
837
+ * 예: `OUT_OF_STOCK`·`IDEMPOTENCY_CONFLICT`·`CART_NOT_FOUND`. 이걸로 분기한다.
838
+ *
839
+ * 코드를 안 싣는 구버전 백엔드 응답에서는 `error`(HTTP 사유구, "Conflict")로 **폴백**한다 —
840
+ * 그때는 원인을 가를 수 없으니, 폴백 값에 의존하는 분기를 짜지 마라.
841
+ * 열린 계약이라 문자열이다(서버가 코드를 추가해도 SDK 릴리스가 필요 없다).
842
+ */
843
+ readonly code: string | null;
844
+ /** 검증 실패 시 필드별 메시지. 그 외엔 빈 배열. */
845
+ readonly validationErrors: ValidationError[];
846
+ /** 파싱된 원본 에러 본문(있으면). */
847
+ readonly body: ApiErrorBody | null;
848
+ constructor(message: string, options: {
849
+ status: number;
850
+ code?: string | null;
851
+ validationErrors?: ValidationError[];
852
+ body?: ApiErrorBody | null;
853
+ cause?: unknown;
854
+ });
855
+ /** IP 레이트리밋(429) — 문의 폼에서 "잠시 후 다시" 를 띄울 때 쓴다. */
856
+ get isRateLimited(): boolean;
857
+ /** 응답 본문(파싱된 것)으로부터 에러를 만든다. */
858
+ static fromBody(status: number, body: unknown): OnequeError;
859
+ }
860
+
861
+ export { type ApiErrorBody, type AuthTokens, type AvailabilityParams, type AvailabilitySlot, type Booking, type BookingStatus, type BusinessType, type Cart, type CartLine, type Category, type CheckoutInput, type ConsentInput, type ConsentStatus, type ConsentType, type CreateBookingInput, type CreateReviewInput, type CustomerSummary, type InquiryCreated, type InquiryInput, type KnownSectionType, type LeadCreated, type LeadInput, type LeadTracking, type ListPostsParams, type ListProductsParams, type ListReviewsParams, type MediaUrl, type Menu, type MenuPosition, type OnequeClient, type OnequeClientOptions, OnequeError, type OrderAccess, type OrderDetail, type OrderHistoryEntry, type OrderItemLine, type OrderStatus, type OrderSummary, type PageContent, type PageSection, type Paginated, type PaymentSession, type PostDetail, type PostSummary, type ProductCategory, type ProductDetail, type ProductSummary, type ProductType, type ProductVariant, type RatingSummary, type ReadOptions, type RequestContext, type Review, type ShipToInput, type ShipmentEvent, type ShipmentInfo, type ShipmentStatus, type ShopSession, type SiteConfig, type SocialLoginInput, type SocialProvider, type ValidationError, createOnequeClient };