@zalkera/client 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +18 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +18 -13
- package/dist/index.js.map +1 -1
- package/llms.txt +44 -13
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -142,7 +142,7 @@ await zalkera.submitInquiry(input, { clientIp: ip });
|
|
|
142
142
|
빈 배열은 truthy 라 `if (items)` 로 결측을 판별하면 안 된다 — `items.length` 을 봐라.
|
|
143
143
|
- 위 가드로 **필수 값이 없으면 그 섹션만 안 그린다**. 페이지 전체가 죽으면 안 된다.
|
|
144
144
|
- `mediaSrc(assetId)` → `/media/{id}` **경로 문자열**. 이미지는 전부 이걸 통한다.
|
|
145
|
-
전제: 프로젝트에 `app/media/[id]/route.ts` 프록시 라우트가 있어야 한다(
|
|
145
|
+
전제: 프로젝트에 `app/media/[id]/route.ts` 프록시 라우트가 있어야 한다(템플릿에 있다).
|
|
146
146
|
이 헬퍼는 경로만 만든다 — 라우트를 안 만들면 이미지가 404 다.
|
|
147
147
|
- `SECTION_CONTRACT` / `SECTION_CONTRACT_REV` / `sectionsOfVertical(vertical)` — 아는 섹션 어휘의 코드 표현(§9).
|
|
148
148
|
타입·키를 **지어내지 말고** 여기서 확인한다.
|
|
@@ -216,7 +216,7 @@ export async function POST(req: Request) {
|
|
|
216
216
|
if (session.widget) {
|
|
217
217
|
// 위젯형(토스) — 내 사이트에서 결제창을 띄우고, 성공 콜백 파라미터를 confirm 으로 넘긴다
|
|
218
218
|
// widget = { vendor:"TOSS_PAYMENTS", clientKey, orderId, amount, orderName }
|
|
219
|
-
// →
|
|
219
|
+
// → 템플릿의 checkout/page.tsx(openWidget)·payment/complete/page.tsx 를 그대로 쓴다
|
|
220
220
|
await zalkera.confirmPayment(orderNo, { paymentKey }, access); // BFF 경유
|
|
221
221
|
} else {
|
|
222
222
|
redirect(session.paymentUrl); // 리다이렉트형(PG 결제창으로 이동)
|
|
@@ -240,7 +240,7 @@ export async function POST(req: Request) {
|
|
|
240
240
|
>
|
|
241
241
|
> 관용구는 소비자에 따라 둘이다:
|
|
242
242
|
> - **브라우저(쿠키 있음)** = `` `co-${session.cartSessionKey}` `` **+ 체크아웃 성공 응답에서 카트 쿠키 회전**
|
|
243
|
-
> (
|
|
243
|
+
> (템플릿 `rotateCartSessionKey`). **회전 없이 카트키를 멱등키로 쓰지 마라** — 카트 쿠키는 30일이라
|
|
244
244
|
> 회전이 없으면 위의 "영구 재사용"이 그대로 실현된다(실측 사고). 회전을 응답에 실으면 Set-Cookie 가
|
|
245
245
|
> 성공 도달과 원자적으로 묶여서, 응답 유실 시엔 옛 키가 살아남아 재시도가 원주문을 재생한다 —
|
|
246
246
|
> **인메모리 "시도당 키"보다 이 쪽이 안전하다**(새로고침에 안 죽는다).
|
|
@@ -275,7 +275,7 @@ const shipment = await zalkera.getShipment(orderNo, { phone }); // 배송 상태
|
|
|
275
275
|
// 1) 슬롯 보여주기 — 공개(비로그인 가능).
|
|
276
276
|
// ⚠️ 이 RSC 직독 샘플은 **요청마다 렌더되는 동적 라우트에서만** 신선하다. 상품 상세가 ISR
|
|
277
277
|
// (force-static + revalidate=N)이면 슬롯이 프리렌더에 구워져 N초 낡은 시간표를 보여주고
|
|
278
|
-
// SLOT_FULL 을 만든다 — 그땐 **클라이언트 아일랜드 + BFF 프록시**로 내려라(
|
|
278
|
+
// SLOT_FULL 을 만든다 — 그땐 **클라이언트 아일랜드 + BFF 프록시**로 내려라(템플릿
|
|
279
279
|
// BookingPanel + /api/booking/availability 가 그 선례다. 볼라틸한 데이터는 클라이언트 아일랜드로).
|
|
280
280
|
const slots = await zalkera.availability({
|
|
281
281
|
productId: product.id, // slug 아님 — getProduct(slug).id
|
|
@@ -359,11 +359,40 @@ if (booking.orderNo) { // status=PENDING
|
|
|
359
359
|
- ✅ **SEO 페이지에 JSON-LD(schema.org) 필수.** 상품 상세 = `Product` + variant 마다 `Offer`(price·
|
|
360
360
|
priceCurrency·availability) + 후기 있으면 `AggregateRating`. 홈 = `Organization`(오프라인 점포면
|
|
361
361
|
`LocalBusiness`, 뷰티샵이면 `BeautySalon` 으로 좁힌다). 목록·상세엔 `BreadcrumbList`.
|
|
362
|
-
|
|
362
|
+
템플릿의 `src/components/JsonLd.tsx`(안전 직렬화 + `productJsonLd`/`organizationJsonLd`/
|
|
363
363
|
`breadcrumbJsonLd` 헬퍼)를 **그대로 쓴다 — 재발명 금지**.
|
|
364
|
-
- ✅
|
|
365
|
-
|
|
366
|
-
|
|
364
|
+
- ✅ **목록 라우트도 그래프를 낸다 — `ItemList`.** 상품 목록(`/products`)·글 목록(`/blog`)은 **화면에 그리는
|
|
365
|
+
바로 그 순서·그 항목**으로 `ItemList` 를 낸다(`itemListJsonLd`). 왜 목록에도 그래프가 필요한가: 상세 N건만
|
|
366
|
+
있으면 "이 가게가 무엇을 파는가"를 기계가 한 번에 못 받고 상세를 하나씩 발견해야 한다 — 목록은 그 N건을
|
|
367
|
+
한 노드로 묶어 주는 자리이자 크롤러가 상세로 들어가는 내부 링크 허브다. 각 `ListItem` 은 `url` 로 상세를
|
|
368
|
+
가리키고 **이름만** 들고 가는 요약형으로 둔다: 가격·재고를 목록에 복제하면 같은 사실이 두 곳에 살다가
|
|
369
|
+
갈라지고, 갈라진 쪽이 거짓이 된다 — 가격의 정본은 상세의 `Offer` 다. **항목이 0건이면 `ItemList` 자체를
|
|
370
|
+
내지 않는다**(빈 목록을 그래프로 주장하지 않는다 — "페이지에 없는 것을 쓰지 마라"의 목록판). 목록에도
|
|
371
|
+
`BreadcrumbList` 를 함께 낸다. 그리고 목록은 **ISR 로 유지한다** — 정렬·필터를 `searchParams` 로 받는 순간
|
|
372
|
+
라우트가 동적 렌더로 강등돼 크롤러 방문마다 서버 렌더가 돈다. 더 넓히려면 `/products/page/[n]` 같은
|
|
373
|
+
**정적 세그먼트**로 늘려라. 본보기: `src/app/products/page.tsx` · `src/app/blog/page.tsx`.
|
|
374
|
+
- ✅ **예약(시술) 사이트의 목록 보장은 `SERVICE_MENU` 섹션이 낸다.** 섹션 어휘 `SERVICE_MENU` 는 계약상
|
|
375
|
+
`jsonLd: "ItemList"` 이고(`SECTION_CONTRACT` · `contractRev` 2 이상), config 의 `productIds` **순서 그대로**
|
|
376
|
+
목록 그래프를 낸다 — 원장이 정한 노출 순서를 기계에도 같은 순서로 준다. 시술 목록의 정위치는 **홈의 이
|
|
377
|
+
섹션**이지 별도 라우트가 아니다: 뷰티 사이트에 목록 라우트를 강제하는 것은 디자인 자유를 깎으면서 얻는
|
|
378
|
+
것이 없어서, 예약 유형의 목록 보장은 이 산출로 충족한다(`FAQ_LIST`↔`FAQPage` 와 같은 선례 — 목록의 정본이
|
|
379
|
+
그 섹션의 배열이라 다른 데서 다시 만들면 두 벌이 되고 갈라진다). 본보기:
|
|
380
|
+
`src/components/sections/ServiceMenuSection.tsx`.
|
|
381
|
+
- ✅ **CMS 고정 페이지(`/[slug]`)를 비워 두지 마라 — `WebPage` + `BreadcrumbList`.** 홈은 `Organization`,
|
|
382
|
+
상품은 `Product`, 글은 `BlogPosting` 을 내는데 콘솔·시드가 만든 서브페이지(회사소개·이용안내 등)만 그래프가
|
|
383
|
+
비기 쉽다. 순수 마케팅 사이트는 **그 서브페이지가 콘텐츠의 전부**라, 비어 있으면 답변 엔진이 인용할 노드가
|
|
384
|
+
홈 하나뿐이 된다. `name` 은 페이지가 그리는 `<h1>` 과 같은 값으로, `description` 은 SEO 오버라이드가
|
|
385
|
+
**실제로 있을 때만** 붙인다(`datePublished`·`author`·`image` 는 그 페이지에 그런 것이 없으므로 내지 않는다).
|
|
386
|
+
서브페이지의 `BreadcrumbList` 는 홈 아래 1뎁스(`홈 > 회사소개`)다. 본보기: `src/app/[slug]/page.tsx` ·
|
|
387
|
+
`webPageJsonLd`.
|
|
388
|
+
- ✅ **`sitemap.ts`·`robots.ts` 필수.** sitemap 엔 **실제로 존재하는 공개 라우트만** — 홈 · **목록 라우트
|
|
389
|
+
(`/products`·`/blog`)** · 상품 상세 · 글 상세 · 콘텐츠 페이지. 목록 라우트를 빼면 카탈로그의 허브가 크롤러에게
|
|
390
|
+
안 알려져 상세 N건을 개별 발견에만 맡기게 된다. 반대로 **비어 있는 목록은 싣지 않는다** — 상품 0건에
|
|
391
|
+
`/products` 를, 글 0건에 `/blog` 를 실으면 크롤러에게 빈 페이지를 색인시키는 것이라 위 `ItemList` 규칙과
|
|
392
|
+
같은 판단이다(둘 다 "없는 것을 있다고 말하지 않는다"). 본보기 `src/app/sitemap.ts` 가 목록 API 를 훑어
|
|
393
|
+
이 조건부 등재를 한다. robots 는 세션·쓰기 경로(`/api/`·`/cart`·`/checkout`·`/mypage`·`/orders`·`/login`·
|
|
394
|
+
`/auth`)만 막고 공개 카탈로그는 전부 연다. **AI 크롤러(GPTBot·ClaudeBot 등)를 막지 마라** — 발견 경로를
|
|
395
|
+
우리 손으로 닫는 것이다.
|
|
367
396
|
- ✅ **절대 URL.** JSON-LD·sitemap·robots 는 상대경로 불가. 단일 사이트는 env(`ZALKERA_SITE_URL`),
|
|
368
397
|
멀티테넌트는 요청 호스트에서 만든다.
|
|
369
398
|
- ✅ **ISR 유지.** JSON-LD 를 넣는다고 페이지를 동적으로 만들지 마라 — 전부 프리렌더 값이다(§5 ISR-우선과 한 몸).
|
|
@@ -371,7 +400,7 @@ if (booking.orderNo) { // status=PENDING
|
|
|
371
400
|
- ❌ **페이지에 없는 것을 JSON-LD 에 쓰지 마라.** 구조화 데이터는 **보이는 내용만** 서술한다. 후기 0건인데
|
|
372
401
|
`aggregateRating`, 렌더하지도 않는 `image`, 지어낸 브랜드·재고는 전부 구조화 데이터 정책 위반(리치결과 박탈).
|
|
373
402
|
값이 없으면 **그 필드를 통째로 뺀다**(널·0 을 넣지 않는다).
|
|
374
|
-
- ✅ **상품 이미지는 `/media/{id}` 안정 URL 로.**
|
|
403
|
+
- ✅ **상품 이미지는 `/media/{id}` 안정 URL 로.** 템플릿의 `src/app/media/[id]/route.ts` 가 `X-Tenant` 를 붙여
|
|
375
404
|
백엔드를 부르고 302 Location 만 넘긴다(바이트는 스토리지→브라우저 직행 — Next 런타임에 태우지 마라).
|
|
376
405
|
`<img src={`/media/${product.coverAssetId}`} loading="lazy">` + JSON-LD `image` 둘 다 이 URL 을 쓴다.
|
|
377
406
|
`next/image` 는 최적화 프록시가 바이트를 런타임에 태우므로 금지(쓰려면 `unoptimized`).
|
|
@@ -430,7 +459,7 @@ catch (e) {
|
|
|
430
459
|
|
|
431
460
|
## 8. 스타일 규약 — Tailwind v4 + 테마 토큰 (필수)
|
|
432
461
|
|
|
433
|
-
|
|
462
|
+
템플릿은 **Tailwind v4** 와 **테마 토큰**으로 스타일한다. 화면을 그릴 때 아래를 지킨다 — 어기면
|
|
434
463
|
생성물이 초라해지거나(생짜 HTML) 테넌트의 "말로 색 바꾸기"가 깨진다. CI validator(S1~S5)가 강제한다.
|
|
435
464
|
|
|
436
465
|
**스택 — 이것만 쓴다**
|
|
@@ -530,8 +559,10 @@ page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" },
|
|
|
530
559
|
- 이미지는 `assetId`(숫자)다. `mediaSrc(id)`(이 패키지 export)로 프록시 경유해 그린다.
|
|
531
560
|
- **모든 href 는 `safeLinkUrl()` 을 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
|
|
532
561
|
- 자유 HTML 은 없다 — plain text + 줄바꿈뿐(`whitespace-pre-wrap`).
|
|
533
|
-
- 타입·키를 **지어내지 마라.** 계약 정본은 `
|
|
534
|
-
|
|
562
|
+
- 타입·키를 **지어내지 마라.** 계약 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고,
|
|
563
|
+
이 패키지가 export 하는 `SECTION_CONTRACT` 는 그것을 npm 으로 실어 나르는 **운반체**다(`src/sections.ts`
|
|
564
|
+
KDoc 이 같은 말을 한다). 코드에서 읽을 것은 `SECTION_CONTRACT` 가 맞지만, 어휘를 **늘리는** 결정은
|
|
565
|
+
정본 쪽에서 난다 — 필요한 어휘가 없으면 만들지 말고 **보고**한다.
|
|
535
566
|
|
|
536
567
|
**디스패치는 직접 짠다** — `type` 으로 컴포넌트를 고르는 `switch` 하나면 된다. 위 규약에 더해 둘을 지켜라:
|
|
537
568
|
|
|
@@ -560,7 +591,7 @@ page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" },
|
|
|
560
591
|
SSR 마크업에 실린다**(AI 답변엔진이 읽는다). 아코디언 라이브러리를 쓰지 마라. `FAQPage` JSON-LD 를 함께 낸다.
|
|
561
592
|
- `TESTIMONIALS` 에 **`Review`·`AggregateRating` 을 내지 마라.** 자사 사이트의 자사 후기에 별점을 붙이는 것은
|
|
562
593
|
self-serving reviews 정책 위반이라 제재 대상이다. 이건 빠뜨린 게 아니라 결정이다.
|
|
563
|
-
- `LEAD_CTA` 는
|
|
594
|
+
- `LEAD_CTA` 는 템플릿의 `components/LeadForm` 을 그대로 쓴다(UTM·클릭ID 추적·레이트리밋 안내 동봉).
|
|
564
595
|
전환 부품을 두 벌 만들지 마라. 이 섹션은 **`id="lead"` 앵커**를 갖는다 — 원페이지 랜딩에서 히어로 CTA 를
|
|
565
596
|
`"ctaHref": "#lead"` 로 여기에 걸 수 있다. 앵커를 지우면 그 버튼이 조용히 아무 데도 안 간다.
|
|
566
597
|
- `LOGO_WALL` 에 실존 기업 로고를 넣지 마라 — 사용 허락이 있는 것만.
|