@zalkera/client 0.21.11 → 0.22.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.d.ts CHANGED
@@ -541,7 +541,7 @@ interface ShipmentInfo {
541
541
  }
542
542
 
543
543
  /**
544
- * ISR 읽기 옵션 — Next.js 캐시 태그(memo31 §0-1). RSC/ISR 페이지에서 읽기 메서드에 넘기면
544
+ * ISR 읽기 옵션 — Next.js 캐시 태그. RSC/ISR 페이지에서 읽기 메서드에 넘기면
545
545
  * 그 fetch 에 `next.tags` 가 실려, 백엔드가 `revalidateTag(tag)` 로 온디맨드 무효화할 수 있다.
546
546
  * 태그 컨벤션: `site-config`(사이트 설정·테마·레이아웃), `products`(카탈로그), `product:{slug}`(특정 상품).
547
547
  * 넘기지 않으면 세그먼트 기본 캐시(페이지의 `revalidate` 주기)만 적용된다 — 하위호환 유지.
@@ -569,7 +569,7 @@ interface ZalkeraClientOptions {
569
569
  tenant: string;
570
570
  /**
571
571
  * 스토어프론트 서버 시크릿 키(선택·`oqsk_…`). 주면 모든 요청에 `X-Storefront-Key` 헤더로 실려,
572
- * 백엔드가 이 키로 테넌트 신원을 증명한다(memo78[tenant] 무인증 신뢰의 보안 승격).
572
+ * 백엔드가 이 키로 테넌트 신원을 증명한다 — `tenant` 만으로 신뢰하던 것의 보안 승격이다.
573
573
  *
574
574
  * ⚠️ **진짜 비밀이다.** 오직 서버 `.env`(예: `ZALKERA_STOREFRONT_KEY`)에만 두고, 브라우저 번들에
575
575
  * 절대 넣지 마라 — `NEXT_PUBLIC_*` 접두사·클라이언트 컴포넌트 import 금지. 이 클라이언트가 서버
@@ -584,7 +584,7 @@ interface ZalkeraClientOptions {
584
584
  * Next.js 의 `fetch` 캐시 옵션을 감싸는 래퍼를 넣을 때 쓴다.
585
585
  *
586
586
  * (내부 `request` 전송부는 이 주입점을 데이터 소스 seam 으로 유지한다 — 미래의 로컬 픽스처(mock)
587
- * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다 — memo78 §14 후속.)
587
+ * 모드를 갈아엎지 않고 얹기 위한 여지다. mock 구현은 현재 없다.
588
588
  */
589
589
  fetch?: typeof fetch;
590
590
  /** 모든 요청에 추가할 헤더(선택). */
@@ -859,7 +859,7 @@ declare class ZalkeraError extends Error {
859
859
  */
860
860
  get isRateLimited(): boolean;
861
861
  /**
862
- * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
862
+ * 스토어프론트 시크릿 키 문제 — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
863
863
  * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
864
864
  * `TENANT_MISMATCH`(403·키↔tenant 불일치).
865
865
  */
@@ -869,13 +869,13 @@ declare class ZalkeraError extends Error {
869
869
  }
870
870
 
871
871
  /**
872
- * 섹션 어휘 계약 — **정본의 코드 표현**(memo102 §6).
872
+ * 섹션 어휘 계약 — **정본의 코드 표현**.
873
873
  *
874
- * 정본은 백엔드 레포의 `doc/contracts/section-vocabulary.json` 이고, 이 파일은 그것을 npm 으로
874
+ * 정본은 백엔드 레포의 섹션 어휘 계약이고, 이 파일은 그것을 npm 으로
875
875
  * 실어 나르는 **운반체**다. 스토어프론트 렌더러가 이 상수를 읽어 자기 커버리지를 기계로 검사한다 —
876
876
  * 사본이 갈라진 채 조용히 굳는 것을 막는 게 목적이지, 실시간 동일성이 목적은 아니다(계약이 원래
877
877
  * 스큐 내성으로 설계돼 있다: 미지 타입은 스킵). rev 7 에서 사본은 **둘**이다(이 운반체·렌더러) —
878
- * 백엔드 `SectionType` enum 과 콘솔 zod 는 거처(DB)와 함께 퇴역했다(memo144).
878
+ * 백엔드 `SectionType` enum 과 콘솔 zod 는 거처(DB)와 함께 퇴역했다.
879
879
  *
880
880
  * 두 레포가 갈라져 있어 상호 CI 강제가 불가능하므로 **사람 이음새가 정확히 한 곳** 남는다 —
881
881
  * 백엔드 JSON ↔ 이 파일. [SECTION_CONTRACT_REV] 를 백엔드 스펙의 `contractRev` 와 맞춰 두고,
@@ -884,31 +884,31 @@ declare class ZalkeraError extends Error {
884
884
  /**
885
885
  * 백엔드 스펙 `contractRev` 와 같아야 한다. 어긋나면 동기 스크립트가 잡는다.
886
886
  *
887
- * rev 4 = **참조 방언·콘텐츠 파일의 1급 승격**(memo129 §1.4). 섹션 타입 12종도 config 키 선언도
887
+ * rev 4 = **참조 방언·콘텐츠 파일의 1급 승격**. 섹션 타입 12종도 config 키 선언도
888
888
  * 안 바뀌었다 — 정본에 `dialects`(id ↔ 참조)와 `contentFile`(`content/pages/*.json`) 절이 생겼고,
889
889
  * 이 패키지는 그 방언을 읽는 헬퍼([asHandle]·[asHandleArray]·[assetPath]·[readConfig])를 실어 나른다.
890
890
  * 그래서 아래 `SECTION_CONTRACT` 리터럴은 rev 3 과 **바이트 동일**하다(동기 스크립트가 확인한다).
891
891
  *
892
- * rev 5 = **`categorySlug` 를 대등 참조로**(memo139). `SERVICE_MENU`·`BOOKING_CTA` 의 필수성 단위가
892
+ * rev 5 = **`categorySlug` 를 대등 참조로**. `SERVICE_MENU`·`BOOKING_CTA` 의 필수성 단위가
893
893
  * "productIds 가 있는가"에서 **"참조가 하나라도 있는가"**(`requiredRefsAnyOf`)로 옮겨갔다. 그 시대의
894
894
  * 판단이었고 rev 6 이 뒤집었다(아래).
895
895
  *
896
- * rev 6 = **`SERVICE_MENU`·`BOOKING_CTA` 완전 삭제 — 12종 → 10종**(memo142 §오너확정2-1). rev 3·5 가
896
+ * rev 6 = **`SERVICE_MENU`·`BOOKING_CTA` 완전 삭제 — 12종 → 10종**. rev 3·5 가
897
897
  * "조회형 섹션은 참조를 반드시 실어라"로 조이던 잣대가 **"조회형 섹션을 싣지 마라"로 반전**됐다.
898
- * 경계 규칙(memo142 §1): 값이 콘텐츠 파일에 사는 저작물은 **선언 섹션**의 소관이고, 값이 업무 DB 에
898
+ * 경계 규칙: 값이 콘텐츠 파일에 사는 저작물은 **선언 섹션**의 소관이고, 값이 업무 DB 에
899
899
  * 살고 화면이 비추기만 하는 조회는 **소스가 이 패키지를 직접 호출**해 그린다(`listProducts()`·
900
900
  * `listProductCategories()`). 절반 선언(`SERVICE_MENU`)은 "어디에"만 선언에 두고 "어떻게"(카드 그리드·
901
901
  * 필드·개수)를 공유 렌더러에 얼려 두는 형태였고, 그것이 자연어로 다양한 디자인을 만든다는 방향과 반대다.
902
902
  *
903
903
  * **`retired` 표기가 아니라 삭제**인 이유: 실측상 정당한 잔존 소비자가 0이었고(상용 `page_section`·
904
- * `product`·`product_category` 전부 0행), memo128 이미 `page_section` 계열을 퇴역 방향으로 잡아 뒀다.
904
+ * `product`·`product_category` 전부 0행), 백엔드가 이미 `page_section` 계열을 퇴역 방향으로 잡아 뒀다.
905
905
  * 제3 상태는 계약·검사기·팩 게이트·콘솔이 각자 해석해야 하는 축을 새로 만든다.
906
906
  *
907
907
  * ⚠ **계약이 스큐 내성이라 이 삭제가 구 사이트를 깨지 않는다** — 렌더러는 미지 타입을 조용히 스킵한다.
908
- * 어휘를 강제하지도 않는다(memo125 요건 1): 자기 소스에 무엇을 적든 자유이고, 집행은 **우리 산출물인
908
+ * 어휘를 강제하지도 않는다(레인 A 의 첫 요건): 자기 소스에 무엇을 적든 자유이고, 집행은 **우리 산출물인
909
909
  * 팩**에만 선다.
910
910
  *
911
- * rev 7 = **DB 방언 소거 — 거처가 하나 남았다**(memo144). `page`·`page_section`·`menu` 계열이 퇴역하면서
911
+ * rev 7 = **DB 방언 소거 — 거처가 하나 남았다**. `page`·`page_section`·`menu` 계열이 퇴역하면서
912
912
  * 정본의 `dialects.id`(숫자 id 표기)가 가리킬 자리가 없어졌다. rev 4 가 방언을 1급으로 승격하며 rev 를
913
913
  * 올렸던 것의 **역연산**이라 서술 정리가 아니라 잣대 변경이다. **아래 리터럴은 rev 6 과 바이트 동일**이다
914
914
  * — 섹션 10종·`requiredRefs(AnyOf)` 는 한 글자도 안 바뀐다(동기 스크립트가 확인한다). 이 패키지에서
@@ -925,7 +925,7 @@ interface SectionSpec {
925
925
  /** 이 섹션이 산출하는 schema.org 타입. null 이면 구조화 데이터 없음. */
926
926
  readonly jsonLd: string | null;
927
927
  /**
928
- * **필수 참조 config 키**(정본 `config` 선언에서 `?` 가 없는 `*Id`/`*Ids` 키 — memo119 §2.6-3·rev 3).
928
+ * **필수 참조 config 키**(정본 `config` 선언에서 `?` 가 없는 `*Id`/`*Ids` 키·rev 3).
929
929
  *
930
930
  * 계약 전체를 실어 나르지 않고 이 축만 뽑아 오는 이유: 이 값을 읽는 소비자가 **팩 게이트 하나**이고,
931
931
  * 그가 답해야 하는 질문이 정확히 "이 섹션이 아무것도 안 가리킨 채 시드에 들어와 있는가"이기 때문이다.
@@ -935,7 +935,7 @@ interface SectionSpec {
935
935
  * ⚠ **rev 6 기준 이 축을 쓰는 타입은 0 이다.** 그 요구를 갖던 둘이 어휘에서 삭제됐기 때문이다.
936
936
  * 필드를 남겨 두는 것은 계약 기계를 유지하기 위해서다 — 참조가 필수인 **저작물** 타입이 앞으로
937
937
  * 생길 수 있고(에셋 축), 팩 게이트가 이 선언을 읽는 코드도 그대로 선다. 다만 **조회형 타입의 증설로**
938
- * 이 축이 되살아나는 일은 없다(memo142 §6-2 그 문을 닫았다).
938
+ * 이 축이 되살아나는 일은 없다 조회형 섹션을 없앤 결정이 그 문을 닫았다.
939
939
  *
940
940
  * 키 이름은 정본 그대로 **id 형**이다. 시드가 쓰는 참조형 키로 미리 바꿔 두지 않는 이유: 이 패키지는
941
941
  * 정본의 운반체이지 시드 문법의 번역기가 아니고, id↔참조 대응 규칙은 팩 게이트가 자기 자리에서 안다.
@@ -951,7 +951,7 @@ interface SectionSpec {
951
951
  * 안 가리킨 채 시드에 들어와 개시 직후 조용히 사라지는 섹션.
952
952
  *
953
953
  * ⚠ `requiredRefs` 와 같이 **rev 6 기준 이 축을 쓰는 타입도 0 이다**(그 둘이 삭제됐다). 남기는
954
- * 이유도 같다 — 계약 기계는 유지하고, 되살릴 문은 memo142 §6-2 가 닫았다.
954
+ * 이유도 같다 — 계약 기계는 유지하되, 되살릴 문은 이미 닫혔다.
955
955
  *
956
956
  * `requiredRefs`(무조건 필수)와 **함께** 쓴다: 그룹으로 표현되는 섹션은 `requiredRefs` 가 빈 배열이고,
957
957
  * 종전처럼 단일 키가 무조건 필수인 섹션은 이 필드가 빈 배열이다. 둘 다 빈 배열이면 참조 요구가 없다.
@@ -1084,7 +1084,7 @@ declare function safeLinkUrl(raw: unknown): string;
1084
1084
  * @param headers `Headers` 또는 `NextRequest.headers` — `get(name)` 하나만 쓴다.
1085
1085
  * @param options 홉 수 명시. 우선순위: 명시 > env `ZALKERA_TRUSTED_PROXY_HOPS` > 기본 1.
1086
1086
  * @returns 원 방문자 IP. 정할 수 없으면 `undefined`(문자열 `"unknown"` 을 지어내지 않는다) —
1087
- * 그대로 `RequestContext.clientIp` 에 넣으면 되고, 클라이언트가 헤더를 생략한다.
1087
+ * 그대로 `RequestContext.clientIp` 에 넣는다 백엔드가 `undefined` 를 받으면 그 필드를 생략한다..
1088
1088
  */
1089
1089
  declare function visitorIp(headers: HeaderReader, options?: VisitorIpOptions): string | undefined;
1090
1090
  /** `get(name)` 만 요구한다 — `Headers`·`NextRequest.headers`·직접 만든 객체가 전부 들어맞는다. */
@@ -1111,7 +1111,7 @@ interface VisitorIpOptions {
1111
1111
  * **페이지 전체가 500** 이 난다 — 계약은 "그 섹션만 사라진다" 였다. 아래 가드들이 그 계약을
1112
1112
  * 지키는 장치다. [parseThemeColors] 와 같은 사상.
1113
1113
  *
1114
- * **이 패키지에 사는 이유**(memo108 §1): 이건 표현이 아니라 **계약을 안전하게 읽는 법**이다. 마크업은
1114
+ * **이 패키지에 사는 이유**: 이건 표현이 아니라 **계약을 안전하게 읽는 법**이다. 마크업은
1115
1115
  * 만드는 쪽 자유지만, '섹션 하나가 사이트를 죽이지 않는다'는 계약은 한 벌이어야 한다 — 사본이 갈라지면
1116
1116
  * 그 계약이 조용히 깨진다(471946c 교훈).
1117
1117
  */
@@ -1121,7 +1121,7 @@ declare function parseConfig<T>(config: string | null): T | null;
1121
1121
  *
1122
1122
  * 계약이 말하는 config 는 **객체**다(`content/pages/*.json` 의 `sections[].config`). 문자열도 받는 이유는
1123
1123
  * 둘이다: ⑴ 손으로 고치는 파일이라 config 를 통째 문자열로 적어 넣는 일이 실제로 있고 ⑵ 종전의 다른
1124
- * 거처(DB 컬럼)가 문자열을 줬다 — 그 거처는 rev 7 에서 사라졌지만(memo144) 관용은 append-only 로 남긴다.
1124
+ * 거처(DB 컬럼)가 문자열을 줬다 — 그 거처는 rev 7 에서 사라졌지만 관용은 append-only 로 남긴다.
1125
1125
  * 소비자가 두 갈래로 갈리면 섹션 컴포넌트가 두 벌이 되고, 그것이 이 패키지가 사본을 안 만드는 이유
1126
1126
  * 그대로다. 그래서 입구를 하나로 좁힌다.
1127
1127
  *
@@ -1142,9 +1142,13 @@ declare function asId(value: unknown): number | undefined;
1142
1142
  *
1143
1143
  * 경로 조각은 URL 인코딩한다 — 선언은 `number` 지만 실제 인자는 백엔드가 검증하지 않는 raw config
1144
1144
  * 에서 온다(이 파일 상단 참고). 인코딩이 없으면 `../` 이 프록시 라우트 밖으로 새어 나간다.
1145
- * **throw 하지 않는다**는 이 파일의 계약은 그대로다 — 인코딩만 하고 값을 판정하지 않는다.
1145
+ * **throw 하지 않는다**는 이 파일의 계약은 그대로다 — 판정에 걸리면 던지지 않고 `undefined` 를 준다.
1146
+ *
1147
+ * ⚠ **점은 인코딩이 안 된다.** `encodeURIComponent(".")` 는 `"."` 그대로라 `mediaSrc("..")` 가
1148
+ * `/media/..` 를 만든다 — 한 단이지만 프록시 라우트 밖이다. 형제인 전송 단일점(`client.ts` 의
1149
+ * `INVALID_PATH`)은 같은 불변식을 이미 세워 두었는데 여기만 없었다.
1146
1150
  */
1147
- declare function mediaSrc(assetId: number): string;
1151
+ declare function mediaSrc(assetId: number): string | undefined;
1148
1152
  /** 상품 handle 하나. 문자열이 아니거나 공백뿐이면 `undefined` — 형식은 검사하지 않는다(런타임은 관용). */
1149
1153
  declare function asHandle(value: unknown): string | undefined;
1150
1154
  /**
@@ -1170,8 +1174,8 @@ declare function assetPath(value: unknown): string | undefined;
1170
1174
  * 읽어들인 색은 globals.css 의 `@theme` 변수를 `<html>` inline style 로 덮어, `bg-primary` 등
1171
1175
  * 전 유틸리티가 테넌트 색으로 바뀐다(layout.tsx). CSS 주입 방어를 위해 값은 hex 형식 검증 후에만 싣는다.
1172
1176
  *
1173
- * **이 패키지에 사는 이유**(memo108 §1): L1(말로 색 바꾸기)의 기계는 표현이 아니라 계약이다 — 어떤 토큰을
1174
- * 쓸지는 코드가 정하고 값은 config 가 정한다는 규약(memo65 §3)의 집행부이고, 순수 함수라 React 에 안 매인다.
1177
+ * **이 패키지에 사는 이유**: L1(말로 색 바꾸기)의 기계는 표현이 아니라 계약이다 — 어떤 토큰을
1178
+ * 쓸지는 코드가 정하고 값은 config 가 정한다는 규약의 집행부이고, 순수 함수라 React 에 안 매인다.
1175
1179
  * 이 함수의 반환을 `<html style={...}>` 에 싣는 **배선은 각 사이트의 몫**이다(validator S8 이 그걸 센다).
1176
1180
  */
1177
1181
  interface ParsedTheme {
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ var ZalkeraError = class _ZalkeraError extends Error {
34
34
  return this.status === 429;
35
35
  }
36
36
  /**
37
- * 스토어프론트 시크릿 키 문제(memo78) — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
37
+ * 스토어프론트 시크릿 키 문제 — 개발자 오배선 신호. `true` 면 `secretKey` 옵션을
38
38
  * 점검하라(미설정·불일치·폐기). [code] 로 정밀 분기: `STOREFRONT_KEY_REQUIRED`(401·키 필요)·
39
39
  * `TENANT_MISMATCH`(403·키↔tenant 불일치).
40
40
  */
@@ -46,7 +46,7 @@ var ZalkeraError = class _ZalkeraError extends Error {
46
46
  if (isApiErrorBody(body)) {
47
47
  const code = body.errorCode ?? body.error;
48
48
  return new _ZalkeraError(
49
- // 스토어프론트 키 오배선(memo78)은 개발자용 안내로 메시지를 덮는다 — 백엔드는 정보 누출
49
+ // 스토어프론트 키 오배선은 개발자용 안내로 메시지를 덮는다 — 백엔드는 정보 누출
50
50
  // 최소화로 두루뭉술한 401 을 주므로, SDK 가 "무엇을 고쳐야 하는지"를 또렷이 알려준다.
51
51
  STOREFRONT_KEY_MESSAGES[code] ?? body.message,
52
52
  {
@@ -306,12 +306,12 @@ function safeJsonParse(text) {
306
306
  // src/sections.ts
307
307
  var SECTION_CONTRACT_REV = 7;
308
308
  var SECTION_CONTRACT = [
309
- // 뷰티(memo47) — 조회형 둘(SERVICE_MENU·BOOKING_CTA)은 rev 6 에서 삭제됐다(위 KDoc).
309
+ // 뷰티 — 조회형 둘(SERVICE_MENU·BOOKING_CTA)은 rev 6 에서 삭제됐다(위 KDoc).
310
310
  // 시술 목록의 ItemList 는 사라진 것이 아니라 거처가 바뀌었다: `/products` 라우트와 소스가 조합하는
311
311
  // 진열이 낸다(보장표 `service-menu-itemlist` 의 route 는 원래 `any` — 판정 지점은 산출물이다).
312
312
  { type: "BEFORE_AFTER_GALLERY", vertical: "BEAUTY", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
313
313
  { type: "DOCTOR_INTRO", vertical: "BEAUTY", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
314
- // 기업 마케팅(memo102 §2)
314
+ // 기업 마케팅
315
315
  { type: "HERO", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
316
316
  { type: "FEATURE_GRID", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
317
317
  { type: "TEXT_MEDIA", vertical: "GENERAL", jsonLd: null, requiredRefs: [], requiredRefsAnyOf: [] },
@@ -352,7 +352,7 @@ function safeLinkUrl(raw) {
352
352
  if (url.startsWith("/")) return internalPath(url) ?? "#";
353
353
  try {
354
354
  const parsed = new URL(url);
355
- return ALLOWED_SCHEMES.has(parsed.protocol) ? url : "#";
355
+ return ALLOWED_SCHEMES.has(parsed.protocol) ? parsed.href : "#";
356
356
  } catch {
357
357
  try {
358
358
  return internalPath(url) ?? "#";
@@ -419,7 +419,9 @@ function asId(value) {
419
419
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
420
420
  }
421
421
  function mediaSrc(assetId) {
422
- return `/media/${seg(assetId)}`;
422
+ const raw = String(assetId);
423
+ if (raw === "." || raw === "..") return void 0;
424
+ return `/media/${seg(raw)}`;
423
425
  }
424
426
  function asHandle(value) {
425
427
  if (typeof value !== "string") return void 0;
@@ -521,5 +523,3 @@ function toRgb(hex) {
521
523
  }
522
524
 
523
525
  export { SECTION_CONTRACT, SECTION_CONTRACT_REV, ZalkeraError, asHandle, asHandleArray, asId, asIdArray, asObjectArray, asString, assetPath, createZalkeraClient, mediaSrc, parseConfig, parseThemeColors, readConfig, safeLinkUrl, sectionsOfVertical, visitorIp };
524
- //# sourceMappingURL=index.js.map
525
- //# sourceMappingURL=index.js.map
package/llms.txt CHANGED
@@ -92,7 +92,8 @@ visitorIp(req.headers, { trustedHops: 2 }); // 예: CDN + 로드밸런서
92
92
  ### 콘텐츠(CMS)
93
93
  - `getSiteConfig()` — 회사명·연락처·테마·SEO 기본값
94
94
  - `listCategories()` · `listPosts({category?,page?,size?,sort?})` · `getPost(slug)` · `recordPostView(slug,ctx)`
95
- - `getMediaUrl(id)` — 에셋 id → 단기 만료 URL. **마크업에는 넣지 마라**(§4.9 프록시가 정답)
95
+ - `getMediaUrl(id)` → `MediaUrl { url, expiresAt }` — 에셋 id → 단기 만료 URL **객체**(문자열이 아니다).
96
+ **마크업에는 넣지 마라**(§4.9 프록시가 정답) — `expiresAt` 이 지나면 깨진 이미지가 박제된다.
96
97
  - ⚠ **고정 페이지·섹션·내비를 주는 API 는 없다.** 사이트의 얼굴(페이지·섹션·문구·섹션 이미지·내비)의
97
98
  정본은 **레포 파일**이라 백엔드 왕복이 아예 없다 — 파일 형상은 §9.1(`content/pages/*.json`·`content/nav.json`),
98
99
  그것을 읽어 그리는 라우트 전문은 §4.8 이다. 고정 페이지를 만드는 일 = **json 1개 + 매니페스트 1행**.
@@ -163,10 +164,11 @@ const tokens = await zalkera.socialLogin({
163
164
  code,
164
165
  redirectUri,
165
166
  // 신규 가입 화면에서 받은 값. 셋 다 없으면 400 이다.
167
+ // 키는 `consentType` 이고 `policyVersion` 이 **필수**다 — 동의받은 약관의 판본을 남긴다.
166
168
  consents: [
167
- {type: "TERMS", granted: true},
168
- {type: "PRIVACY", granted: true},
169
- {type: "OVER_14", granted: true},
169
+ {consentType: "TERMS", policyVersion: "v1", granted: true},
170
+ {consentType: "PRIVACY", policyVersion: "v1", granted: true},
171
+ {consentType: "OVER_14", policyVersion: "v1", granted: true},
170
172
  // 선택: MARKETING_EMAIL · MARKETING_SMS
171
173
  ],
172
174
  });
@@ -215,7 +217,8 @@ const tokens = await zalkera.socialLogin({
215
217
  - `asIdArray(v)` / `asObjectArray(v)` — **빈 배열**로 강하한다(undefined 아님).
216
218
  빈 배열은 truthy 라 `if (items)` 로 결측을 판별하면 안 된다 — `items.length` 을 봐라.
217
219
  - 위 가드로 **필수 값이 없으면 그 섹션만 안 그린다**. 페이지 전체가 죽으면 안 된다.
218
- - `mediaSrc(assetId)` → `/media/{id}` **경로 문자열**. 이미지는 전부 이걸 통한다.
220
+ - `mediaSrc(assetId)` → `/media/{id}` **경로 문자열**(또는 `undefined`). 이미지는 전부 이걸 통한다.
221
+ 값이 `.`·`..` 면 `undefined` 다 — 프록시 라우트 밖을 가리키는 경로를 만들지 않는다. 던지지는 않으므로 `src` 에 그대로 넣지 말고 없으면 그 이미지를 안 그린다.
219
222
  전제: 프로젝트에 `src/app/media/[id]/route.ts` 프록시 라우트가 있어야 한다(**§4.9 에 전문**).
220
223
  이 헬퍼는 경로만 만든다 — 라우트를 안 만들면 이미지가 404 다.
221
224
  - `assetPath(v)` — **소스 참조 표기**(계약 `dialects.reference`)를 읽는다. 숫자 id 를 못 쓰는 자리에서 에셋은
@@ -239,7 +242,7 @@ const tokens = await zalkera.socialLogin({
239
242
 
240
243
  ```ts
241
244
  const config = await zalkera.getSiteConfig({ tags: ["site-config"] });
242
- const product = await zalkera.getProduct(slug, { tags: ["products", `product:${slug}`] });
245
+ const product = await zalkera.getProduct(slug, { tags: ["products", "site-config"] });
243
246
  ```
244
247
 
245
248
  **백엔드가 실제로 발화하는 태그는 둘뿐이다**(실측 — `OperationRegistryChangeApplier`):
@@ -520,7 +523,7 @@ const header = (nav as { header?: { label: string; href: string }[] }).header ??
520
523
 
521
524
  ### 4.9 미디어 프록시 라우트 (`/media/{id}`) — **필수 부품**
522
525
 
523
- `mediaSrc(assetId)` 는 `/media/{id}` **경로 문자열만** 만든다. 그 경로를 받는 라우트는 프로젝트가 갖는다.
526
+ `mediaSrc(assetId)` 는 `/media/{id}` **경로 문자열만** 만든다(`.`·`..` 면 `undefined`). 그 경로를 받는 라우트는 프로젝트가 갖는다.
524
527
  **안 만들면 사이트의 이미지가 전부 404 다.** 아래를 `src/app/media/[id]/route.ts` 로 그대로 복사해라.
525
528
 
526
529
  왜 프록시가 필요한가 — 백엔드 공개 미디어는 둘 다 브라우저·크롤러가 직접 못 쓴다:
@@ -1089,7 +1092,7 @@ shadcn 소스는 자기 변수층(`--card`·`--muted-foreground` …)을 전제
1089
1092
  | `"source"` | 사이트의 얼굴(페이지·섹션·문구·섹션 이미지·내비)의 정본이 **레포 파일**. 지금 만드는 사이트는 전부 이 형상이다 |
1090
1093
  | 미선언 | 안전 기본 — **선언이 없으면 이 계약을 안 쓰는 레포로 본다**(계약 검사도 걸지 않는다). 문구를 tsx 에 직접 든 레포가 여기고, 그래도 개시·발행·"말로 고치기"는 전부 정상이다 |
1091
1094
 
1092
- > ⚠ **`"sections-db"` 는 은퇴한 값이다**(어휘 rev 7 · memo144). 섹션이 백엔드 DB 에도 살던 시절의
1095
+ > ⚠ **`"sections-db"` 는 은퇴한 값이다**(어휘 rev 7). 섹션이 백엔드 DB 에도 살던 시절의
1093
1096
  > 전환기 표기였는데 그 거처(`page_section` 계열)가 통째로 퇴역했다 — 값과 함께 `getPage`·`listPages`·
1094
1097
  > `listMenus` 표면도, 콘솔의 페이지·섹션·메뉴 편집 화면도 내려갔다. 아직 이 값을 선언한 레포가 있다면
1095
1098
  > 검사기(`npx zalkera-validate`)가 경고로 알려 준다. 옮길 곳은 §9.1 이고, 옮기고 나면 선언은 `"source"` 다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalkera/client",
3
- "version": "0.21.11",
3
+ "version": "0.22.0",
4
4
  "description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
5
5
  "license": "MIT",
6
6
  "author": "Credium Co., Ltd.",
@@ -23,7 +23,8 @@
23
23
  "contracts",
24
24
  "bin/validate-storefront.mjs",
25
25
  "bin/check-aeo-surfaces.mjs",
26
- "lib"
26
+ "lib",
27
+ "!dist/*.map"
27
28
  ],
28
29
  "main": "./dist/index.cjs",
29
30
  "module": "./dist/index.js",
@@ -34,9 +35,14 @@
34
35
  },
35
36
  "exports": {
36
37
  ".": {
37
- "types": "./dist/index.d.ts",
38
- "import": "./dist/index.js",
39
- "require": "./dist/index.cjs"
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
+ }
40
46
  },
41
47
  "./contracts/aeo-surface-guarantees.json": "./contracts/aeo-surface-guarantees.json",
42
48
  "./bin/check-aeo-surfaces.mjs": "./bin/check-aeo-surfaces.mjs",
@@ -49,7 +55,7 @@
49
55
  "typecheck": "tsc --noEmit",
50
56
  "test": "vitest run",
51
57
  "test:watch": "vitest",
52
- "prepublishOnly": "npm run check:tarball && npm run format:check && npm run typecheck && npm test && npm run check:published && npm run build",
58
+ "prepublishOnly": "npm run build && npm run check:tarball && npm run format:check && npm run typecheck && npm test && npm run check:published",
53
59
  "check:published": "node scripts/check-published-drift.mjs",
54
60
  "format": "prettier --write \"src/**/*.ts\" \"scripts/**/*.mjs\" \"bin/**/*.mjs\"",
55
61
  "format:check": "prettier --check \"src/**/*.ts\" \"scripts/**/*.mjs\" \"bin/**/*.mjs\"",
@@ -64,5 +70,6 @@
64
70
  "tsup": "^8.3.5",
65
71
  "typescript": "^5.7.2",
66
72
  "vitest": "^2.1.8"
67
- }
73
+ },
74
+ "sideEffects": false
68
75
  }