@zalkera/client 0.24.4 → 0.26.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.
@@ -35,8 +35,8 @@
35
35
  * 아니면(`border-[2px]`·`to-[50%]`) 안 잡고, `var(…)`·`theme(…)` 경유는 면제다. 테넌트 색은
36
36
  * 토큰 경유가 규약. 초판이 `-[#` 세 형태만 봐서 18가지가 새던 자리 — [COLOR_UTILITY_ROOTS] 참고.
37
37
  * S5 warning — src/app/globals.css 외의 .css 파일 존재(단일 CSS 원칙).
38
- * C2 error — 섹션 렌더러 switch 가 계약(@zalkera/client SECTION_CONTRACT)을 덮지 못함. 계약에 있는
39
- * 타입을 렌더러가 모르면 그 섹션은 **조용히 안 그려진다**(미지 타입 스킵이 계약이라 에러도 안 난다).
38
+ * C2 error — 섹션 렌더러 switch 가 **콘텐츠가 실제로 쓰는** 계약 타입을 덮지 못함. 타입을
39
+ * 렌더러가 모르면 그 섹션은 **조용히 안 그려진다**(미지 타입 스킵이 계약이라 에러도 안 난다).
40
40
  * S8 error — **표현 계약(L1) 배선 부재**(declared 전용). globals.css 의 `@theme`+`--color-primary`
41
41
  * 정의와 root layout 의 테마 주입(`parseThemeColors(` 호출 + `<html>` style)을 센다. 이게 없으면 콘솔의
42
42
  * "말로 색 바꾸기"가 **성공 보고를 내고 화면은 그대로**인 거짓성공이 된다 — validator 가 여태 L1 의
@@ -2885,8 +2885,9 @@ function checkLayoutBlastRadius() {
2885
2885
  }
2886
2886
 
2887
2887
  /**
2888
- * C2 — 섹션 렌더러 커버리지. vendored `@zalkera/client` SECTION_CONTRACT 와 SectionRenderer 의 case
2889
- * 대조한다. 어휘 사본이 넷이라 사람 주석 규약으로는 갈라짐을 막는다는 실측된 교훈이라,
2888
+ * C2 — 섹션 렌더러 커버리지. **콘텐츠가 쓰는 타입**을 SectionRenderer 의 case 와 대조한다
2889
+ * (계약 전량이 아니다 전량을 재면 계약에 타입이 하나 추가될 그것을 안 쓰는 기존 사이트가
2890
+ * 전부 관문에서 막혀, 계약의 하위호환 약속과 어긋난다). 어휘 사본이 넷이라 사람 주석 규약으로는 갈라짐을 못 막는다는 게 실측된 교훈이라,
2890
2891
  * 레포 안에서 확인 가능한 짝은 기계가 센다. 계약을 못 읽으면(구 client·미설치) **검사를 건너뛴다** —
2891
2892
  * BYO 레포에서 이 검사가 빌드를 막으면 안 되기 때문이다.
2892
2893
  */
@@ -2996,12 +2997,19 @@ function checkSectionCoverage() {
2996
2997
  * 못 읽거나 깨진 파일은 **여기서 말하지 않는다** — N2·N4 가 그 일을 한다. 이 함수는 「무엇을
2997
2998
  * 쓰는가」만 답하고, 모르면 조용히 뺀다(모르는 것을 「쓴다」고 세면 없는 결함을 만든다).
2998
2999
  */
2999
- function usedSectionTypes(repoRoot) {
3000
+ function usedSectionTypes(repoRoot, what = "콘텐츠 섹션 타입 수집") {
3000
3001
  const out = [];
3001
3002
  for (const file of contentPageFiles(repoRoot)) {
3003
+ // ⚠ **맨 `readFileSync` 로 되돌리지 마라.** 이 자리는 상한(`MAX_READ_BYTES`)도 봉쇄
3004
+ // (`containedPath`)도 안 받는 읽기였다. 종전엔 `C2` 하나만 여기 닿았고 그것은 우리
3005
+ // 렌더러 파일이 있는 트리에서만 도는데, 지금은 선언 없는 **업로드 레포**가 그대로 닿는다.
3006
+ // 실측: 59MB 콘텐츠 파일을 구판은 `EFBIG` 로 **거부**했고 이 자리는 읽고 파싱까지 했다.
3007
+ // 페이지마다 도는 순회라 총량 상한도 없다.
3008
+ const text = readSafe(file, what);
3009
+ if (text === null) continue;
3002
3010
  let page;
3003
3011
  try {
3004
- page = JSON.parse(readFileSync(file, "utf8"));
3012
+ page = JSON.parse(text);
3005
3013
  } catch {
3006
3014
  continue;
3007
3015
  }
@@ -4422,6 +4430,82 @@ checkManualCarrier(); // W-LLMS — zip 이 나르는 명세가 설치본과 같
4422
4430
  if (!singletonFound)
4423
4431
  warnings.push(`[W1] createZalkeraClient 싱글턴을 찾지 못했습니다 — 서버 사이드 호출 패턴이 있는지 확인하세요.`);
4424
4432
 
4433
+ /**
4434
+ * **선언은 없는데 우리 계약 어휘를 쓰고 있다** — 셋째 사실이다.
4435
+ *
4436
+ * 두 모드 판정은 사실을 둘로만 갈라 왔다: 「선언했다」(계약 모드)와 「선언 안 했다」(BYO — 정상 형상).
4437
+ * 그 사이에 셋째가 있다. `content/pages/*.json` 이 `SECTION_CONTRACT` 의 타입을 적고 있으면 그 레포는
4438
+ * **우리 어휘를 쓰는 중**이다 — 그 문자열은 우리가 발행한 식별자라 남이 우연히 쓰지 않는다.
4439
+ *
4440
+ * 이 자리가 왜 구멍이었나. 선언 두 줄을 지우면 S·N·D 축이 **error 에서 warning 으로 내려가 rc 가
4441
+ * 0** 이 된다(실측: 오류 5 → 0). 고쳐진 것은 없다 — 깨진 이미지는 그대로 나가고 미지 타입 섹션은
4442
+ * 그대로 화면에서 사라진다. 검사기를 고치거나 지울 필요도 없이 **가장 싼 우회**가 여기 있었다.
4443
+ *
4444
+ * 그런데 일괄 격상은 답이 아니다. 선언 부재는 BYO 의 **정상 형상**이라, 그것만으로 error 를 세우면
4445
+ * 우리 계약을 자처한 적 없는 레포를 기계가 벌한다(「출처 자유의 부정」). 그래서 **선언이 아니라
4446
+ * 코드가 하는 일**을 증거로 삼는다 — 어휘를 쓰면 우리 계약 위에 있다.
4447
+ *
4448
+ * 처분은 error(rc=1)가 아니라 **rc=7(못 잼)** 이다. 우리가 아는 것은 「어느 잣대를 댈지 모르겠다」
4449
+ * 까지이지 「위반이다」가 아니다. 이 검사기는 그 칸을 이미 갖고 있다(모르는 선언값 `EDECL`).
4450
+ * 고치는 값도 싸다 — 선언 두 줄이면 다음 실행이 진짜 판정을 낸다.
4451
+ *
4452
+ * **관문에서만 선다.** 로컬 `validate` 는 경고로 끝난다 — 남의 개발 루프를 끊지 않는다.
4453
+ *
4454
+ * ## 이 판정이 **안 잡는 것** (심의 실측 — 잡는 척하지 않는다)
4455
+ *
4456
+ * 증거가 「콘텐츠 파일이 우리 어휘를 쓴다」 하나뿐이라, 그 증거를 없애면 조용해진다.
4457
+ *
4458
+ * - **어휘를 자기 이름으로 바꾸면** 빠져나간다(rc=0). 렌더러까지 같이 고쳐야 하는 기계적 작업이고,
4459
+ * 그러고 나면 다른 error 도 안 선다 — `N4`(계약에 없는 타입)는 `inferred` 에서 경고이고 `C2` 는
4460
+ * 쓰는 타입을 계약과 교집합으로 걸러서 함께 조용해진다.
4461
+ * - **콘텐츠를 `content/pages/` 밖으로 옮기면** 빠져나간다. `CONTENT_MODE` 가 `none` 이 되어 N 축이
4462
+ * 통째로 스킵된다. `content/pages/<slug>/index.json` 도 같다 — [contentPageFiles] 는 재귀하지 않는다.
4463
+ * 이 축은 이 판정 이전부터 그랬다.
4464
+ * - **가짜 계약을 반입하면**(`"@zalkera/client": "file:./vendor/fake"` 에 비지 않은 `SECTION_CONTRACT`)
4465
+ * 빠져나간다. 비어 있으면 [warnIfContractMissing] 이 관문에서 rc=7 로 세우지만, **비지 않았는데
4466
+ * 내용이 다른** 것은 못 가른다.
4467
+ * - **배송 디렉터리 이름이 `src`** 면 [resolveRepoRoot] 가 한 칸 위를 루트로 잡아 콘텐츠가 안 보인다.
4468
+ * 서빙 관문은 `/build` 로 복사해 띄우므로 그 경로로는 닿지 않는다(`sandbox/build.sh`).
4469
+ *
4470
+ * 반대 방향의 한계도 적는다. 어휘의 절반(`HERO`·`FAQ_LIST`·`TESTIMONIALS`·`LEAD_CTA`)은 업계 통용어라
4471
+ * **우리와 무관한 레포가 우연히 겹칠 수 있다.** 그때 이 판정은 「선언하라」고만 하고 위반을 주장하지
4472
+ * 않는다 — 처분을 error 가 아니라 rc=7 로 둔 이유가 그것이다.
4473
+ *
4474
+ * 이 축들을 더 조이려면 증거를 넓혀야 하는데(예: `@zalkera/client` 의존 존재를 OR), 그러면 관문이
4475
+ * 부르는 검사기가 곧 그 의존이라 **선언이 사실상 필수**가 된다. 그것은 기계가 아니라 계약의 결정이다.
4476
+ */
4477
+ function usesContractVocabulary(repoRoot) {
4478
+ const known = sectionContractMap();
4479
+ // 계약을 못 읽으면 판정하지 않는다(BYO 레포를 막지 않는다). 그 형상은 `warnIfContractMissing`
4480
+ // 이 관문에서 따로 rc=7 로 세운다 — 여기서 두 번 외치지 않는다.
4481
+ if (known === null) return false;
4482
+ return usedSectionTypes(repoRoot, "선언 부재 판정(섹션 어휘 대조)").some((t) => known.has(t));
4483
+ }
4484
+
4485
+ if (CONTENT_MODE === "inferred" && usesContractVocabulary(repoRootDir)) {
4486
+ // ⚠ **콘텐츠축에만 건다.** 증거가 콘텐츠 어휘라 그것이 말해 주는 것은 「이 레포가 우리 콘텐츠
4487
+ // 계약 위에 있다」까지다. 스타일축까지 같이 세우면 **콘텐츠 계약은 따르되 스타일은 자기 것을
4488
+ // 쓰는** 레포가 빠져나갈 길이 없어진다 — `styling` 을 빼면 여기서 서고, `tailwind-tokens` 로
4489
+ // 적으면 S1~S4 가 error 로 서고, 다른 값을 적으면 EDECL 로 선다. 세 길이 다 막힌 자리였다.
4490
+ // 그 형상이 곧 풀소스 BYO 이고, 이 판정의 전제(「선언 부재는 BYO 의 정상 형상」)와도 어긋난다.
4491
+ const pkg = join(repoRootDir, "package.json");
4492
+ // 어느 파일을 고치라는 말인지 정확해야 한다. 워크스페이스 하위 트리에는 자기 package.json 이
4493
+ // 없을 수 있고, 그때 이 경로를 그대로 찍으면 **없는 파일을 고치라는 지시**가 된다.
4494
+ const where = fileExists(pkg)
4495
+ ? "package.json"
4496
+ : `package.json(${relative(process.cwd(), repoRootDir) || "."} 에 없습니다)`;
4497
+ // ⚠ **처방을 `what` 안에 담는다.** 관문 출력은 「이 소스를 만든 개발자에게 아래 줄을 그대로
4498
+ // 전달하십시오」로 시작하는데, 넘겨받는 그 줄에 무엇을 하라는 말이 없으면 전달이 무의미하다.
4499
+ // 처방이 더 필요한 쪽은 로컬 루프가 아니라 **업로드한 서드파티**다.
4500
+ const what =
4501
+ `콘텐츠가 SECTION_CONTRACT 의 섹션 타입을 쓰는데 ${where} 에 zalkera 선언이 없어 ` +
4502
+ `콘텐츠(N) 규칙군을 경고로만 돌렸습니다 — 어느 잣대를 댈지 정하지 못했습니다. ` +
4503
+ `\`"zalkera": {"content": "source"}\` 를 선언하십시오 — 우리 계약을 따르는 것이 아니면 ` +
4504
+ `콘텐츠를 \`content/pages/\` 밖에 두십시오`;
4505
+ if (GATE_MODE) markUnmeasured(pkg, "EDECL", what);
4506
+ else warnings.push(`[N0] ${what}`);
4507
+ }
4508
+
4425
4509
  console.log(
4426
4510
  `스타일 규약 모드: ${STYLE_MODE}` +
4427
4511
  (STYLE_MODE === "declared"
package/dist/index.cjs CHANGED
@@ -330,7 +330,7 @@ function safeJsonParse(text) {
330
330
  }
331
331
 
332
332
  // src/sections.ts
333
- var SECTION_CONTRACT_REV = 7;
333
+ var SECTION_CONTRACT_REV = 8;
334
334
  var SECTION_CONTRACT = [
335
335
  // 뷰티 — 조회형 둘(SERVICE_MENU·BOOKING_CTA)은 rev 6 에서 삭제됐다(위 KDoc).
336
336
  // 시술 목록의 ItemList 는 사라진 것이 아니라 거처가 바뀌었다: `/products` 라우트와 소스가 조합하는
@@ -344,7 +344,13 @@ var SECTION_CONTRACT = [
344
344
  { type: "LOGO_WALL", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
345
345
  { type: "STATS_BAND", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
346
346
  { type: "TESTIMONIALS", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
347
+ // 방문 정보 단(rev 8) — 내점형 업장이 어휘만으로 조립된다. 셋 다 `jsonLd: null` 인 이유는
348
+ // 주소·영업시간의 구조화 데이터 거처가 섹션이 아니라 `site_config → organizationJsonLd`
349
+ // 이기 때문이다. 섹션 값은 표시 문자열(「휴진」)이라 구조화하면 지어낸 그래프가 된다.
350
+ { type: "FACILITY_GALLERY", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
347
351
  { type: "FAQ_LIST", vertical: "GENERAL", jsonLd: "FAQPage", requiredRefs: [], requiredRefsAnyOf: [] },
352
+ { type: "BUSINESS_HOURS", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
353
+ { type: "DIRECTIONS", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
348
354
  { type: "LEAD_CTA", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] }
349
355
  ];
350
356
  function sectionsOfVertical(vertical) {
package/dist/index.d.cts CHANGED
@@ -893,7 +893,12 @@ declare class ZalkeraError extends Error {
893
893
  * "productIds 가 있는가"에서 **"참조가 하나라도 있는가"**(`requiredRefsAnyOf`)로 옮겨갔다. 그 시대의
894
894
  * 판단이었고 rev 6 이 뒤집었다(아래).
895
895
  *
896
- * rev 6 = **`SERVICE_MENU`·`BOOKING_CTA` 완전 삭제12종 10종**. rev 3·5 가
896
+ * rev 8 = **방문 정보 신설** `FACILITY_GALLERY`·`BUSINESS_HOURS`·`DIRECTIONS` GENERAL
897
+ * 더한다. 내점형 업장(의원·매장·공방·학원)이 어휘만으로 조립되게 하는 것이 목적이고, 셋 다
898
+ * `jsonLd: null` 이다 — 주소·영업시간의 구조화 데이터 거처는 섹션이 아니라 `site_config` 에서 나오는
899
+ * 홈의 조직 노드다. 섹션 값은 표시 문자열이라 구조화하면 지어낸 그래프가 되고 주체가 중복된다.
900
+ *
901
+ * rev 6 = **`SERVICE_MENU`·`BOOKING_CTA` 완전 삭제**. rev 3·5 가
897
902
  * "조회형 섹션은 참조를 반드시 실어라"로 조이던 잣대가 **"조회형 섹션을 싣지 마라"로 반전**됐다.
898
903
  * 경계 규칙: 값이 콘텐츠 파일에 사는 저작물은 **선언 섹션**의 소관이고, 값이 업무 DB 에
899
904
  * 살고 화면이 비추기만 하는 조회는 **소스가 이 패키지를 직접 호출**해 그린다(`listProducts()`·
@@ -910,13 +915,12 @@ declare class ZalkeraError extends Error {
910
915
  *
911
916
  * rev 7 = **DB 방언 소거 — 거처가 하나 남았다**. `page`·`page_section`·`menu` 계열이 퇴역하면서
912
917
  * 정본의 `dialects.id`(숫자 id 표기)가 가리킬 자리가 없어졌다. rev 4 가 방언을 1급으로 승격하며 rev 를
913
- * 올렸던 것의 **역연산**이라 서술 정리가 아니라 잣대 변경이다. **아래 리터럴은 rev 6 과 바이트 동일**이다
914
- * — 섹션 10종·`requiredRefs(AnyOf)` 는 한 글자도 안 바뀐다(동기 스크립트가 확인한다). 이 패키지에서
918
+ * 올렸던 것의 **역연산**이라 서술 정리가 아니라 잣대 변경이다. 패키지에서
915
919
  * 함께 내려간 것은 그 거처를 읽던 표면이다: `getPage`·`listPages`·`listMenus` 와 그 타입들.
916
920
  * 남은 표기는 소스 하나 — `content/pages/*.json` 이 쓰는 참조 표기(`asset` 계열 문자열)이고,
917
921
  * 어휘 표의 `assetId` 계열 키 이름은 그 시절 표기가 굳은 것이다(대응은 정본 `dialects.reference`).
918
922
  */
919
- declare const SECTION_CONTRACT_REV = 7;
923
+ declare const SECTION_CONTRACT_REV = 8;
920
924
  /** 업종 분류 — 어휘를 묶어 보여 줄 때의 그룹핑이지 사용 제한이 아니다(GENERAL 은 뷰티 사이트도 쓴다). */
921
925
  type SectionVertical = "BEAUTY" | "GENERAL";
922
926
  interface SectionSpec {
@@ -959,7 +963,9 @@ interface SectionSpec {
959
963
  readonly requiredRefsAnyOf: readonly (readonly string[])[];
960
964
  }
961
965
  /**
962
- * 아는 섹션 전량(rev 7 기준 **10종**rev 6 동일). **순서는 관행 아크**(주목→가치→신뢰→행동)이고,
966
+ * 아는 섹션 전량. **종수를 여기 적지 않는다** 적으면 아래 배열과 갈리고, 주석은 `dist/index.d.ts`
967
+ * 로 실려 **소비자 편집기 툴팁까지 배송**되므로 그 거짓이 그대로 나간다.
968
+ * **순서는 관행 아크**(주목→가치→신뢰→방문 정보→행동)이고,
963
969
  * 어휘를 목록으로 보여 주는 자리는 이 순서를 그대로 쓰면 된다. 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.
964
970
  *
965
971
  * `requiredRefs` 는 빈 배열이라도 **반드시 적는다**. 생략을 허용하면 동기 스크립트의 리터럴 정규식이
@@ -1014,12 +1020,30 @@ declare const SECTION_CONTRACT: readonly [{
1014
1020
  readonly jsonLd: null;
1015
1021
  readonly requiredRefs: readonly [];
1016
1022
  readonly requiredRefsAnyOf: readonly [];
1023
+ }, {
1024
+ readonly type: "FACILITY_GALLERY";
1025
+ readonly vertical: "GENERAL";
1026
+ readonly jsonLd: null;
1027
+ readonly requiredRefs: readonly [];
1028
+ readonly requiredRefsAnyOf: readonly [];
1017
1029
  }, {
1018
1030
  readonly type: "FAQ_LIST";
1019
1031
  readonly vertical: "GENERAL";
1020
1032
  readonly jsonLd: "FAQPage";
1021
1033
  readonly requiredRefs: readonly [];
1022
1034
  readonly requiredRefsAnyOf: readonly [];
1035
+ }, {
1036
+ readonly type: "BUSINESS_HOURS";
1037
+ readonly vertical: "GENERAL";
1038
+ readonly jsonLd: null;
1039
+ readonly requiredRefs: readonly [];
1040
+ readonly requiredRefsAnyOf: readonly [];
1041
+ }, {
1042
+ readonly type: "DIRECTIONS";
1043
+ readonly vertical: "GENERAL";
1044
+ readonly jsonLd: null;
1045
+ readonly requiredRefs: readonly [];
1046
+ readonly requiredRefsAnyOf: readonly [];
1023
1047
  }, {
1024
1048
  readonly type: "LEAD_CTA";
1025
1049
  readonly vertical: "GENERAL";
package/dist/index.d.ts CHANGED
@@ -893,7 +893,12 @@ declare class ZalkeraError extends Error {
893
893
  * "productIds 가 있는가"에서 **"참조가 하나라도 있는가"**(`requiredRefsAnyOf`)로 옮겨갔다. 그 시대의
894
894
  * 판단이었고 rev 6 이 뒤집었다(아래).
895
895
  *
896
- * rev 6 = **`SERVICE_MENU`·`BOOKING_CTA` 완전 삭제12종 10종**. rev 3·5 가
896
+ * rev 8 = **방문 정보 신설** `FACILITY_GALLERY`·`BUSINESS_HOURS`·`DIRECTIONS` GENERAL
897
+ * 더한다. 내점형 업장(의원·매장·공방·학원)이 어휘만으로 조립되게 하는 것이 목적이고, 셋 다
898
+ * `jsonLd: null` 이다 — 주소·영업시간의 구조화 데이터 거처는 섹션이 아니라 `site_config` 에서 나오는
899
+ * 홈의 조직 노드다. 섹션 값은 표시 문자열이라 구조화하면 지어낸 그래프가 되고 주체가 중복된다.
900
+ *
901
+ * rev 6 = **`SERVICE_MENU`·`BOOKING_CTA` 완전 삭제**. rev 3·5 가
897
902
  * "조회형 섹션은 참조를 반드시 실어라"로 조이던 잣대가 **"조회형 섹션을 싣지 마라"로 반전**됐다.
898
903
  * 경계 규칙: 값이 콘텐츠 파일에 사는 저작물은 **선언 섹션**의 소관이고, 값이 업무 DB 에
899
904
  * 살고 화면이 비추기만 하는 조회는 **소스가 이 패키지를 직접 호출**해 그린다(`listProducts()`·
@@ -910,13 +915,12 @@ declare class ZalkeraError extends Error {
910
915
  *
911
916
  * rev 7 = **DB 방언 소거 — 거처가 하나 남았다**. `page`·`page_section`·`menu` 계열이 퇴역하면서
912
917
  * 정본의 `dialects.id`(숫자 id 표기)가 가리킬 자리가 없어졌다. rev 4 가 방언을 1급으로 승격하며 rev 를
913
- * 올렸던 것의 **역연산**이라 서술 정리가 아니라 잣대 변경이다. **아래 리터럴은 rev 6 과 바이트 동일**이다
914
- * — 섹션 10종·`requiredRefs(AnyOf)` 는 한 글자도 안 바뀐다(동기 스크립트가 확인한다). 이 패키지에서
918
+ * 올렸던 것의 **역연산**이라 서술 정리가 아니라 잣대 변경이다. 패키지에서
915
919
  * 함께 내려간 것은 그 거처를 읽던 표면이다: `getPage`·`listPages`·`listMenus` 와 그 타입들.
916
920
  * 남은 표기는 소스 하나 — `content/pages/*.json` 이 쓰는 참조 표기(`asset` 계열 문자열)이고,
917
921
  * 어휘 표의 `assetId` 계열 키 이름은 그 시절 표기가 굳은 것이다(대응은 정본 `dialects.reference`).
918
922
  */
919
- declare const SECTION_CONTRACT_REV = 7;
923
+ declare const SECTION_CONTRACT_REV = 8;
920
924
  /** 업종 분류 — 어휘를 묶어 보여 줄 때의 그룹핑이지 사용 제한이 아니다(GENERAL 은 뷰티 사이트도 쓴다). */
921
925
  type SectionVertical = "BEAUTY" | "GENERAL";
922
926
  interface SectionSpec {
@@ -959,7 +963,9 @@ interface SectionSpec {
959
963
  readonly requiredRefsAnyOf: readonly (readonly string[])[];
960
964
  }
961
965
  /**
962
- * 아는 섹션 전량(rev 7 기준 **10종**rev 6 동일). **순서는 관행 아크**(주목→가치→신뢰→행동)이고,
966
+ * 아는 섹션 전량. **종수를 여기 적지 않는다** 적으면 아래 배열과 갈리고, 주석은 `dist/index.d.ts`
967
+ * 로 실려 **소비자 편집기 툴팁까지 배송**되므로 그 거짓이 그대로 나간다.
968
+ * **순서는 관행 아크**(주목→가치→신뢰→방문 정보→행동)이고,
963
969
  * 어휘를 목록으로 보여 주는 자리는 이 순서를 그대로 쓰면 된다. 값 추가는 백엔드 스펙을 먼저 고친 뒤 여기로 옮긴다.
964
970
  *
965
971
  * `requiredRefs` 는 빈 배열이라도 **반드시 적는다**. 생략을 허용하면 동기 스크립트의 리터럴 정규식이
@@ -1014,12 +1020,30 @@ declare const SECTION_CONTRACT: readonly [{
1014
1020
  readonly jsonLd: null;
1015
1021
  readonly requiredRefs: readonly [];
1016
1022
  readonly requiredRefsAnyOf: readonly [];
1023
+ }, {
1024
+ readonly type: "FACILITY_GALLERY";
1025
+ readonly vertical: "GENERAL";
1026
+ readonly jsonLd: null;
1027
+ readonly requiredRefs: readonly [];
1028
+ readonly requiredRefsAnyOf: readonly [];
1017
1029
  }, {
1018
1030
  readonly type: "FAQ_LIST";
1019
1031
  readonly vertical: "GENERAL";
1020
1032
  readonly jsonLd: "FAQPage";
1021
1033
  readonly requiredRefs: readonly [];
1022
1034
  readonly requiredRefsAnyOf: readonly [];
1035
+ }, {
1036
+ readonly type: "BUSINESS_HOURS";
1037
+ readonly vertical: "GENERAL";
1038
+ readonly jsonLd: null;
1039
+ readonly requiredRefs: readonly [];
1040
+ readonly requiredRefsAnyOf: readonly [];
1041
+ }, {
1042
+ readonly type: "DIRECTIONS";
1043
+ readonly vertical: "GENERAL";
1044
+ readonly jsonLd: null;
1045
+ readonly requiredRefs: readonly [];
1046
+ readonly requiredRefsAnyOf: readonly [];
1023
1047
  }, {
1024
1048
  readonly type: "LEAD_CTA";
1025
1049
  readonly vertical: "GENERAL";
package/dist/index.js CHANGED
@@ -328,7 +328,7 @@ function safeJsonParse(text) {
328
328
  }
329
329
 
330
330
  // src/sections.ts
331
- var SECTION_CONTRACT_REV = 7;
331
+ var SECTION_CONTRACT_REV = 8;
332
332
  var SECTION_CONTRACT = [
333
333
  // 뷰티 — 조회형 둘(SERVICE_MENU·BOOKING_CTA)은 rev 6 에서 삭제됐다(위 KDoc).
334
334
  // 시술 목록의 ItemList 는 사라진 것이 아니라 거처가 바뀌었다: `/products` 라우트와 소스가 조합하는
@@ -342,7 +342,13 @@ var SECTION_CONTRACT = [
342
342
  { type: "LOGO_WALL", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
343
343
  { type: "STATS_BAND", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
344
344
  { type: "TESTIMONIALS", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
345
+ // 방문 정보 단(rev 8) — 내점형 업장이 어휘만으로 조립된다. 셋 다 `jsonLd: null` 인 이유는
346
+ // 주소·영업시간의 구조화 데이터 거처가 섹션이 아니라 `site_config → organizationJsonLd`
347
+ // 이기 때문이다. 섹션 값은 표시 문자열(「휴진」)이라 구조화하면 지어낸 그래프가 된다.
348
+ { type: "FACILITY_GALLERY", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
345
349
  { type: "FAQ_LIST", vertical: "GENERAL", jsonLd: "FAQPage", requiredRefs: [], requiredRefsAnyOf: [] },
350
+ { type: "BUSINESS_HOURS", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
351
+ { type: "DIRECTIONS", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
346
352
  { type: "LEAD_CTA", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] }
347
353
  ];
348
354
  function sectionsOfVertical(vertical) {
package/llms.txt CHANGED
@@ -1262,7 +1262,7 @@ content/
1262
1262
  - **React key 는 `type`+index 로 짠다.** 섹션에 id 는 없고(계약에 그런 필드가 없다) `sortOrder` 도 없다 —
1263
1263
  배열 순서가 곧 순서다(§9.1).
1264
1264
 
1265
- ### 9.3 어휘 (10종)
1265
+ ### 9.3 어휘 (13종)
1266
1266
 
1267
1267
  **규약**
1268
1268
  - 파싱 헬퍼는 이 패키지가 준다: `readConfig`/`parseConfig` + `asString`/`asId`/`asIdArray`/`asObjectArray`
@@ -1288,12 +1288,38 @@ content/
1288
1288
  | `LOGO_WALL` | GENERAL | title?, **items**[{**assetId**, name?, href?}] | — |
1289
1289
  | `STATS_BAND` | GENERAL | **items**[{**value**, **label**, suffix?}] — value 는 표시 문자열("1,200") | — |
1290
1290
  | `TESTIMONIALS` | GENERAL | title?, **items**[{**quote**, author?, role?, assetId?}] | **없음(의도)** |
1291
+ | `FACILITY_GALLERY` | GENERAL | title?, eyebrow?, **items**[{**assetId**, caption?}] | — |
1291
1292
  | `FAQ_LIST` | GENERAL | title?, **items**[{**question**, **answer**}] | `FAQPage` |
1293
+ | `BUSINESS_HOURS` | GENERAL | title?, eyebrow?, **rows**[{**days**, **time**, closed?}], note? | — |
1294
+ | `DIRECTIONS` | GENERAL | title?, eyebrow?, **address**, detail?, links?[{**label**, **href**}], items?[{**label**, **body**}] | — |
1292
1295
  | `LEAD_CTA` | GENERAL | title?, body?, interest?, quick? | — (`submitLead` 계약) |
1293
1296
  | `BEFORE_AFTER_GALLERY` | BEAUTY | **items**[{**beforeAssetId**, **afterAssetId**, caption?}] | — |
1294
1297
  | `DOCTOR_INTRO` | BEAUTY | **name**, title?, photoAssetId?, bio? | — |
1295
1298
 
1296
- **10종이다.** rev 6 에서 `SERVICE_MENU`·`BOOKING_CTA` **삭제**됐다 — 둘 다 업무 데이터(상품·갈래)
1299
+ **13종이다.** rev 8 에서 방문 정보 (`FACILITY_GALLERY`·`BUSINESS_HOURS`·`DIRECTIONS`)
1300
+ 더해졌다 — 내점형 업장(의원·매장·공방·학원)이 어휘만으로 조립되게 하는 것이 목적이다. 종전에는
1301
+ `TEXT_MEDIA` 로 번역해야 했고, 그러면 「휴진」 같은 상태가 본문 문장에 묻혀 화면에서 구분되지 않았다.
1302
+
1303
+ **셋 다 JSON-LD 가 없다.** 섹션 값은 **화면에 보이는 문자열**(「휴진」·「평일 09:30–18:30」)이라
1304
+ 구조화하면 지어낸 그래프가 되고, 홈이 이미 내는 주체 노드가 중복된다. `FAQ_LIST` 만 비-null 인 것은
1305
+ FAQ 내용의 거처가 섹션 config 뿐이어서이고, 이 셋은 그 반대형이다.
1306
+
1307
+ ⚠ **그래서 화면과 그래프의 거처가 다르다.** 영업시간·주소를 고치라는 요청을 받으면 **둘 다** 봐라 —
1308
+ 화면은 콘텐츠 파일의 이 섹션들이 내고, 기계가 읽는 값은 `site_config`(§4.10 의 조직 노드)가 낸다.
1309
+ 한쪽만 고치면 사람이 보는 것과 검색·AI 가 읽는 것이 갈린다.
1310
+
1311
+ **섹션별 주의**
1312
+ - `FACILITY_GALLERY` — 사진이 하나도 없으면 그리지 마라. 제목만 남은 빈 격자는 「준비 중」으로 읽히는데
1313
+ 실제로는 콘텐츠가 빠진 것이라, 그 둘이 화면에서 구분되지 않는다.
1314
+ - `BUSINESS_HOURS` — 쉬는 날은 `closed: true` 로 **표시하고**, 「휴진」 같은 문자열을 찾아내려 하지 마라.
1315
+ 표기는 편집자 자유(「쉽니다」·「정기휴무」)라 문자열 매칭은 깨진다.
1316
+ - `DIRECTIONS` — `links` 는 **누르는 것**(지도 바로가기), `items` 는 **읽는 것**(주차·지하철·버스)이다.
1317
+ 섞지 마라 — 렌더러가 버튼으로 그릴지 문단으로 그릴지 알 수 없다. 주차 안내를 `detail` 한 문자열에
1318
+ 뭉치면 이 rev 가 잡겠다던 「본문에 묻힘」이 된다.
1319
+ `links[].href` 는 `safeLinkUrl()` 을 태운다. 허용 스킴은 `http`·`https`·`mailto`·`tel` 과 내부
1320
+ 경로다. 지도 앱 딥링크(`kakaomap://` 류)는 **조용히 막히므로** 웹 지도 주소(https)를 적어라.
1321
+
1322
+ rev 6 에서 `SERVICE_MENU`·`BOOKING_CTA` 가 **삭제**됐다 — 둘 다 업무 데이터(상품·갈래)를
1297
1323
  가리키는 **조회형**이었고, 조회의 자리는 선언이 아니라 소스이기 때문이다. 대체 타입을 만들지 마라
1298
1324
  (`PRODUCT_GRID` 류 신설은 같은 위반의 재생산이다): 상품을 진열하려면 tsx 에서 `listProducts()` 를 부르고
1299
1325
  원하는 대로 그려라. 미지 타입은 렌더러가 조용히 스킵하므로 옛 타입이 적힌 콘텐츠도 페이지를 죽이지 않는다.
package/package.json CHANGED
@@ -1,75 +1,75 @@
1
1
  {
2
- "name": "@zalkera/client",
3
- "version": "0.24.4",
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
- "contracts",
24
- "bin/validate-storefront.mjs",
25
- "bin/check-aeo-surfaces.mjs",
26
- "lib",
27
- "!dist/*.map"
28
- ],
29
- "main": "./dist/index.cjs",
30
- "module": "./dist/index.js",
31
- "types": "./dist/index.d.ts",
32
- "bin": {
33
- "zalkera-aeo-check": "./bin/check-aeo-surfaces.mjs",
34
- "zalkera-validate": "./bin/validate-storefront.mjs"
35
- },
36
- "exports": {
37
- ".": {
38
- "import": {
39
- "types": "./dist/index.d.ts",
40
- "default": "./dist/index.js"
41
- },
42
- "require": {
43
- "types": "./dist/index.d.cts",
44
- "default": "./dist/index.cjs"
45
- }
2
+ "name": "@zalkera/client",
3
+ "version": "0.26.0",
4
+ "description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
5
+ "license": "MIT",
6
+ "author": "Credium Co., Ltd.",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
46
10
  },
47
- "./contracts/aeo-surface-guarantees.json": "./contracts/aeo-surface-guarantees.json",
48
- "./bin/check-aeo-surfaces.mjs": "./bin/check-aeo-surfaces.mjs",
49
- "./bin/validate-storefront.mjs": "./bin/validate-storefront.mjs",
50
- "./lib/site-crawl.mjs": "./lib/site-crawl.mjs"
51
- },
52
- "scripts": {
53
- "build": "tsup",
54
- "dev": "tsup --watch",
55
- "typecheck": "tsc --noEmit",
56
- "test": "vitest run",
57
- "test:watch": "vitest",
58
- "prepublishOnly": "npm run build && npm run check:tarball && npm run format:check && npm run typecheck && npm test && npm run check:published",
59
- "check:published": "node scripts/check-published-drift.mjs",
60
- "format": "prettier --write \"src/**/*.ts\" \"scripts/**/*.mjs\" \"bin/**/*.mjs\"",
61
- "format:check": "prettier --check \"src/**/*.ts\" \"scripts/**/*.mjs\" \"bin/**/*.mjs\"",
62
- "check:tarball": "node scripts/check-tarball.mjs"
63
- },
64
- "engines": {
65
- "node": ">=18"
66
- },
67
- "devDependencies": {
68
- "@types/node": "^22.10.2",
69
- "prettier": "^3.9.6",
70
- "tsup": "^8.3.5",
71
- "typescript": "^5.7.2",
72
- "vitest": "^2.1.8"
73
- },
74
- "sideEffects": false
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
+ "contracts",
24
+ "bin/validate-storefront.mjs",
25
+ "bin/check-aeo-surfaces.mjs",
26
+ "lib",
27
+ "!dist/*.map"
28
+ ],
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "bin": {
33
+ "zalkera-aeo-check": "./bin/check-aeo-surfaces.mjs",
34
+ "zalkera-validate": "./bin/validate-storefront.mjs"
35
+ },
36
+ "exports": {
37
+ ".": {
38
+ "import": {
39
+ "types": "./dist/index.d.ts",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/index.d.cts",
44
+ "default": "./dist/index.cjs"
45
+ }
46
+ },
47
+ "./contracts/aeo-surface-guarantees.json": "./contracts/aeo-surface-guarantees.json",
48
+ "./bin/check-aeo-surfaces.mjs": "./bin/check-aeo-surfaces.mjs",
49
+ "./bin/validate-storefront.mjs": "./bin/validate-storefront.mjs",
50
+ "./lib/site-crawl.mjs": "./lib/site-crawl.mjs"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "dev": "tsup --watch",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "prepublishOnly": "npm run build && npm run check:tarball && npm run format:check && npm run typecheck && npm test && npm run check:published",
59
+ "check:published": "node scripts/check-published-drift.mjs",
60
+ "format": "prettier --write \"src/**/*.ts\" \"scripts/**/*.mjs\" \"bin/**/*.mjs\"",
61
+ "format:check": "prettier --check \"src/**/*.ts\" \"scripts/**/*.mjs\" \"bin/**/*.mjs\"",
62
+ "check:tarball": "node scripts/check-tarball.mjs"
63
+ },
64
+ "engines": {
65
+ "node": ">=18"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^22.10.2",
69
+ "prettier": "^3.9.6",
70
+ "tsup": "^8.3.5",
71
+ "typescript": "^5.7.2",
72
+ "vitest": "^2.1.8"
73
+ },
74
+ "sideEffects": false
75
75
  }