@zalkera/client 0.13.0 → 0.13.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.
Files changed (2) hide show
  1. package/llms.txt +181 -20
  2. package/package.json +1 -1
package/llms.txt CHANGED
@@ -62,6 +62,9 @@ await zalkera.submitInquiry(input, { clientIp: ip });
62
62
  - `getSiteConfig()` — 회사명·연락처·테마·SEO 기본값
63
63
  - `listCategories()` · `listPosts({category?,page?,size?,sort?})` · `getPost(slug)` · `recordPostView(slug,ctx)`
64
64
  - `getPage(slug)` · `listPages({page?,size?})`(열거 전용·본문 없음·sitemap 용·size 상한 100) · `listMenus()` (HEADER/FOOTER) · `getMediaUrl(id)`
65
+ - ⚠ 이 셋(`getPage`·`listPages`·`listMenus`)은 **섹션이 DB 에 있는 사이트**(`"content": "sections-db"`)의 표면이다.
66
+ **새로 만드는 사이트는 안 부른다** — 페이지·섹션·내비의 정본이 레포 파일이라 백엔드 왕복이 아예 없다(§4.8·§9.1).
67
+ 계약은 append-only 라 메서드는 남지만, 소스 정본 레포에서 이걸 부르면 빈 결과로 화면이 비는 쪽이 정상이다.
65
68
  - `submitInquiry(input, ctx)` · `submitLead(input, ctx)` ← ctx.clientIp 필수(서버)
66
69
 
67
70
  ### 커머스 — 카탈로그(공개)
@@ -345,6 +348,70 @@ if (booking.orderNo) { // status=PENDING
345
348
  홉을 `{ clientIp }` 로 넘긴다(안 넘기면 방문자 전원 429 — §4.6·문의와 동일 관용구).
346
349
  - 선례: `components/LeadForm.tsx`(아일랜드·UTM 캡처) · `app/api/lead/route.ts`(BFF·201 관통).
347
350
 
351
+ ### 4.8 콘텐츠 파일로 그리는 고정 페이지 (`"content": "source"` · 권장)
352
+
353
+ 사이트의 얼굴을 레포가 정본으로 가질 때의 배선이다. 파일 형상·방언은 §9.1·§9.2, 아래는 그것을 읽는 코드다.
354
+ **라우트는 하나면 된다** — 페이지가 늘어도 `content/` 에 json 이 느는 것이지 라우트가 늘지 않는다.
355
+
356
+ ```ts
357
+ // content/index.ts — 이 디렉터리의 유일한 코드. **정적 import** 라야 dev HMR + standalone 트레이싱을 얻는다.
358
+ import home from "./pages/home.json";
359
+ import about from "./pages/about.json";
360
+ import nav from "./nav.json";
361
+
362
+ // 값을 unknown 으로 둔다: 손으로 고치는 파일이라 형상이 틀릴 수 있고, 판정·강하는 로더 한 곳이 한다.
363
+ // 여기서 타입을 주장하면 틀린 json 하나가 **빌드를 세운다** — 계약은 "틀린 부분만 안 그린다" 다.
364
+ export const pages: Record<string, unknown> = { home, about };
365
+ export { nav };
366
+ ```
367
+
368
+ ```ts
369
+ // lib/content.ts — **절대 throw 하지 않는다.**
370
+ import { asString } from "@zalkera/client";
371
+ import { pages } from "../content";
372
+
373
+ export interface ContentSection { type: string; config: unknown } // config 는 **객체**다
374
+
375
+ export function loadPageContent(slug: string) {
376
+ const raw = pages[slug];
377
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
378
+ const r = raw as Record<string, unknown>;
379
+ const sections = (Array.isArray(r.sections) ? r.sections : []).flatMap((s): ContentSection[] => {
380
+ if (s == null || typeof s !== "object") return [];
381
+ const type = asString((s as Record<string, unknown>).type)?.trim();
382
+ return type ? [{ type, config: (s as Record<string, unknown>).config }] : []; // config 는 그대로 넘긴다
383
+ });
384
+ return { title: asString(r.title)?.trim() || slug, seo: r.seo, sections };
385
+ }
386
+
387
+ export const pageSlugs = (): string[] => Object.keys(pages); // sitemap·generateStaticParams 가 읽는다
388
+ ```
389
+
390
+ ```tsx
391
+ // app/[slug]/page.tsx — 전 페이지를 이 하나가 그린다. 백엔드 왕복이 없다.
392
+ export const dynamic = "force-static";
393
+ export function generateStaticParams() { return pageSlugs().map((slug) => ({ slug })); }
394
+
395
+ export default async function StaticPage({ params }: { params: Promise<{ slug: string }> }) {
396
+ const { slug } = await params;
397
+ const page = loadPageContent(slug);
398
+ if (!page) notFound();
399
+ if (slug === "home") redirect("/"); // 홈의 정본 주소는 루트다(같은 내용이 두 URL 로 색인되면 손해)
400
+ return (
401
+ <main>
402
+ <JsonLd data={webPageJsonLd({ title: page.title, slug }, siteUrl())} />
403
+ <h1>{page.title}</h1>
404
+ <SectionList sections={page.sections} /> {/* 정렬하지 않는다 — 배열 순서가 화면 순서다 */}
405
+ </main>
406
+ );
407
+ }
408
+ ```
409
+
410
+ - 상품 참조 섹션(`SERVICE_MENU`·`BOOKING_CTA`)이 있으면 `SectionList` 가 `listProducts` 를 **1회** 부르고
411
+ `slug`(= handle)를 키로 맵을 만들어 각 섹션에 넘긴다. 조회가 실패해도 페이지는 산다 — 그 섹션만 빠진다.
412
+ - 홈(`app/page.tsx`)은 `loadPageContent("home")` 의 섹션이 있으면 그것을 그리고, 없으면 커머스 골격으로
413
+ 강하한다. 콘텐츠 없는 것은 **정상**이다(커머스 테넌트).
414
+
348
415
  ## 5. 흔한 실수(하지 말 것)
349
416
 
350
417
  - ❌ 클라이언트 컴포넌트에서 `@zalkera/client` import → baseUrl 노출. ✅ 서버에서만.
@@ -360,6 +427,14 @@ if (booking.orderNo) { // status=PENDING
360
427
  - ❌ **`var(--oneq-*)` 참조**(정의처 없는 죽은 레거시 토큰). ✅ 테넌트 색은 토큰 유틸리티(`bg-primary`·
361
428
  `text-primary` 등, §8)로 쓴다.
362
429
  - ❌ 서버에서 `submitInquiry`/`submitLead`/`recordPostView` 부를 때 clientIp 누락 → 방문자 전원 rate-limit.
430
+ - ❌ **콘텐츠 파일 사이트에서 페이지를 라우트로 신설**(`app/오시는길/page.tsx` 를 새로 짜기). ✅ `content/pages/<slug>.json`
431
+ **+ 매니페스트 1행**이면 끝이고 라우팅·sitemap 은 이미 있다(§4.8). 라우트를 새로 짜면 그 페이지만 계약 밖으로
432
+ 나가 다음번 "말로 고치기"가 다시 tsx 탐색이 된다 — 실측된 회귀다.
433
+ - ❌ **소스 파일에 숫자 id 적기**(`"assetId": 12`·`"productIds": [3,7]`). ✅ 소스 방언으로 — 에셋은 `public/`
434
+ 루트 절대 경로, 상품은 handle(§9.2). 숫자 id 는 테넌트 스코프라 그 소스를 재업로드하면 의미를 잃는다.
435
+ - ❌ **콘텐츠 파일에 `sortOrder` 를 넣거나 읽은 뒤 정렬**. ✅ 배열 순서가 곧 화면 순서다(§9.1).
436
+ - ❌ **콘텐츠 json 을 런타임 `fs.readFile` 로 읽기.** ✅ `content/index.ts` 정적 import — fs 로 읽으면 dev HMR 이
437
+ 안 돌고 `next build`(standalone) 산출물에서 페이지가 통째로 사라진다.
363
438
  - ❌ **상품·후기·게시글 등 도메인 데이터를 하드코딩**(리터럴 배열·목업 후기 등). ✅ 반드시 `@zalkera/client`
364
439
  로 조회한다. 계약에 없는 데이터(예: slug→productId 매핑이 없어 후기를 못 붙임)면 **하드코딩으로 때우지
365
440
  말고 그 사실을 보고**한다. 하드코딩한 데이터는 프리뷰·실사이트에서 실데이터와 갈라져 첫인상을 죽인다.
@@ -392,7 +467,8 @@ if (booking.orderNo) { // status=PENDING
392
467
  라우트가 동적 렌더로 강등돼 크롤러 방문마다 서버 렌더가 돈다. 더 넓히려면 `/products/page/[n]` 같은
393
468
  **정적 세그먼트**로 늘려라. 본보기: `src/app/products/page.tsx` · `src/app/blog/page.tsx`.
394
469
  - ✅ **예약(시술) 사이트의 목록 보장은 `SERVICE_MENU` 섹션이 낸다.** 섹션 어휘 `SERVICE_MENU` 는 계약상
395
- `jsonLd: "ItemList"` 이고(`SECTION_CONTRACT` · `contractRev` 2 이상), config 의 `productIds` **순서 그대로**
470
+ `jsonLd: "ItemList"` 이고(`SECTION_CONTRACT` · `contractRev` 2 이상), config 의 상품 참조 배열
471
+ (DB 방언 `productIds` · 소스 방언 `products` — §9.2) **순서 그대로**
396
472
  목록 그래프를 낸다 — 원장이 정한 노출 순서를 기계에도 같은 순서로 준다. 시술 목록의 정위치는 **홈의 이
397
473
  섹션**이지 별도 라우트가 아니다: 뷰티 사이트에 목록 라우트를 강제하는 것은 디자인 자유를 깎으면서 얻는
398
474
  것이 없어서, 예약 유형의 목록 보장은 이 산출로 충족한다(`FAQ_LIST`↔`FAQPage` 와 같은 선례 — 목록의 정본이
@@ -573,39 +649,120 @@ CI validator **S6** 가 좌변 잔존을 error 로 막는다.
573
649
 
574
650
  ## 9. 섹션 어휘 — 페이지는 데이터로 그린다
575
651
 
576
- 고정 페이지(`getPage(slug)`)는 본문 HTML 있는 아니라 **섹션 배열**을 갖는다. 각 섹션은 `type`(코드 enum)과
577
- `config`(타입별 JSON)뿐이고, 백엔드는 config 를 **파싱하지 않는다** — 스키마는 프론트 계약이다.
652
+ 고정 페이지는 본문 HTML 덩어리가 아니라 **섹션 배열**이다. 각 섹션은 `type`(코드 enum)과 `config`(타입별
653
+ JSON)뿐이고, 백엔드는 config 를 **파싱하지 않는다** — 스키마는 프론트 계약이다.
654
+
655
+ **왜 이 구조인가**: 콘텐츠가 마크업에 박히지 않고 계약을 타므로, 테넌트가 "말로" 고친 것이 재코딩 없이
656
+ 반영된다("FAQ 에 배송 질문 추가해줘"). **마크업에 문구를 하드코딩하면 이 경로가 죽는다** — 문구 한 줄을
657
+ 고치는 일이 컴포넌트 트리 탐색이 되고, 그 탐색이 곧 토큰이다.
658
+
659
+ **그 배열이 사는 곳은 둘이고, 어느 쪽인지는 레포가 선언한다.**
660
+
661
+ ```json
662
+ // package.json
663
+ "zalkera": { "styling": "tailwind-tokens", "content": "source" }
664
+ ```
665
+
666
+ | 선언 | 의미 |
667
+ |---|---|
668
+ | `"source"` | 사이트의 얼굴(페이지·섹션·문구·섹션 이미지·내비)의 정본이 **레포 파일**. **템플릿과 v2 팩 태생이 이 형상**이다(카탈로그에 v1 팩이 남아 있는 동안 그쪽 태생은 `sections-db` 다) |
669
+ | `"sections-db"` | 전환기 표기 — 섹션이 백엔드에 있고 `getPage(slug)` 가 준다(구 프리셋 태생 사이트) |
670
+ | 미선언 | 안전 기본 — **선언이 없으면 이 계약을 안 쓰는 레포로 본다**(계약 검사도 걸지 않는다) |
671
+
672
+ > ⚠ 이 선언은 **지금은 계약 표기일 뿐**이다. 콘솔이 이 값을 읽어 페이지·섹션 폼을 감추는 배선은
673
+ > **아직 서지 않았다** — 그때까지는 선언과 무관하게 콘솔 폼이 보인다. 레시피가 실물을 앞지르지
674
+ > 않으려고 그대로 적는다.
675
+
676
+ 두 거처는 **같은 어휘·같은 config 키**를 쓴다. 다른 것은 참조를 적는 방언 하나뿐이다(§9.2).
677
+
678
+ ### 9.1 소스 정본 (`"content": "source"`) — 권장 형상
679
+
680
+ ```
681
+ content/
682
+ index.ts # 이 디렉터리의 유일한 코드 — 페이지 json 을 정적 import 해 slug→맵으로 내놓는다
683
+ nav.json # { "header": [{label, href}], "footer": [...] }
684
+ pages/home.json # 페이지당 1파일. 키가 곧 URL 경로(home → /, about → /about)
685
+ ```
686
+
687
+ ```json
688
+ // content/pages/home.json
689
+ {
690
+ "title": "홈",
691
+ "seo": { "title": "…", "description": "…" },
692
+ "sections": [
693
+ { "type": "HERO", "config": { "title": "…", "asset": "/images/hero.png" } },
694
+ { "type": "SERVICE_MENU", "config": { "products": ["care-basic", "gel-onecolor"] } }
695
+ ]
696
+ }
697
+ ```
698
+
699
+ - **배열 순서가 화면 순서다.** `sortOrder` 키는 **없다** — 정렬하지 마라. 순서를 정하는 곳이 둘이면
700
+ "후기를 위로 올려줘"가 매번 어느 쪽을 고치는지 판별 문제가 된다.
701
+ - **`config` 는 객체다**(문자열이 아니다). 두 거처를 하나의 소비자로 읽으려면 `readConfig` 를 쓴다 —
702
+ 문자열이든 객체든 받는다.
703
+ - **정적 import 매니페스트를 우회하지 마라.** 런타임 `fs` 로 읽으면 ⑴ dev 에서 json 을 고쳐도 HMR 이
704
+ 안 돌고(확인이 비싸지면 재시도가 늘고 그게 곧 토큰이다) ⑵ `next build`(standalone) 산출물에 콘텐츠가
705
+ 트레이싱되지 않아 **개시된 사이트에서만** 페이지가 사라진다.
706
+ - **페이지 신설 = json 1개 + 매니페스트 1행.** 라우트를 새로 짜지 마라 — `src/app/[slug]/page.tsx` 하나가
707
+ 전 페이지를 그리고 `src/app/sitemap.ts` 가 매니페스트에서 등재한다. 라우트를 새로 짜면 그 페이지만
708
+ 계약 밖으로 나가 "말로 고치기"가 다시 tsx 탐색이 된다.
709
+ - **로더는 절대 throw 하지 않는다.** json 은 사람과 AI 가 손으로 고치는 파일이라 형상이 틀릴 수 있다 —
710
+ 틀린 곳만 안 그려지고 페이지는 산다. 문구 하나 잘못 고쳤다고 사이트가 500 이면 "말로 고친다"가 성립 안 한다.
711
+ - 이 계약을 **안 지킨 레포도 정상으로 돈다.** 문구를 tsx 에 직접 든 레포도 개시·발행·"말로 고치기"가
712
+ 전부 동작한다 — 강제가 아니라 권장이고, 검사는 위 선언을 **스스로 했을 때만** 격상된다.
713
+
714
+ ### 9.2 참조 방언 — 소스는 숫자 id 를 못 쓴다
715
+
716
+ DB 는 자기가 발급한 숫자 id 를 쓰고, **소스는 그 id 를 알 수 없으므로** 사람이 읽고 쓰는 참조를 쓴다.
717
+ **거처가 방언을 정한다.**
718
+
719
+ | 참조 | DB 방언(`sections-db`) | 소스 방언(`source`) |
720
+ |---|---|---|
721
+ | 에셋 | `assetId`·`*AssetId` : number → `mediaSrc(id)` | `asset`·`*Asset` : `"/images/hero.png"` → `assetPath(v)` |
722
+ | 상품 1개 | `productId` : number | `product`·`*Product` : handle 문자열 |
723
+ | 상품 배열 | `productIds` : number[] | `products`·`*Products` : handle 문자열 배열 |
724
+
725
+ - **handle = `ProductSummary.slug`.** 소스가 DB 카탈로그를 가리키는 **유일한 안정 키**다.
726
+ - ❌ **소스에 숫자 id 를 적지 마라.** 테넌트 스코프 값이라, 고객이 그 소스를 소유하고 다른 테넌트에
727
+ 재업로드하는 순간 의미를 잃는다.
728
+ - `assetPath` 는 **레포 루트 절대 경로만** 통과시킨다(스킴·`//`·`..`·역슬래시 → `undefined`). 이미지 실물은
729
+ 레포 `public/` 에 둔다. 원격 이미지는 계약 영역이 아니라 자유 영역이다.
730
+ - `asHandleArray` 는 **배열 순서를 보존한다** — 그 순서가 노출 순서의 원장이다.
731
+ - 상품 상세의 커버(`coverAssetId`)는 소스 방언에서도 여전히 숫자 id 다. 그건 카탈로그가 발급한 값이지
732
+ 소스가 적는 값이 아니다 — 섹션 이미지와 상품 이미지는 다른 축이다.
733
+ - **아래 §9.4 어휘 표의 `config` 선언은 DB 방언 표기다.** 소스에 적을 때 이 표로 옮겨 적는다.
734
+ 키의 **의미**는 두 방언이 같다.
735
+
736
+ ### 9.3 DB 정본 (`"content": "sections-db"`) — 전환기
578
737
 
579
738
  ```ts
580
739
  const page = await zalkera.getPage("home");
581
740
  page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" }, …]
582
741
  ```
583
742
 
584
- **왜 구조인가**: 콘텐츠가 마크업에 박히지 않고 계약을 타므로, 테넌트가 콘솔에서·"말로" 고친 것이 재코딩 없이
585
- 반영된다("FAQ 배송 질문 추가해줘"). **마크업에 문구를 하드코딩하면 경로가 죽는다.**
743
+ - `config` **문자열**이다(컬럼 저장 형상). `parseConfig` 또는 `readConfig` 읽는다.
744
+ - **`sortOrder` 정렬한다.** 서버가 정렬해 주지만 순서가 틀리면 화면에서 바로 티가 난다.
745
+ - **key 는 `type`+`sortOrder`+index 로 짠다.** `PageSection` 계약에 id 가 없다(실측).
746
+
747
+ ### 9.4 어휘 (12종) — 두 거처 공통
586
748
 
587
749
  **규약**
588
- - `config` **문자열**이다. 파싱은 `parseConfig` + `asString`/`asId`/`asIdArray`/`asObjectArray` 로 한다
589
- (이 패키지가 export 절대 throw 하지 않는다). 필수 필드가 없으면 **그 섹션만 안 그린다**.
590
- 페이지 전체가 죽으면 안 된다.
591
- - **미지 타입은 조용히 건너뛴다.** 어휘는 append-only 라 백엔드가 타입을 늘려도 옛 사이트가 깨지지 않아야 한다.
750
+ - 파싱 헬퍼는 패키지가 준다: `readConfig`/`parseConfig` + `asString`/`asId`/`asIdArray`/`asObjectArray`
751
+ + 방언 헬퍼 `asHandle`/`asHandleArray`/`assetPath`. **절대 throw 하지 않는다** 필수 필드가 없으면
752
+ **그 섹션만 안 그린다**. 페이지 전체가 죽으면 안 된다.
753
+ - **미지 타입은 조용히 건너뛴다.** 어휘는 append-only 라 타입이 늘어도 옛 사이트가 깨지지 않아야 한다.
592
754
  `default:` 에서 에러·경고를 내지 마라 — 그게 계약이다.
593
- - 이미지는 `assetId`(숫자)다. `mediaSrc(id)`(이 패키지 export)로 프록시 경유해 그린다.
594
- - **모든 href 는 `safeLinkUrl()` 을 태운다.** 콘솔 입력이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
755
+ - **모든 href `safeLinkUrl()` 태운다.** 콘솔 입력·손으로 고친 json 이라 `javascript:` 가 들어올 수 있다(저장형 XSS).
595
756
  - 자유 HTML 은 없다 — plain text + 줄바꿈뿐(`whitespace-pre-wrap`).
757
+ - **디스패치는 직접 짠다** — `type` 으로 컴포넌트를 고르는 `switch` 하나면 된다.
596
758
  - 타입·키를 **지어내지 마라.** 계약 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고,
597
- 이 패키지가 export 하는 `SECTION_CONTRACT` 는 그것을 npm 으로 실어 나르는 **운반체**다(`src/sections.ts`
759
+ 이 패키지가 export 하는 `SECTION_CONTRACT` 는 그것을 npm 으로 실어 나르는 **운반체**다(`sections.ts`
598
760
  KDoc 이 같은 말을 한다). 코드에서 읽을 것은 `SECTION_CONTRACT` 가 맞지만, 어휘를 **늘리는** 결정은
599
761
  정본 쪽에서 난다 — 필요한 어휘가 없으면 만들지 말고 **보고**한다.
600
762
 
601
- **디스패치는 직접 짠다** — `type` 으로 컴포넌트를 고르는 `switch` 하나면 된다. 규약에 더해 둘을 지켜라:
602
-
603
- - **`sortOrder` 로 한 번 더 정렬한다.** 서버가 정렬해 주지만 순서가 틀리면 화면에서 바로 티가 나는 종류다.
604
- - **key 는 `type`+`sortOrder`+index 로 짠다.** `PageSection` 계약에 id 가 없어(실측) 순수 인덱스보다 낫다.
605
-
606
- **어휘 (12종)** — `vertical` 은 콘솔 픽커 그룹핑용이지 사용 제한이 아니다(GENERAL 은 뷰티 테넌트도 쓴다).
763
+ `vertical` 콘솔 픽커 그룹핑용이지 사용 제한이 아니다(GENERAL 뷰티 테넌트도 쓴다).
607
764
 
608
- | type | vertical | config (필수는 굵게) | JSON-LD |
765
+ | type | vertical | config — **DB 방언 표기**(필수는 굵게 · 소스 방언 대응은 §9.2) | JSON-LD |
609
766
  |---|---|---|---|
610
767
  | `HERO` | GENERAL | eyebrow?, **title**, subtitle?, ctaLabel?, ctaHref?, assetId? | — |
611
768
  | `FEATURE_GRID` | GENERAL | title?, **items**[{icon?, **title**, body?}] | — |
@@ -615,7 +772,7 @@ page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" },
615
772
  | `TESTIMONIALS` | GENERAL | title?, **items**[{**quote**, author?, role?, assetId?}] | **없음(의도)** |
616
773
  | `FAQ_LIST` | GENERAL | title?, **items**[{**question**, **answer**}] | `FAQPage` |
617
774
  | `LEAD_CTA` | GENERAL | title?, body?, interest?, quick? | — (`submitLead` 계약) |
618
- | `SERVICE_MENU` | BEAUTY | productIds?, categorySlug? | |
775
+ | `SERVICE_MENU` | BEAUTY | **productIds**(rev 3 부터 필수), categorySlug? | `ItemList`(rev 2 부터) |
619
776
  | `BEFORE_AFTER_GALLERY` | BEAUTY | **items**[{**beforeAssetId**, **afterAssetId**, caption?}] | — |
620
777
  | `BOOKING_CTA` | BEAUTY | **productId**, label? | — |
621
778
  | `DOCTOR_INTRO` | BEAUTY | **name**, title?, photoAssetId?, bio? | — |
@@ -628,6 +785,10 @@ page.sections // [{ type: "HERO", sortOrder: 0, config: "{\"title\":\"…\"}" },
628
785
  - `LEAD_CTA` 는 템플릿의 `components/LeadForm` 을 그대로 쓴다(UTM·클릭ID 추적·레이트리밋 안내 동봉).
629
786
  전환 부품을 두 벌 만들지 마라. 이 섹션은 **`id="lead"` 앵커**를 갖는다 — 원페이지 랜딩에서 히어로 CTA 를
630
787
  `"ctaHref": "#lead"` 로 여기에 걸 수 있다. 앵커를 지우면 그 버튼이 조용히 아무 데도 안 간다.
788
+ - `SERVICE_MENU` 는 상품 참조가 **필수**다(rev 3). 참조가 0 이면 렌더러가 그 섹션을 안 그려서 **개시 직후
789
+ 조용히 사라진다** — 시술 목록의 AEO 보장(`ItemList`·§5.1)이 그 섹션에 걸려 있으므로 보장까지 함께 죽는다.
790
+ 소스 방언에서는 `config.products` 의 **handle 배열 순서를 그대로** 그린다(정렬하지 마라 — 그 배열이 노출
791
+ 순서의 원장이고, `ItemList` 도 같은 순서로 나가야 한다).
631
792
  - `LOGO_WALL` 에 실존 기업 로고를 넣지 마라 — 사용 허락이 있는 것만.
632
793
 
633
794
  **아이콘 — 큐레이션 맵의 키 문자열만**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalkera/client",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
5
5
  "license": "MIT",
6
6
  "author": "Credium Co., Ltd.",