@zalkera/client 0.6.4 → 0.7.2
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 +2 -37
- package/dist/index.cjs +124 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -3
- package/dist/index.d.ts +52 -3
- package/dist/index.js +117 -2
- package/dist/index.js.map +1 -1
- package/llms.txt +84 -12
- package/package.json +48 -47
package/llms.txt
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
## 1. 설치·초기화
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npm
|
|
17
|
+
npm install @zalkera/client # 공개 npm (MIT)
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
- 버전 못 박기: tarball 을 `vendor/` 에 넣어 써도 결과는 동일하다.
|
|
21
|
+
- ❌ 디렉터리 심링크(`file:<dir>`) 금지 — Next 16 Turbopack 이 프로젝트 밖을 못 읽어 깨진다.
|
|
22
|
+
|
|
20
23
|
```ts
|
|
21
24
|
// lib/zalkera.ts — 서버 전용 싱글턴
|
|
22
25
|
import { createZalkeraClient } from "@zalkera/client";
|
|
@@ -61,9 +64,15 @@ await zalkera.submitInquiry(input, { clientIp: ip });
|
|
|
61
64
|
|
|
62
65
|
### 커머스 — 카탈로그(공개)
|
|
63
66
|
- `listProducts({productType?, keyword?, page?, size?, sort?})` → `Paginated<ProductSummary>` (카드용: slug·name·priceFrom·inStock)
|
|
64
|
-
- `getProduct(slug)` → `ProductDetail { name, productType, variants[] }`
|
|
67
|
+
- `getProduct(slug)` → `ProductDetail { id, slug, name, description, productType, coverAssetId, seo, variants[] }`
|
|
68
|
+
- `seo` 는 **JSON 문자열 패스스루**(백엔드가 검증 안 함·미설정이면 null). 권장 구조는 `{"title","description"}` —
|
|
69
|
+
`SiteConfig.seoDefaults` 와 같은 모양이다. **파싱 실패해도 죽으면 안 된다**(`parseConfig` 로 읽고, 없으면 상품명으로 강하).
|
|
65
70
|
- `listProductCategories()` → `ProductCategory[]`
|
|
66
71
|
- **variant 가 판매 단위다.** 가격·재고·장바구니·주문은 전부 `variant.id` 기준. 단순 상품도 variant 1개.
|
|
72
|
+
`ProductVariant { id, sku, optionSignature, price, compareAtPrice, currency, inStock, available }`
|
|
73
|
+
- `optionSignature` 는 옵션 조합 표시용 문자열("레드/XL") — 단순 상품은 **빈 문자열**이라 그때는 안 그린다.
|
|
74
|
+
- `compareAtPrice` 는 정가(취소선용). null 이면 할인 아님 — **0 과 혼동하지 마라**.
|
|
75
|
+
- 금액 표기는 `currency` 를 따른다. 하드코딩하지 마라.
|
|
67
76
|
- `variant.inStock`(불리언)·`variant.available`(추적 재고 수량, 무한재고면 null)로 재고 표시.
|
|
68
77
|
- `productType`: `PHYSICAL`(배송)·`DIGITAL`(즉시)·`SERVICE`(예약).
|
|
69
78
|
**`SERVICE` 는 장바구니·체크아웃을 타지 않는다** — 별도 예약 흐름(§4.6). 담기 시도는 400 이다.
|
|
@@ -108,11 +117,65 @@ await zalkera.submitInquiry(input, { clientIp: ip });
|
|
|
108
117
|
### 커머스 — 결제·주문·배송
|
|
109
118
|
- `checkout(input, session)` → `OrderDetail` (input: `{ buyerName, buyerPhone, buyerEmail?, shipTo? }`)
|
|
110
119
|
- 주문 생성 시 가격 스냅샷 동결 + 재고 원자적 차감. 재고 부족이면 409. 상태 `PENDING_PAYMENT`.
|
|
111
|
-
- `startPayment(orderNo, access)` → `{ paymentUrl }`
|
|
120
|
+
- `startPayment(orderNo, access)` → `PaymentSession { paymentUrl, pgPaymentId, widget? }`
|
|
121
|
+
- **`widget` 유무가 분기다**: 있으면 위젯형(내 사이트에서 결제창), 없으면 `paymentUrl` 로 리다이렉트(§4.3).
|
|
122
|
+
- `widget` 에는 **브라우저 노출이 전제된 값만** 담긴다(토스: `vendor:"TOSS_PAYMENTS"`·`clientKey`·`orderId`·`amount`·`orderName`). PG secretKey 는 안 들어온다.
|
|
123
|
+
- **키 구성은 벤더마다 다르다** — `widget.vendor` 로 분기해 읽는다.
|
|
124
|
+
- `confirmPayment(orderNo, providerParams, access)` → `void` — **위젯형 전용** 승인 확정.
|
|
125
|
+
성공 콜백 파라미터(`{ paymentKey }` 등)를 그대로 넘긴다. 금액은 안 넘겨도 된다 —
|
|
126
|
+
서버가 **저장된 주문 금액**으로 PG 에 직접 묻는다(브라우저 금액 조작 무효).
|
|
112
127
|
- `getOrder(orderNo, access)` · `listMyOrders(accessToken,{page,size})` · `cancelOrder(orderNo, access)`
|
|
113
128
|
· `completeOrder(orderNo, access)` · `getShipment(orderNo, access)`
|
|
114
129
|
- `access = { accessToken? }`(로그인) **또는 `{ phone? }`(게스트, 주문 시 남긴 연락처)**.
|
|
115
130
|
|
|
131
|
+
### 계약 헬퍼 — 직접 짜지 말고 이걸 부른다
|
|
132
|
+
|
|
133
|
+
이 패키지는 **API 클라이언트이면서 계약 부품의 운반체**다. 아래는 전부 `@zalkera/client` 에서 import 한다 —
|
|
134
|
+
프로젝트 안에 `lib/safeUrl.ts`·`sections/parse.ts` 를 새로 만들지 마라(사본이 갈라지면 수리가 안 퍼진다).
|
|
135
|
+
|
|
136
|
+
- `safeLinkUrl(raw)` → `string` — **모든 `href` 는 이걸 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
|
|
137
|
+
- `parseConfig<T>(config)` → `T | null` — 섹션 `config` JSON 파싱. **절대 throw 하지 않는다**(실패 시 null).
|
|
138
|
+
- `asString(v)` / `asId(v)` — 형이 안 맞으면 `undefined`.
|
|
139
|
+
- `asIdArray(v)` / `asObjectArray(v)` — **빈 배열**로 강하한다(undefined 아님).
|
|
140
|
+
빈 배열은 truthy 라 `if (items)` 로 결측을 판별하면 안 된다 — `items.length` 을 봐라.
|
|
141
|
+
- 위 가드로 **필수 값이 없으면 그 섹션만 안 그린다**. 페이지 전체가 죽으면 안 된다.
|
|
142
|
+
- `mediaSrc(assetId)` → `/media/{id}` **경로 문자열**. 이미지는 전부 이걸 통한다.
|
|
143
|
+
전제: 프로젝트에 `app/media/[id]/route.ts` 프록시 라우트가 있어야 한다(스타터에 있다).
|
|
144
|
+
이 헬퍼는 경로만 만든다 — 라우트를 안 만들면 이미지가 404 다.
|
|
145
|
+
- `SECTION_CONTRACT` / `SECTION_CONTRACT_REV` / `sectionsOfVertical(vertical)` — 아는 섹션 어휘의 코드 표현(§9).
|
|
146
|
+
타입·키를 **지어내지 말고** 여기서 확인한다.
|
|
147
|
+
- `parseThemeColors(raw)` → `ParsedTheme` — 테마 토큰 파서(§8). 화이트리스트라 임의 CSS 가 안 들어온다.
|
|
148
|
+
|
|
149
|
+
### ISR 캐시 태그 (`ReadOptions`)
|
|
150
|
+
|
|
151
|
+
읽기 메서드는 마지막 인자로 `{ tags: [...] }` 를 받는다. 넘기면 그 fetch 에 `next.tags` 가 실려
|
|
152
|
+
**백엔드가 `revalidateTag` 로 온디맨드 무효화**한다 — 테넌트가 콘솔에서 고친 것이 즉시 반영되는 경로다.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const config = await zalkera.getSiteConfig({ tags: ["site-config"] });
|
|
156
|
+
const product = await zalkera.getProduct(slug, { tags: ["products", `product:${slug}`] });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**백엔드가 실제로 발화하는 태그는 둘뿐이다**(실측 — `OperationRegistryChangeApplier`):
|
|
160
|
+
|
|
161
|
+
| 콘솔에서 바뀐 것 | 발화 태그 |
|
|
162
|
+
|---|---|
|
|
163
|
+
| 테마·레이아웃·회사정보(`site.*`) | `site-config` |
|
|
164
|
+
| 상품·가격·재고(`product.*`·`inventory.*`) | `products` |
|
|
165
|
+
| 그 밖(예약·게시물·미지의 op) | 태그 없이 `"/"` 경로 무효화로 폴백 |
|
|
166
|
+
|
|
167
|
+
- **`product:{slug}` 는 기대하지 마라.** 백엔드가 일부러 안 붙인다 — 오퍼레이션 페이로드의 상품 참조가
|
|
168
|
+
고객이 말로 지시한 모호 참조라 slug 와 일치한다는 보장이 없다. 상품 페이지도 `products` 로 받는다.
|
|
169
|
+
- **모든 페이지 fetch 에 `site-config` 를 동승시켜라.** 테마·레이아웃은 전 페이지에 영향인데,
|
|
170
|
+
발화 태그는 `site-config` 하나뿐이라 이걸 안 달면 그 페이지만 옛 테마로 남는다.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// 페이지 라우트 — 자기 태그 + site-config 동승
|
|
174
|
+
const page = await zalkera.getPage(slug, { tags: ["site-config", "pages", `page:${slug}`] });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- **안 넘기면 세그먼트 기본 캐시만 걸린다** — 콘솔에서 고쳐도 화면이 안 바뀐다는 신고가 대개 이것이다.
|
|
178
|
+
|
|
116
179
|
## 4. 레시피
|
|
117
180
|
|
|
118
181
|
### 4.1 상품 목록 페이지 (RSC)
|
|
@@ -154,11 +217,11 @@ export async function POST(req: Request) {
|
|
|
154
217
|
// → 스타터의 checkout/page.tsx(openWidget)·payment/complete/page.tsx 를 그대로 쓴다
|
|
155
218
|
await zalkera.confirmPayment(orderNo, { paymentKey }, access); // BFF 경유
|
|
156
219
|
} else {
|
|
157
|
-
redirect(session.paymentUrl); // 리다이렉트형(
|
|
220
|
+
redirect(session.paymentUrl); // 리다이렉트형(PG 결제창으로 이동)
|
|
158
221
|
}
|
|
159
222
|
```
|
|
160
223
|
`widget` 유무로만 분기한다 — **벤더 이름으로 분기하지 마라**(벤더가 늘어난다).
|
|
161
|
-
3.
|
|
224
|
+
3. PG 결제창에서 결제. **결과는 서버 웹훅으로 백엔드가 처리**(프론트는 관여 안 함).
|
|
162
225
|
4. returnUrl 로 돌아오면 `getOrder(orderNo, access)` 로 상태 확인(잠시 PENDING 일 수 있음 → PAID 폴링/새로고침).
|
|
163
226
|
|
|
164
227
|
> ⚠️ returnUrl·successUrl 콜백의 "성공"을 **신뢰하지 말 것**. 진짜 확정은 백엔드가 한다 — 웹훅(리다이렉트형)
|
|
@@ -283,7 +346,7 @@ if (booking.orderNo) { // status=PENDING
|
|
|
283
346
|
`export const dynamic='force-dynamic'`/`fetch(...,{cache:'no-store'})` **금지**(per-page SSR 유발).
|
|
284
347
|
실시간·개인화 데이터(라이브 재고·개인화)는 **클라이언트 컴포넌트(아일랜드)**로 가져오고, 상태 변경
|
|
285
348
|
(장바구니·주문)은 **BFF route handler** 로 한다. 신선도는 **온디맨드 revalidate**(백엔드 데이터 변경 시
|
|
286
|
-
`POST /api/revalidate`)로 지킨다. 동적 SSR 이 꼭 필요하면(예: 검색) **정당화 주석**(`//
|
|
349
|
+
`POST /api/revalidate`)로 지킨다. 동적 SSR 이 꼭 필요하면(예: 검색) **정당화 주석**(`// zalkera-allow-dynamic:
|
|
287
350
|
<이유>`)이 필요하다 — 서버 동적 렌더는 상시 런타임 원가라 CI validator(§5)가 SEO 라우트에서 강제 차단한다.
|
|
288
351
|
|
|
289
352
|
## 5.1 산출물 규범 — 발견되는 사이트 (필수)
|
|
@@ -350,9 +413,12 @@ catch (e) {
|
|
|
350
413
|
|
|
351
414
|
## 7. 결제 파트너
|
|
352
415
|
|
|
353
|
-
결제·정산은
|
|
416
|
+
결제·정산은 **제3자 PG(결제대행사)** 에 위임한다. 프론트는 카드정보를 절대 다루지 않는다(PCI 범위 밖).
|
|
354
417
|
`startPayment` 가 준 URL 로 보내고, 결과는 백엔드가 웹훅으로 받아 주문 상태를 바꾼다.
|
|
355
418
|
|
|
419
|
+
**어느 PG 인지에 의존하는 분기를 짜지 마라** — PG 는 교체될 수 있다. 프론트가 보는 계약(`startPayment` 가
|
|
420
|
+
주는 리다이렉트 URL, 웹훅 뒤에 바뀌는 주문 상태)은 그대로다.
|
|
421
|
+
|
|
356
422
|
## 8. 스타일 규약 — Tailwind v4 + 테마 토큰 (필수)
|
|
357
423
|
|
|
358
424
|
스타터(템플릿)는 **Tailwind v4** 와 **테마 토큰**으로 스타일한다. 화면을 그릴 때 아래를 지킨다 — 어기면
|
|
@@ -440,23 +506,29 @@ CI validator **S6** 가 좌변 잔존을 error 로 막는다.
|
|
|
440
506
|
|
|
441
507
|
```ts
|
|
442
508
|
const page = await zalkera.getPage("home");
|
|
443
|
-
page.sections // [{
|
|
509
|
+
page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" }, …]
|
|
444
510
|
```
|
|
445
511
|
|
|
446
512
|
**왜 이 구조인가**: 콘텐츠가 마크업에 박히지 않고 계약을 타므로, 테넌트가 콘솔에서·"말로" 고친 것이 재코딩 없이
|
|
447
513
|
반영된다("FAQ 에 배송 질문 추가해줘"). **마크업에 문구를 하드코딩하면 이 경로가 죽는다.**
|
|
448
514
|
|
|
449
515
|
**규약**
|
|
450
|
-
- `config` 는 **문자열**이다. 파싱은
|
|
451
|
-
절대 throw 하지 않는다). 필수 필드가 없으면 **그 섹션만 안 그린다**.
|
|
516
|
+
- `config` 는 **문자열**이다. 파싱은 `parseConfig` + `asString`/`asId`/`asIdArray`/`asObjectArray` 로 한다
|
|
517
|
+
(이 패키지가 export — 절대 throw 하지 않는다). 필수 필드가 없으면 **그 섹션만 안 그린다**.
|
|
518
|
+
페이지 전체가 죽으면 안 된다.
|
|
452
519
|
- **미지 타입은 조용히 건너뛴다.** 어휘는 append-only 라 백엔드가 타입을 늘려도 옛 사이트가 깨지지 않아야 한다.
|
|
453
520
|
`default:` 에서 에러·경고를 내지 마라 — 그게 계약이다.
|
|
454
|
-
- 이미지는 `assetId`(숫자)다. `
|
|
455
|
-
- **모든 href 는 `
|
|
521
|
+
- 이미지는 `assetId`(숫자)다. `mediaSrc(id)`(이 패키지 export)로 프록시 경유해 그린다.
|
|
522
|
+
- **모든 href 는 `safeLinkUrl()` 을 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
|
|
456
523
|
- 자유 HTML 은 없다 — plain text + 줄바꿈뿐(`whitespace-pre-wrap`).
|
|
457
524
|
- 타입·키를 **지어내지 마라.** 계약 정본은 `SECTION_CONTRACT`(이 패키지가 export)이고, 필요한 어휘가 없으면
|
|
458
525
|
만들지 말고 **보고**한다.
|
|
459
526
|
|
|
527
|
+
**디스패치는 직접 짠다** — `type` 으로 컴포넌트를 고르는 `switch` 하나면 된다. 위 규약에 더해 둘을 지켜라:
|
|
528
|
+
|
|
529
|
+
- **`sortOrder` 로 한 번 더 정렬한다.** 서버가 정렬해 주지만 순서가 틀리면 화면에서 바로 티가 나는 종류다.
|
|
530
|
+
- **key 는 `type`+`sortOrder`+index 로 짠다.** `PageSection` 계약에 id 가 없어(실측) 순수 인덱스보다 낫다.
|
|
531
|
+
|
|
460
532
|
**어휘 (12종)** — `vertical` 은 콘솔 픽커 그룹핑용이지 사용 제한이 아니다(GENERAL 은 뷰티 테넌트도 쓴다).
|
|
461
533
|
|
|
462
534
|
| type | vertical | config (필수는 굵게) | JSON-LD |
|
package/package.json
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
"name": "@zalkera/client",
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Credium Co., Ltd.",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"zalkera",
|
|
13
|
+
"headless",
|
|
14
|
+
"commerce",
|
|
15
|
+
"cms",
|
|
16
|
+
"api-client",
|
|
17
|
+
"sdk"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://zalkera.com",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"llms.txt"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"tsup": "^8.3.5",
|
|
48
|
+
"typescript": "^5.7.2",
|
|
49
|
+
"vitest": "^2.1.8"
|
|
32
50
|
}
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsup",
|
|
36
|
-
"dev": "tsup --watch",
|
|
37
|
-
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"test:watch": "vitest",
|
|
40
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
41
|
-
},
|
|
42
|
-
"engines": {
|
|
43
|
-
"node": ">=18"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"tsup": "^8.3.5",
|
|
47
|
-
"typescript": "^5.7.2",
|
|
48
|
-
"vitest": "^2.1.8"
|
|
49
|
-
}
|
|
50
51
|
}
|