@roottale/cms-client 0.20.0 → 0.24.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/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/dist/attribution.d.ts +56 -0
- package/dist/attribution.js +11 -0
- package/dist/attribution.js.map +1 -0
- package/dist/chunk-FRCLSU55.js +75 -0
- package/dist/chunk-FRCLSU55.js.map +1 -0
- package/dist/server.d.ts +12 -1
- package/dist/server.js +13 -1
- package/dist/server.js.map +1 -1
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @roottale/cms-client
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 299c9b2: 문의 유입 어트리뷰션 (ADR-0048 §2). `submitInquiry` 에 `attribution` 필드 추가 — 비콘이 기억한 first-touch(처음 도착한 페이지·단축링크/QR 토큰·utm·외부 referrer 호스트명)를 문의와 함께 보내면 어드민 CRM 에 유입 경로가 표시된다. 신규: `@roottale/cms-client/attribution` 서브패스의 `readAttribution()`(브라우저 전용), 서버 엔트리의 `parseAttributionJson()`, intake API `attr_*` 폼 필드. cms-mcp docs(inquiries/api-reference) 및 예제(AttributionField) 동반 갱신.
|
|
8
|
+
|
|
3
9
|
## 0.20.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ RootTale CMS public API fetch client — **server-only**. Bearer auth (`rtlk_cus
|
|
|
4
4
|
|
|
5
5
|
Companion of [`@roottale/cms-renderer-next`](https://www.npmjs.com/package/@roottale/cms-renderer-next) (React RSC) and [`@roottale/cms-renderer-astro`](https://www.npmjs.com/package/@roottale/cms-renderer-astro). Use this directly if you want raw data; use the renderer packages if you want HTML/React out.
|
|
6
6
|
|
|
7
|
+
> AI 코딩 에이전트로 연동한다면 [`@roottale/cms-mcp`](https://www.npmjs.com/package/@roottale/cms-mcp)를 MCP 서버로 등록하세요 — 통합 문서·예시 코드를 tool로 제공합니다: `npx -y @roottale/cms-mcp`
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@roottale/cms-client/attribution` — 유입 first-touch 판독 (브라우저 전용).
|
|
3
|
+
*
|
|
4
|
+
* RootTale 사이트에 주입되는 비콘(`@roottale/analytics-runtime`, ADR-0048 §1)이
|
|
5
|
+
* 방문자의 first-touch(landing path · 단축링크/QR 토큰 · utm · 외부 referrer
|
|
6
|
+
* hostname)를 `localStorage["_rt_attr"]` 에 저장한다. 본 모듈은 그 값을 문의
|
|
7
|
+
* 폼 제출에 동반시킬 표준형으로 변환한다 — 클라이언트 컴포넌트에서 읽어
|
|
8
|
+
* Server Action 으로 넘기고, 서버에서 `submitInquiry({ fields: { attribution } })`
|
|
9
|
+
* 로 전달하면 된다. 식별자가 아니므로 PII 아님.
|
|
10
|
+
*
|
|
11
|
+
* 사용 예 (Next.js 폼 클라이언트 컴포넌트):
|
|
12
|
+
* ```tsx
|
|
13
|
+
* "use client";
|
|
14
|
+
* import { readAttribution } from "@roottale/cms-client/attribution";
|
|
15
|
+
*
|
|
16
|
+
* function onSubmit(values: FormValues) {
|
|
17
|
+
* return submitContactAction({ ...values, attribution: readAttribution() });
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* 저장 키·형태는 analytics-runtime 비콘과의 공유 계약 — 변경 시 양쪽 동시 갱신.
|
|
22
|
+
*/
|
|
23
|
+
/** 비콘이 first-touch 를 저장하는 localStorage 키. */
|
|
24
|
+
declare const ATTRIBUTION_STORAGE_KEY = "_rt_attr";
|
|
25
|
+
/** 문의에 동반되는 유입 어트리뷰션 (intake 의 `attr_*` 필드로 매핑). */
|
|
26
|
+
interface InquiryAttribution {
|
|
27
|
+
/** 첫 방문 landing pathname (query 제외). */
|
|
28
|
+
landing_path?: string;
|
|
29
|
+
/** 단축링크/QR 토큰 (`rt_src`). */
|
|
30
|
+
rt_src?: string;
|
|
31
|
+
utm_source?: string;
|
|
32
|
+
utm_medium?: string;
|
|
33
|
+
utm_campaign?: string;
|
|
34
|
+
/** 외부 referrer hostname (raw URL 아님). */
|
|
35
|
+
referrer?: string;
|
|
36
|
+
/** first-touch 시각 (ISO 8601). */
|
|
37
|
+
first_touch_at?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 폼 hidden input 등으로 넘어온 attribution JSON 을 안전하게 파싱 — 허용 키만,
|
|
41
|
+
* 문자열만, 길이 상한 적용. 깨진 값은 null (throw 금지). 서버에서도 사용 가능.
|
|
42
|
+
*
|
|
43
|
+
* 사용 예 (FormData 기반 Server Action):
|
|
44
|
+
* ```ts
|
|
45
|
+
* const attribution = parseAttributionJson(formData.get("attribution"));
|
|
46
|
+
* await submitInquiry({ apiKey, fields: { ...rest, attribution } });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function parseAttributionJson(value: unknown): InquiryAttribution | null;
|
|
50
|
+
/**
|
|
51
|
+
* 비콘이 기록한 first-touch 를 판독. 브라우저가 아니거나, 값이 없거나, 형태가
|
|
52
|
+
* 깨졌거나, TTL(30일)이 지났으면 null — 어떤 경우에도 throw 하지 않는다.
|
|
53
|
+
*/
|
|
54
|
+
declare function readAttribution(): InquiryAttribution | null;
|
|
55
|
+
|
|
56
|
+
export { ATTRIBUTION_STORAGE_KEY, type InquiryAttribution, parseAttributionJson, readAttribution };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/attribution.ts
|
|
2
|
+
var ATTRIBUTION_STORAGE_KEY = "_rt_attr";
|
|
3
|
+
var ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4
|
+
function pick(value, maxLen) {
|
|
5
|
+
if (typeof value !== "string") return void 0;
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed ? trimmed.slice(0, maxLen) : void 0;
|
|
8
|
+
}
|
|
9
|
+
function parseAttributionJson(value) {
|
|
10
|
+
let parsed = value;
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
if (!value.trim()) return null;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
20
|
+
const record = parsed;
|
|
21
|
+
const out = {};
|
|
22
|
+
const landingPath = pick(record.landing_path, 512);
|
|
23
|
+
if (landingPath) out.landing_path = landingPath;
|
|
24
|
+
const rtSrc = pick(record.rt_src, 32);
|
|
25
|
+
if (rtSrc) out.rt_src = rtSrc;
|
|
26
|
+
const utmSource = pick(record.utm_source, 100);
|
|
27
|
+
if (utmSource) out.utm_source = utmSource;
|
|
28
|
+
const utmMedium = pick(record.utm_medium, 100);
|
|
29
|
+
if (utmMedium) out.utm_medium = utmMedium;
|
|
30
|
+
const utmCampaign = pick(record.utm_campaign, 100);
|
|
31
|
+
if (utmCampaign) out.utm_campaign = utmCampaign;
|
|
32
|
+
const referrer = pick(record.referrer, 253);
|
|
33
|
+
if (referrer) out.referrer = referrer;
|
|
34
|
+
const firstTouchAt = pick(record.first_touch_at, 40);
|
|
35
|
+
if (firstTouchAt) out.first_touch_at = firstTouchAt;
|
|
36
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
37
|
+
}
|
|
38
|
+
function readAttribution() {
|
|
39
|
+
if (typeof window === "undefined" || !("localStorage" in window)) return null;
|
|
40
|
+
let stored = null;
|
|
41
|
+
try {
|
|
42
|
+
const raw = window.localStorage.getItem(ATTRIBUTION_STORAGE_KEY);
|
|
43
|
+
if (!raw) return null;
|
|
44
|
+
stored = JSON.parse(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!stored || typeof stored !== "object") return null;
|
|
49
|
+
if (typeof stored.ts !== "number" || Date.now() - stored.ts > ATTRIBUTION_TTL_MS) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const out = {
|
|
53
|
+
first_touch_at: new Date(stored.ts).toISOString()
|
|
54
|
+
};
|
|
55
|
+
const landingPath = pick(stored.lp, 512);
|
|
56
|
+
if (landingPath) out.landing_path = landingPath;
|
|
57
|
+
const rtSrc = pick(stored.rs, 32);
|
|
58
|
+
if (rtSrc) out.rt_src = rtSrc;
|
|
59
|
+
const utmSource = pick(stored.us, 100);
|
|
60
|
+
if (utmSource) out.utm_source = utmSource;
|
|
61
|
+
const utmMedium = pick(stored.um, 100);
|
|
62
|
+
if (utmMedium) out.utm_medium = utmMedium;
|
|
63
|
+
const utmCampaign = pick(stored.uc, 100);
|
|
64
|
+
if (utmCampaign) out.utm_campaign = utmCampaign;
|
|
65
|
+
const referrer = pick(stored.rf, 253);
|
|
66
|
+
if (referrer) out.referrer = referrer;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
ATTRIBUTION_STORAGE_KEY,
|
|
72
|
+
parseAttributionJson,
|
|
73
|
+
readAttribution
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=chunk-FRCLSU55.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/attribution.ts"],"sourcesContent":["/**\n * `@roottale/cms-client/attribution` — 유입 first-touch 판독 (브라우저 전용).\n *\n * RootTale 사이트에 주입되는 비콘(`@roottale/analytics-runtime`, ADR-0048 §1)이\n * 방문자의 first-touch(landing path · 단축링크/QR 토큰 · utm · 외부 referrer\n * hostname)를 `localStorage[\"_rt_attr\"]` 에 저장한다. 본 모듈은 그 값을 문의\n * 폼 제출에 동반시킬 표준형으로 변환한다 — 클라이언트 컴포넌트에서 읽어\n * Server Action 으로 넘기고, 서버에서 `submitInquiry({ fields: { attribution } })`\n * 로 전달하면 된다. 식별자가 아니므로 PII 아님.\n *\n * 사용 예 (Next.js 폼 클라이언트 컴포넌트):\n * ```tsx\n * \"use client\";\n * import { readAttribution } from \"@roottale/cms-client/attribution\";\n *\n * function onSubmit(values: FormValues) {\n * return submitContactAction({ ...values, attribution: readAttribution() });\n * }\n * ```\n *\n * 저장 키·형태는 analytics-runtime 비콘과의 공유 계약 — 변경 시 양쪽 동시 갱신.\n */\n\n/** 비콘이 first-touch 를 저장하는 localStorage 키. */\nexport const ATTRIBUTION_STORAGE_KEY = \"_rt_attr\";\n\n/** first-touch 보존 기간 (30일, ms) — 비콘과 동일. 경과분은 stale 로 무시. */\nconst ATTRIBUTION_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** 문의에 동반되는 유입 어트리뷰션 (intake 의 `attr_*` 필드로 매핑). */\nexport interface InquiryAttribution {\n /** 첫 방문 landing pathname (query 제외). */\n landing_path?: string;\n /** 단축링크/QR 토큰 (`rt_src`). */\n rt_src?: string;\n utm_source?: string;\n utm_medium?: string;\n utm_campaign?: string;\n /** 외부 referrer hostname (raw URL 아님). */\n referrer?: string;\n /** first-touch 시각 (ISO 8601). */\n first_touch_at?: string;\n}\n\n/** 비콘이 저장하는 압축형 — 내부 파싱용. */\ninterface StoredFirstTouch {\n v?: number;\n lp?: unknown;\n ts?: unknown;\n rs?: unknown;\n us?: unknown;\n um?: unknown;\n uc?: unknown;\n rf?: unknown;\n}\n\nfunction pick(value: unknown, maxLen: number): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed ? trimmed.slice(0, maxLen) : undefined;\n}\n\n/**\n * 폼 hidden input 등으로 넘어온 attribution JSON 을 안전하게 파싱 — 허용 키만,\n * 문자열만, 길이 상한 적용. 깨진 값은 null (throw 금지). 서버에서도 사용 가능.\n *\n * 사용 예 (FormData 기반 Server Action):\n * ```ts\n * const attribution = parseAttributionJson(formData.get(\"attribution\"));\n * await submitInquiry({ apiKey, fields: { ...rest, attribution } });\n * ```\n */\nexport function parseAttributionJson(value: unknown): InquiryAttribution | null {\n let parsed: unknown = value;\n if (typeof value === \"string\") {\n if (!value.trim()) return null;\n try {\n parsed = JSON.parse(value);\n } catch {\n return null;\n }\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n const record = parsed as Record<string, unknown>;\n const out: InquiryAttribution = {};\n const landingPath = pick(record.landing_path, 512);\n if (landingPath) out.landing_path = landingPath;\n const rtSrc = pick(record.rt_src, 32);\n if (rtSrc) out.rt_src = rtSrc;\n const utmSource = pick(record.utm_source, 100);\n if (utmSource) out.utm_source = utmSource;\n const utmMedium = pick(record.utm_medium, 100);\n if (utmMedium) out.utm_medium = utmMedium;\n const utmCampaign = pick(record.utm_campaign, 100);\n if (utmCampaign) out.utm_campaign = utmCampaign;\n const referrer = pick(record.referrer, 253);\n if (referrer) out.referrer = referrer;\n const firstTouchAt = pick(record.first_touch_at, 40);\n if (firstTouchAt) out.first_touch_at = firstTouchAt;\n return Object.keys(out).length > 0 ? out : null;\n}\n\n/**\n * 비콘이 기록한 first-touch 를 판독. 브라우저가 아니거나, 값이 없거나, 형태가\n * 깨졌거나, TTL(30일)이 지났으면 null — 어떤 경우에도 throw 하지 않는다.\n */\nexport function readAttribution(): InquiryAttribution | null {\n if (typeof window === \"undefined\" || !(\"localStorage\" in window)) return null;\n let stored: StoredFirstTouch | null = null;\n try {\n const raw = window.localStorage.getItem(ATTRIBUTION_STORAGE_KEY);\n if (!raw) return null;\n stored = JSON.parse(raw) as StoredFirstTouch;\n } catch {\n return null;\n }\n if (!stored || typeof stored !== \"object\") return null;\n if (typeof stored.ts !== \"number\" || Date.now() - stored.ts > ATTRIBUTION_TTL_MS) {\n return null;\n }\n\n const out: InquiryAttribution = {\n first_touch_at: new Date(stored.ts).toISOString(),\n };\n const landingPath = pick(stored.lp, 512);\n if (landingPath) out.landing_path = landingPath;\n const rtSrc = pick(stored.rs, 32);\n if (rtSrc) out.rt_src = rtSrc;\n const utmSource = pick(stored.us, 100);\n if (utmSource) out.utm_source = utmSource;\n const utmMedium = pick(stored.um, 100);\n if (utmMedium) out.utm_medium = utmMedium;\n const utmCampaign = pick(stored.uc, 100);\n if (utmCampaign) out.utm_campaign = utmCampaign;\n const referrer = pick(stored.rf, 253);\n if (referrer) out.referrer = referrer;\n return out;\n}\n"],"mappings":";AAwBO,IAAM,0BAA0B;AAGvC,IAAM,qBAAqB,KAAK,KAAK,KAAK,KAAK;AA6B/C,SAAS,KAAK,OAAgB,QAAoC;AAChE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,UAAU,QAAQ,MAAM,GAAG,MAAM,IAAI;AAC9C;AAYO,SAAS,qBAAqB,OAA2C;AAC9E,MAAI,SAAkB;AACtB,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,CAAC,MAAM,KAAK,EAAG,QAAO;AAC1B,QAAI;AACF,eAAS,KAAK,MAAM,KAAK;AAAA,IAC3B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO;AAC3E,QAAM,SAAS;AACf,QAAM,MAA0B,CAAC;AACjC,QAAM,cAAc,KAAK,OAAO,cAAc,GAAG;AACjD,MAAI,YAAa,KAAI,eAAe;AACpC,QAAM,QAAQ,KAAK,OAAO,QAAQ,EAAE;AACpC,MAAI,MAAO,KAAI,SAAS;AACxB,QAAM,YAAY,KAAK,OAAO,YAAY,GAAG;AAC7C,MAAI,UAAW,KAAI,aAAa;AAChC,QAAM,YAAY,KAAK,OAAO,YAAY,GAAG;AAC7C,MAAI,UAAW,KAAI,aAAa;AAChC,QAAM,cAAc,KAAK,OAAO,cAAc,GAAG;AACjD,MAAI,YAAa,KAAI,eAAe;AACpC,QAAM,WAAW,KAAK,OAAO,UAAU,GAAG;AAC1C,MAAI,SAAU,KAAI,WAAW;AAC7B,QAAM,eAAe,KAAK,OAAO,gBAAgB,EAAE;AACnD,MAAI,aAAc,KAAI,iBAAiB;AACvC,SAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,MAAM;AAC7C;AAMO,SAAS,kBAA6C;AAC3D,MAAI,OAAO,WAAW,eAAe,EAAE,kBAAkB,QAAS,QAAO;AACzE,MAAI,SAAkC;AACtC,MAAI;AACF,UAAM,MAAM,OAAO,aAAa,QAAQ,uBAAuB;AAC/D,QAAI,CAAC,IAAK,QAAO;AACjB,aAAS,KAAK,MAAM,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,MAAI,OAAO,OAAO,OAAO,YAAY,KAAK,IAAI,IAAI,OAAO,KAAK,oBAAoB;AAChF,WAAO;AAAA,EACT;AAEA,QAAM,MAA0B;AAAA,IAC9B,gBAAgB,IAAI,KAAK,OAAO,EAAE,EAAE,YAAY;AAAA,EAClD;AACA,QAAM,cAAc,KAAK,OAAO,IAAI,GAAG;AACvC,MAAI,YAAa,KAAI,eAAe;AACpC,QAAM,QAAQ,KAAK,OAAO,IAAI,EAAE;AAChC,MAAI,MAAO,KAAI,SAAS;AACxB,QAAM,YAAY,KAAK,OAAO,IAAI,GAAG;AACrC,MAAI,UAAW,KAAI,aAAa;AAChC,QAAM,YAAY,KAAK,OAAO,IAAI,GAAG;AACrC,MAAI,UAAW,KAAI,aAAa;AAChC,QAAM,cAAc,KAAK,OAAO,IAAI,GAAG;AACvC,MAAI,YAAa,KAAI,eAAe;AACpC,QAAM,WAAW,KAAK,OAAO,IAAI,GAAG;AACpC,MAAI,SAAU,KAAI,WAAW;AAC7B,SAAO;AACT;","names":[]}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { InquiryAttribution } from './attribution.js';
|
|
2
|
+
export { parseAttributionJson } from './attribution.js';
|
|
3
|
+
|
|
1
4
|
interface RssItemInput {
|
|
2
5
|
title: string;
|
|
3
6
|
/** 글 절대 URL. */
|
|
@@ -92,6 +95,7 @@ declare function jsonLdScript(schema: JsonLd | JsonLd[]): string;
|
|
|
92
95
|
*
|
|
93
96
|
* intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.
|
|
94
97
|
*/
|
|
98
|
+
|
|
95
99
|
type InquiryVertical = "consulting" | "medical" | "tax" | "legal";
|
|
96
100
|
/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */
|
|
97
101
|
declare const INQUIRY_ERROR_MESSAGES: Record<string, string>;
|
|
@@ -122,6 +126,13 @@ interface SubmitInquiryFields {
|
|
|
122
126
|
* 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.
|
|
123
127
|
*/
|
|
124
128
|
extras?: Record<string, string>;
|
|
129
|
+
/**
|
|
130
|
+
* 유입 first-touch 어트리뷰션 (ADR-0048 §2, 선택). 브라우저에서
|
|
131
|
+
* `readAttribution()`(`@roottale/cms-client/attribution`)으로 읽어 Server
|
|
132
|
+
* Action 인자로 넘긴 값을 그대로 전달하면 된다. CRM 에 "이 문의가 어느
|
|
133
|
+
* 글/링크/QR 에서 왔는지"가 표시된다. null/undefined 면 미동반 제출.
|
|
134
|
+
*/
|
|
135
|
+
attribution?: InquiryAttribution | null;
|
|
125
136
|
}
|
|
126
137
|
interface SubmitInquiryOptions {
|
|
127
138
|
/** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */
|
|
@@ -411,4 +422,4 @@ declare class CmsApiError extends Error {
|
|
|
411
422
|
declare function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage>;
|
|
412
423
|
declare function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null>;
|
|
413
424
|
|
|
414
|
-
export { type AnalyticsTagConfig, type AnalyticsTagProvider, type ArticleSchemaInput, CMS_CLIENT_VERSION, CmsApiError, type CmsPostContent, type CmsPostContentPage, type CmsPostType, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, type FetchAnalyticsOptions, type FetchBlogSettingsOptions, type FetchPostOptions, type FetchPostsOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, type InquiryVertical, type ResolvedPostDisplay, type RootTaleBlogSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, type SiteAnalyticsTags, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, articleSchema, breadcrumbSchema, faqSchema, fetchAnalyticsConfig, fetchBlogSettings, fetchPost, fetchPosts, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, jsonLdScript, organizationSchema, parseInquiryRedirect, resolvePostDisplay, rssItemsFromPosts, submitInquiry, websiteSchema };
|
|
425
|
+
export { type AnalyticsTagConfig, type AnalyticsTagProvider, type ArticleSchemaInput, CMS_CLIENT_VERSION, CmsApiError, type CmsPostContent, type CmsPostContentPage, type CmsPostType, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, type FetchAnalyticsOptions, type FetchBlogSettingsOptions, type FetchPostOptions, type FetchPostsOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryVertical, type ResolvedPostDisplay, type RootTaleBlogSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, type SiteAnalyticsTags, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, articleSchema, breadcrumbSchema, faqSchema, fetchAnalyticsConfig, fetchBlogSettings, fetchPost, fetchPosts, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, jsonLdScript, organizationSchema, parseInquiryRedirect, resolvePostDisplay, rssItemsFromPosts, submitInquiry, websiteSchema };
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseAttributionJson
|
|
3
|
+
} from "./chunk-FRCLSU55.js";
|
|
4
|
+
|
|
1
5
|
// src/seo.ts
|
|
2
6
|
function generateRssXml(input) {
|
|
3
7
|
const lang = input.language ?? "ko-KR";
|
|
@@ -213,12 +217,19 @@ async function submitInquiry(options) {
|
|
|
213
217
|
if (f.privacyConsent) body.set("privacy_consent", "on");
|
|
214
218
|
if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
|
|
215
219
|
if (f.leadKind) body.set("lead_kind", f.leadKind);
|
|
220
|
+
if (f.attribution) {
|
|
221
|
+
for (const [key, value] of Object.entries(f.attribution)) {
|
|
222
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
223
|
+
body.set(`attr_${key}`, value.trim());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
216
226
|
if (f.extras) {
|
|
217
227
|
for (const [rawKey, rawValue] of Object.entries(f.extras)) {
|
|
218
228
|
const key = rawKey.trim();
|
|
219
229
|
const value = String(rawValue ?? "").trim();
|
|
220
230
|
if (!key || !value) continue;
|
|
221
231
|
if (key.startsWith("_") || RESERVED_INQUIRY_KEYS.has(key)) continue;
|
|
232
|
+
if (key.startsWith("attr_")) continue;
|
|
222
233
|
if (body.has(key)) continue;
|
|
223
234
|
body.set(key, value);
|
|
224
235
|
}
|
|
@@ -258,7 +269,7 @@ function assertServer() {
|
|
|
258
269
|
|
|
259
270
|
// src/server.ts
|
|
260
271
|
var DEFAULT_BASE_URL2 = "https://api.roottale.com";
|
|
261
|
-
var CLIENT_VERSION = true ? "0.
|
|
272
|
+
var CLIENT_VERSION = true ? "0.24.0" : "dev";
|
|
262
273
|
var CMS_CLIENT_VERSION = CLIENT_VERSION;
|
|
263
274
|
function authHeaders(apiKey) {
|
|
264
275
|
return {
|
|
@@ -489,6 +500,7 @@ export {
|
|
|
489
500
|
inquiryErrorMessage,
|
|
490
501
|
jsonLdScript,
|
|
491
502
|
organizationSchema,
|
|
503
|
+
parseAttributionJson,
|
|
492
504
|
parseInquiryRedirect,
|
|
493
505
|
resolvePostDisplay,
|
|
494
506
|
rssItemsFromPosts,
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/seo.ts","../src/inquiry.ts","../src/server.ts"],"sourcesContent":["// 프레임워크 무관 SEO 헬퍼 — RSS 2.0 피드 + Schema.org JSON-LD 빌더.\n// 외부 사이트(Next route handler / Astro endpoint / RSC)가 fetchPosts 결과로\n// /feed.xml 응답이나 <head> JSON-LD 를 만들 때 사용. 순수 함수(네트워크 없음).\n\nimport type { CmsPostContent } from \"./server\";\n\n// ── RSS 2.0 ────────────────────────────────────────────────────────────────\n\nexport interface RssItemInput {\n title: string;\n /** 글 절대 URL. */\n link: string;\n /** 고유 식별자. 미지정 시 link 사용. */\n guid?: string;\n description?: string | null;\n /** 발행 시각. */\n pubDate?: string | Date | null;\n}\n\nexport interface RssFeedInput {\n /** 사이트/피드 제목. */\n title: string;\n /** 사이트 홈 URL. */\n link: string;\n /** 이 피드 자체의 URL (atom:self). */\n feedUrl: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n items: RssItemInput[];\n}\n\n/** RSS 2.0 XML 문자열 생성. */\nexport function generateRssXml(input: RssFeedInput): string {\n const lang = input.language ?? \"ko-KR\";\n const lastBuild =\n input.items.length > 0 ? toRfc822(input.items[0]!.pubDate) : \"\";\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">',\n );\n lines.push(\"<channel>\");\n lines.push(`<title>${escapeXml(input.title)}</title>`);\n lines.push(`<link>${escapeXml(input.link)}</link>`);\n lines.push(\n `<atom:link href=\"${escapeXml(input.feedUrl)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n lines.push(\n `<description>${escapeXml(input.description ?? input.title)}</description>`,\n );\n lines.push(`<language>${escapeXml(lang)}</language>`);\n if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);\n for (const item of input.items) {\n const guid = item.guid ?? item.link;\n lines.push(\"<item>\");\n lines.push(`<title>${escapeXml(item.title)}</title>`);\n lines.push(`<link>${escapeXml(item.link)}</link>`);\n lines.push(`<guid isPermaLink=\"false\">${escapeXml(guid)}</guid>`);\n if (item.description) {\n lines.push(`<description>${cdata(item.description)}</description>`);\n }\n const pub = toRfc822(item.pubDate);\n if (pub) lines.push(`<pubDate>${pub}</pubDate>`);\n lines.push(\"</item>\");\n }\n lines.push(\"</channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\n/** CmsPostContent[] → RSS item[]. postUrl 로 글 절대 URL 생성. */\nexport function rssItemsFromPosts(\n posts: CmsPostContent[],\n opts: { postUrl: (post: CmsPostContent) => string },\n): RssItemInput[] {\n return posts.map((p) => ({\n title: p.title,\n link: opts.postUrl(p),\n guid: opts.postUrl(p),\n description: p.excerpt,\n pubDate: p.publishedAt,\n }));\n}\n\nfunction toRfc822(value: string | Date | null | undefined): string {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toUTCString();\n}\n\n// ── Schema.org JSON-LD 빌더 ─────────────────────────────────────────────────\n\ntype JsonLd = Record<string, unknown>;\n\nexport interface ArticleSchemaInput {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | Date | null;\n dateModified?: string | Date | null;\n authorName?: string | null;\n publisherName?: string;\n publisherLogo?: string;\n}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.description) schema.description = input.description;\n if (input.image) schema.image = input.image;\n const pub = toIso(input.datePublished);\n if (pub) schema.datePublished = pub;\n const mod = toIso(input.dateModified);\n if (mod) schema.dateModified = mod;\n if (input.authorName)\n schema.author = { \"@type\": \"Person\", name: input.authorName };\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n return schema;\n}\n\nexport function breadcrumbSchema(\n items: { name: string; url: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\nexport function organizationSchema(input: {\n name: string;\n url: string;\n logo?: string;\n sameAs?: string[];\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n name: input.name,\n url: input.url,\n ...(input.logo ? { logo: input.logo } : {}),\n ...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),\n };\n}\n\nexport function websiteSchema(input: {\n name: string;\n url: string;\n searchUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: input.name,\n url: input.url,\n ...(input.searchUrl\n ? {\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: `${input.searchUrl}{search_term_string}`,\n \"query-input\": \"required name=search_term_string\",\n },\n }\n : {}),\n };\n}\n\nexport function faqSchema(\n qa: { question: string; answer: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: qa.map((x) => ({\n \"@type\": \"Question\",\n name: x.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: x.answer },\n })),\n };\n}\n\n/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n const json = JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/ld+json\">${json}</script>`;\n}\n\nfunction toIso(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nfunction cdata(value: string): string {\n return `<![CDATA[${value.replace(/]]>/g, \"]]]]><![CDATA[>\")}]]>`;\n}\n","/**\n * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.\n *\n * 외부 고객사 사이트가 *블로그 조회(`fetchPosts`)와 동일한 API key 하나* 로 리드도\n * 접수하기 위한 SSR-only 클라이언트. 브라우저가 아니라 사이트 서버(Server Action /\n * Route Handler)에서 호출해야 한다 — API key 는 절대 브라우저로 노출 금지\n * (ADR-0023 §5.1 #15). 키가 tenant 를 권위 식별하므로 `tenant_slug` 불필요.\n *\n * 사용 예 (Next.js server action):\n * ```ts\n * \"use server\";\n * import { submitInquiry } from \"@roottale/cms-client/server\";\n *\n * const result = await submitInquiry({\n * apiKey: process.env.ROOTTALE_API_KEY!, // fetchPosts 와 같은 키\n * fields: {\n * vertical: \"tax\",\n * contactName: name,\n * businessName: company,\n * email,\n * phone,\n * message,\n * privacyConsent: true,\n * },\n * });\n * if (result.ok) showSuccess(); // 그 키의 tenant /crm 에 도착\n * else showError(result.message);\n * ```\n *\n * intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.\n */\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport type InquiryVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\n/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */\nexport const INQUIRY_ERROR_MESSAGES: Record<string, string> = {\n consent_privacy: \"개인정보 수집·이용 동의가 필요합니다.\",\n consent_overseas: \"의료 분야는 국외이전 동의가 필요합니다.\",\n invalid_vertical: \"업종 분류가 유효하지 않습니다.\",\n missing_fields: \"필수 항목이 비어 있습니다.\",\n invalid_email: \"이메일 형식이 올바르지 않습니다.\",\n invalid_api_key: \"연동 인증에 실패했습니다. API 키를 확인해주세요.\",\n internal: \"전송에 실패했습니다. 잠시 후 다시 시도해주세요.\",\n};\n\nexport interface SubmitInquiryFields {\n vertical: InquiryVertical;\n contactName: string;\n businessName: string;\n email: string;\n phone: string;\n /** 자유 문의 내용 (선택). */\n message?: string;\n /** 상담 분야 라벨 등 부가 메타 (선택). */\n consultationField?: string;\n /** 현재 사이트 URL (선택). */\n currentSiteUrl?: string;\n /** 개인정보 수집·이용 동의 (필수 true). */\n privacyConsent: boolean;\n /** 의료 vertical 의 국외이전 동의 (medical 일 때 필수). */\n overseasTransferConsent?: boolean;\n /**\n * \"patient\"(고객사 방문자 문의, 기본) | \"sales\"(RootTale 자체 B2B 영업 리드).\n * sales 는 medical 국외이전 동의 게이트 면제.\n */\n leadKind?: \"patient\" | \"sales\";\n /**\n * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가\n * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.\n * 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.\n */\n extras?: Record<string, string>;\n}\n\n/** submitInquiry 가 전용으로 매핑하는 키 — extras 에서 충돌 시 무시. */\nconst RESERVED_INQUIRY_KEYS = new Set([\n \"vertical\",\n \"contact_name\",\n \"business_name\",\n \"email\",\n \"phone\",\n \"message\",\n \"consultation_field\",\n \"current_site_url\",\n \"privacy_consent\",\n \"overseas_transfer_consent\",\n \"lead_kind\",\n]);\n\nexport interface SubmitInquiryOptions {\n /** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */\n apiKey: string;\n /** API base URL 오버라이드. 기본 `https://api.roottale.com`. */\n baseUrl?: string;\n fields: SubmitInquiryFields;\n signal?: AbortSignal;\n}\n\nexport type SubmitInquiryResult =\n | { ok: true }\n | { ok: false; code: string | null; message: string };\n\n/** `err` 코드를 사용자 메시지로 (미지의 코드는 generic). */\nexport function inquiryErrorMessage(code: string | null | undefined): string {\n return (code && INQUIRY_ERROR_MESSAGES[code]) || INQUIRY_ERROR_MESSAGES.internal;\n}\n\n/**\n * intake API 의 302 Location (`?ok=1` / `?err=*`) 을 구조화 결과로 파싱.\n * 외부 사이트가 ERROR_MESSAGES·파싱을 각자 재구현하지 않도록 공유.\n */\nexport function parseInquiryRedirect(\n location: string | null,\n base: string = DEFAULT_BASE_URL,\n): SubmitInquiryResult {\n if (!location) {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n let url: URL;\n try {\n url = new URL(location, base);\n } catch {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n if (url.searchParams.get(\"ok\") === \"1\") return { ok: true };\n const code = url.searchParams.get(\"err\");\n return { ok: false, code, message: inquiryErrorMessage(code) };\n}\n\nexport function formatKoreanPhoneNumber(input: string): string {\n let digits = input.replace(/\\D/g, \"\");\n if (digits.startsWith(\"82\") && digits.length >= 11) {\n digits = `0${digits.slice(2)}`;\n }\n if (digits.length === 11) {\n return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n if (digits.startsWith(\"02\")) {\n return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;\n }\n if (digits.length === 8) {\n return `${digits.slice(0, 4)}-${digits.slice(4)}`;\n }\n return input.trim();\n}\n\n/**\n * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).\n * 키의 tenant 로 라우팅되어 admin(mysite.roottale.com) /crm 에서 관리된다.\n */\nexport async function submitInquiry(\n options: SubmitInquiryOptions,\n): Promise<SubmitInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const f = options.fields;\n\n const body = new FormData();\n body.set(\"vertical\", f.vertical);\n body.set(\"contact_name\", f.contactName);\n body.set(\"business_name\", f.businessName);\n body.set(\"email\", f.email);\n body.set(\"phone\", formatKoreanPhoneNumber(f.phone));\n if (f.message) body.set(\"message\", f.message);\n if (f.consultationField) body.set(\"consultation_field\", f.consultationField);\n if (f.currentSiteUrl) body.set(\"current_site_url\", f.currentSiteUrl);\n // 사용자가 명시 동의한 경우에만 전달 (자동 부여 금지).\n if (f.privacyConsent) body.set(\"privacy_consent\", \"on\");\n if (f.overseasTransferConsent) body.set(\"overseas_transfer_consent\", \"on\");\n if (f.leadKind) body.set(\"lead_kind\", f.leadKind);\n // 표준 외 임의 항목 — 예약 키/`_`접두/빈 값/중복은 건너뛰고 원본 그대로 전달.\n if (f.extras) {\n for (const [rawKey, rawValue] of Object.entries(f.extras)) {\n const key = rawKey.trim();\n const value = String(rawValue ?? \"\").trim();\n if (!key || !value) continue;\n if (key.startsWith(\"_\") || RESERVED_INQUIRY_KEYS.has(key)) continue;\n if (body.has(key)) continue;\n body.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/v1/public/inquiries`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body,\n // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n }\n\n // 잘못된/만료된 키 → 라우트가 401.\n if (response.status === 401) {\n return {\n ok: false,\n code: \"invalid_api_key\",\n message: inquiryErrorMessage(\"invalid_api_key\"),\n };\n }\n\n const location = response.headers.get(\"location\");\n if (location) return parseInquiryRedirect(location, baseUrl);\n\n // redirect 없이 성공 응답이면 ok 로 간주, 그 외엔 generic 실패.\n if (response.ok) return { ok: true };\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client submitInquiry must not run in the browser. \" +\n \"Call it from a Server Action / Route Handler. \" +\n \"Shipping the API key to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n","/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport type CmsTaxonomyKind = \"category\" | \"tag\" | \"custom\";\n\nexport interface CmsTermRef {\n id: string;\n taxonomy: CmsTaxonomyKind;\n slug: string;\n name: string;\n}\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n /**\n * featured 이미지 delivery URL (CF Images variant, md 우선). 서버가 `featured_media_id`\n * → `media.variants_json` 를 해석해 제공. 대표 이미지 미설정 시 null.\n */\n featuredImageUrl: string | null;\n authorId: string | null;\n /** Public display name for the post author. Email is not exposed. */\n authorName: string | null;\n /** Public profile image URL for the post author. */\n authorImageUrl: string | null;\n /** Public biography text for the post author. */\n authorBio: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /**\n * ADR-0041 — 포맷된 커스텀 필드(ACF) 값. 서버가 필드 정의(코드 ⊕ DB 그룹)로\n * `meta_json.acf` 를 포맷 — image/file/gallery 는 media variant URL 객체.\n * 해석된 그룹이 없거나 구 서버면 null.\n */\n fields: Record<string, unknown> | null;\n /**\n * Attached taxonomy terms (categories + tags + custom). Server 가 category →\n * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면\n * 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === \"category\")`.\n */\n terms: CmsTermRef[];\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\n// fleet 가시성 — 자기 버전을 자가 보고 (build-time 주입, tsup define).\n// api-core 가 사이트별 check-in 으로 기록 → internal `GET /v1/internal/sites`.\ndeclare const __CMS_CLIENT_VERSION__: string | undefined;\nconst CLIENT_VERSION =\n typeof __CMS_CLIENT_VERSION__ === \"string\" ? __CMS_CLIENT_VERSION__ : \"dev\";\n\n/** 설치된 cms-client 버전 (build-time 주입) — fleet well-known 라우트 등에서 사용. */\nexport const CMS_CLIENT_VERSION = CLIENT_VERSION;\n\nfunction authHeaders(apiKey: string): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey}`,\n \"x-roottale-client\": `cms-client/${CLIENT_VERSION}`,\n };\n}\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (mysite.roottale.com) 에서 저장한 값과 동일한 shape.\n * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.\n */\nexport interface RootTaleTheme {\n colors?: {\n primary?: string;\n primaryForeground?: string;\n foreground?: string;\n background?: string;\n secondary?: string;\n secondaryForeground?: string;\n accent?: string;\n accentForeground?: string;\n muted?: string;\n mutedForeground?: string;\n destructive?: string;\n destructiveForeground?: string;\n border?: string;\n input?: string;\n ring?: string;\n };\n fonts?: {\n body?: string;\n display?: string;\n };\n radius?: {\n sm?: string;\n md?: string;\n lg?: string;\n xl?: string;\n };\n}\n\nexport interface FetchThemeOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값\n * (없으면 빈 객체) — renderer 가 이 값을 CSS 변수로 주입한다.\n *\n * 자동 fetch (renderer 가 theme prop 없을 때) 와 명시 호출 모두 지원.\n * 네트워크/서버 에러는 throw — 호출자가 fallback 처리.\n */\nexport async function fetchTheme(options: FetchThemeOptions): Promise<RootTaleTheme> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n colors?: RootTaleTheme[\"colors\"];\n fonts?: RootTaleTheme[\"fonts\"];\n radius?: RootTaleTheme[\"radius\"];\n };\n const theme: RootTaleTheme = {};\n if (json.colors) theme.colors = json.colors;\n if (json.fonts) theme.fonts = json.fonts;\n if (json.radius) theme.radius = json.radius;\n return theme;\n}\n\nexport type AnalyticsTagProvider = \"ga4\" | \"clarity\" | \"meta_pixel\" | \"naver\";\n\n/** analytics-runtime `TagConfig` 와 구조 호환 — `buildManifest` 입력. */\nexport interface AnalyticsTagConfig {\n provider: AnalyticsTagProvider;\n id: string;\n enabled: boolean;\n}\n\nexport interface SiteAnalyticsTags {\n siteId: string;\n tags: AnalyticsTagConfig[];\n}\n\nexport interface FetchAnalyticsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement\n * id / Clarity project id 등, 비밀 아님). RootTale 관리용 default + 테넌트 고객용\n * 설정을 머지한 결과를 반환한다. analytics-runtime `buildManifest` 입력으로 쓰고,\n * 동의는 항상 fail-closed(런타임이 처리)이므로 키 노출 위험 없음(public id).\n */\nexport async function fetchAnalyticsConfig(\n options: FetchAnalyticsOptions,\n): Promise<SiteAnalyticsTags> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n site_id: string;\n tags?: AnalyticsTagConfig[];\n };\n return { siteId: json.site_id, tags: json.tags ?? [] };\n}\n\n/**\n * 블로그 표시 설정 — admin /design 의 \"블로그 표시\" 에서 저장한 사이트 기본값.\n * renderer 가 prop 미지정 시 이 값을 기본으로 적용 (글별 override 는 post 의\n * metaJson.display 가 우선).\n */\nexport interface RootTaleBlogSettings {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n /** 목차 상단 제목. null = renderer 기본(\"목차\"). */\n tocTitle: string | null;\n /** 작성자 카드 상단 라벨. null = renderer 기본(\"작성자\"). */\n authorCardEyebrow: string | null;\n /** 작성자 카드 설명. null = 설명 없음. */\n authorCardDescription: string | null;\n /** 작성자 프로필 이름. null = 글 작성자 이름 사용. */\n authorProfileName: string | null;\n /** 작성자 프로필 bio. null = 설명 없음. */\n authorProfileBio: string | null;\n /** 작성자 프로필 이미지 URL. null = 이니셜 아바타 사용. */\n authorProfileImageUrl: string | null;\n /** 작성자 프로필 이미지 모서리. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** 작성자 프로필 이미지 object-position x (%). */\n authorProfileImagePositionX: number;\n /** 작성자 프로필 이미지 object-position y (%). */\n authorProfileImagePositionY: number;\n}\n\nexport interface FetchBlogSettingsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —\n * 호출자(renderer)가 안전 기본값으로 fallback.\n */\nexport async function fetchBlogSettings(\n options: FetchBlogSettingsOptions,\n): Promise<RootTaleBlogSettings> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n show_table_of_contents?: boolean;\n show_author?: boolean;\n show_date?: boolean;\n show_author_card?: boolean;\n toc_title?: string | null;\n author_card_eyebrow?: string | null;\n author_card_description?: string | null;\n author_profile_name?: string | null;\n author_profile_bio?: string | null;\n author_profile_image_url?: string | null;\n author_profile_image_radius?: string | null;\n author_profile_image_position_x?: number;\n author_profile_image_position_y?: number;\n };\n const radius =\n json.author_profile_image_radius === \"rounded\" ||\n json.author_profile_image_radius === \"square\"\n ? json.author_profile_image_radius\n : \"circle\";\n const percent = (x: unknown): number =>\n typeof x === \"number\" && Number.isFinite(x)\n ? Math.max(0, Math.min(100, Math.round(x)))\n : 50;\n return {\n showTableOfContents: json.show_table_of_contents === true,\n showAuthor: json.show_author !== false,\n showDate: json.show_date !== false,\n showAuthorCard: json.show_author_card !== false,\n tocTitle:\n typeof json.toc_title === \"string\" && json.toc_title.length > 0\n ? json.toc_title\n : null,\n authorCardEyebrow:\n typeof json.author_card_eyebrow === \"string\" &&\n json.author_card_eyebrow.length > 0\n ? json.author_card_eyebrow\n : null,\n authorCardDescription:\n typeof json.author_card_description === \"string\" &&\n json.author_card_description.length > 0\n ? json.author_card_description\n : null,\n authorProfileName:\n typeof json.author_profile_name === \"string\" &&\n json.author_profile_name.length > 0\n ? json.author_profile_name\n : null,\n authorProfileBio:\n typeof json.author_profile_bio === \"string\" &&\n json.author_profile_bio.length > 0\n ? json.author_profile_bio\n : null,\n authorProfileImageUrl:\n typeof json.author_profile_image_url === \"string\" &&\n json.author_profile_image_url.length > 0\n ? json.author_profile_image_url\n : null,\n authorProfileImageRadius: radius,\n authorProfileImagePositionX: percent(json.author_profile_image_position_x),\n authorProfileImagePositionY: percent(json.author_profile_image_position_y),\n };\n}\n\n/** fetch 실패/미설정 시 사용할 안전 기본값 (기존 렌더러 동작). */\nexport const DEFAULT_BLOG_SETTINGS: RootTaleBlogSettings = {\n showTableOfContents: false,\n showAuthor: true,\n showDate: true,\n showAuthorCard: true,\n tocTitle: null,\n authorCardEyebrow: null,\n authorCardDescription: null,\n authorProfileName: null,\n authorProfileBio: null,\n authorProfileImageUrl: null,\n authorProfileImageRadius: \"circle\",\n authorProfileImagePositionX: 50,\n authorProfileImagePositionY: 50,\n};\n\nexport interface ResolvedPostDisplay {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n tocTitle: string;\n authorCardEyebrow: string;\n authorCardDescription: string | null;\n authorProfileName: string | null;\n authorProfileBio: string | null;\n authorProfileImageUrl: string | null;\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n authorProfileImagePositionX: number;\n authorProfileImagePositionY: number;\n}\n\n/**\n * 글 1건의 실제 표시 여부 결정. 우선순위:\n * 명시 prop > 글별 override(post.metaJson.display) > 사이트 기본값(settings).\n * tocTitle: 명시 prop > settings.tocTitle > \"목차\".\n */\nexport function resolvePostDisplay(\n post: { metaJson?: Record<string, unknown> | null },\n settings: RootTaleBlogSettings,\n explicit?: { showTableOfContents?: boolean; tableOfContentsTitle?: string },\n): ResolvedPostDisplay {\n const display =\n post.metaJson && typeof post.metaJson === \"object\"\n ? ((post.metaJson as Record<string, unknown>).display as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const ov = (key: string): boolean | undefined =>\n display && typeof display[key] === \"boolean\"\n ? (display[key] as boolean)\n : undefined;\n return {\n showTableOfContents:\n explicit?.showTableOfContents ?? ov(\"toc\") ?? settings.showTableOfContents,\n showAuthor: ov(\"author\") ?? settings.showAuthor,\n showDate: ov(\"date\") ?? settings.showDate,\n showAuthorCard: settings.showAuthorCard,\n tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? \"목차\",\n authorCardEyebrow: settings.authorCardEyebrow ?? \"작성자\",\n authorCardDescription: settings.authorCardDescription,\n authorProfileName: settings.authorProfileName,\n authorProfileBio: settings.authorProfileBio,\n authorProfileImageUrl: settings.authorProfileImageUrl,\n authorProfileImageRadius: settings.authorProfileImageRadius,\n authorProfileImagePositionX: settings.authorProfileImagePositionX,\n authorProfileImagePositionY: settings.authorProfileImagePositionY,\n };\n}\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return fromWire(json);\n}\n\ntype CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n slug: string;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n /** featured 이미지 delivery URL. 구 서버는 미포함 — backward-compat optional. */\n featured_media_url?: string | null;\n author_id: string | null;\n author_name?: string | null;\n author_image_url?: string | null;\n author_bio?: string | null;\n body_json: Record<string, unknown>;\n status: \"published\";\n meta_json: Record<string, unknown>;\n /** ADR-0041 포맷 필드. 구 서버는 미포함 — backward-compat optional. */\n fields?: Record<string, unknown> | null;\n /** 0.2.0 이전 서버는 미포함 — backward-compat 차원에서 optional. */\n terms?: CmsTermRef[];\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n slug: wire.slug,\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorBio: wire.author_bio ?? null,\n bodyJson: wire.body_json,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n terms: wire.terms ?? [],\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n\nasync function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n\n// 프레임워크 무관 SEO 헬퍼 (RSS 2.0 + Schema.org JSON-LD) 재노출.\nexport * from \"./seo\";\n\n// 리드 접수 — 블로그 조회와 동일한 API key 하나로 server-side 제출 (submitInquiry).\nexport * from \"./inquiry\";\n"],"mappings":"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","names":["DEFAULT_BASE_URL","assertServer"]}
|
|
1
|
+
{"version":3,"sources":["../src/seo.ts","../src/inquiry.ts","../src/server.ts"],"sourcesContent":["// 프레임워크 무관 SEO 헬퍼 — RSS 2.0 피드 + Schema.org JSON-LD 빌더.\n// 외부 사이트(Next route handler / Astro endpoint / RSC)가 fetchPosts 결과로\n// /feed.xml 응답이나 <head> JSON-LD 를 만들 때 사용. 순수 함수(네트워크 없음).\n\nimport type { CmsPostContent } from \"./server\";\n\n// ── RSS 2.0 ────────────────────────────────────────────────────────────────\n\nexport interface RssItemInput {\n title: string;\n /** 글 절대 URL. */\n link: string;\n /** 고유 식별자. 미지정 시 link 사용. */\n guid?: string;\n description?: string | null;\n /** 발행 시각. */\n pubDate?: string | Date | null;\n}\n\nexport interface RssFeedInput {\n /** 사이트/피드 제목. */\n title: string;\n /** 사이트 홈 URL. */\n link: string;\n /** 이 피드 자체의 URL (atom:self). */\n feedUrl: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n items: RssItemInput[];\n}\n\n/** RSS 2.0 XML 문자열 생성. */\nexport function generateRssXml(input: RssFeedInput): string {\n const lang = input.language ?? \"ko-KR\";\n const lastBuild =\n input.items.length > 0 ? toRfc822(input.items[0]!.pubDate) : \"\";\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">',\n );\n lines.push(\"<channel>\");\n lines.push(`<title>${escapeXml(input.title)}</title>`);\n lines.push(`<link>${escapeXml(input.link)}</link>`);\n lines.push(\n `<atom:link href=\"${escapeXml(input.feedUrl)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n lines.push(\n `<description>${escapeXml(input.description ?? input.title)}</description>`,\n );\n lines.push(`<language>${escapeXml(lang)}</language>`);\n if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);\n for (const item of input.items) {\n const guid = item.guid ?? item.link;\n lines.push(\"<item>\");\n lines.push(`<title>${escapeXml(item.title)}</title>`);\n lines.push(`<link>${escapeXml(item.link)}</link>`);\n lines.push(`<guid isPermaLink=\"false\">${escapeXml(guid)}</guid>`);\n if (item.description) {\n lines.push(`<description>${cdata(item.description)}</description>`);\n }\n const pub = toRfc822(item.pubDate);\n if (pub) lines.push(`<pubDate>${pub}</pubDate>`);\n lines.push(\"</item>\");\n }\n lines.push(\"</channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\n/** CmsPostContent[] → RSS item[]. postUrl 로 글 절대 URL 생성. */\nexport function rssItemsFromPosts(\n posts: CmsPostContent[],\n opts: { postUrl: (post: CmsPostContent) => string },\n): RssItemInput[] {\n return posts.map((p) => ({\n title: p.title,\n link: opts.postUrl(p),\n guid: opts.postUrl(p),\n description: p.excerpt,\n pubDate: p.publishedAt,\n }));\n}\n\nfunction toRfc822(value: string | Date | null | undefined): string {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? \"\" : d.toUTCString();\n}\n\n// ── Schema.org JSON-LD 빌더 ─────────────────────────────────────────────────\n\ntype JsonLd = Record<string, unknown>;\n\nexport interface ArticleSchemaInput {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | Date | null;\n dateModified?: string | Date | null;\n authorName?: string | null;\n publisherName?: string;\n publisherLogo?: string;\n}\n\nexport function articleSchema(input: ArticleSchemaInput): JsonLd {\n const schema: JsonLd = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: input.headline,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": input.url },\n };\n if (input.description) schema.description = input.description;\n if (input.image) schema.image = input.image;\n const pub = toIso(input.datePublished);\n if (pub) schema.datePublished = pub;\n const mod = toIso(input.dateModified);\n if (mod) schema.dateModified = mod;\n if (input.authorName)\n schema.author = { \"@type\": \"Person\", name: input.authorName };\n if (input.publisherName) {\n schema.publisher = {\n \"@type\": \"Organization\",\n name: input.publisherName,\n ...(input.publisherLogo\n ? { logo: { \"@type\": \"ImageObject\", url: input.publisherLogo } }\n : {}),\n };\n }\n return schema;\n}\n\nexport function breadcrumbSchema(\n items: { name: string; url: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((it, i) => ({\n \"@type\": \"ListItem\",\n position: i + 1,\n name: it.name,\n item: it.url,\n })),\n };\n}\n\nexport function organizationSchema(input: {\n name: string;\n url: string;\n logo?: string;\n sameAs?: string[];\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n name: input.name,\n url: input.url,\n ...(input.logo ? { logo: input.logo } : {}),\n ...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),\n };\n}\n\nexport function websiteSchema(input: {\n name: string;\n url: string;\n searchUrl?: string;\n}): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n name: input.name,\n url: input.url,\n ...(input.searchUrl\n ? {\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: `${input.searchUrl}{search_term_string}`,\n \"query-input\": \"required name=search_term_string\",\n },\n }\n : {}),\n };\n}\n\nexport function faqSchema(\n qa: { question: string; answer: string }[],\n): JsonLd {\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: qa.map((x) => ({\n \"@type\": \"Question\",\n name: x.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: x.answer },\n })),\n };\n}\n\n/** JSON-LD 객체 → `<script type=\"application/ld+json\">` HTML 문자열 (XSS-safe). */\nexport function jsonLdScript(schema: JsonLd | JsonLd[]): string {\n // `</script>` 주입 방지를 위해 `<` 이스케이프.\n const json = JSON.stringify(schema).replace(/</g, \"\\\\u003c\");\n return `<script type=\"application/ld+json\">${json}</script>`;\n}\n\nfunction toIso(value: string | Date | null | undefined): string | null {\n if (!value) return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d.toISOString();\n}\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nfunction cdata(value: string): string {\n return `<![CDATA[${value.replace(/]]>/g, \"]]]]><![CDATA[>\")}]]>`;\n}\n","/**\n * `@roottale/cms-client` — 리드(문의) 접수 server-side 헬퍼.\n *\n * 외부 고객사 사이트가 *블로그 조회(`fetchPosts`)와 동일한 API key 하나* 로 리드도\n * 접수하기 위한 SSR-only 클라이언트. 브라우저가 아니라 사이트 서버(Server Action /\n * Route Handler)에서 호출해야 한다 — API key 는 절대 브라우저로 노출 금지\n * (ADR-0023 §5.1 #15). 키가 tenant 를 권위 식별하므로 `tenant_slug` 불필요.\n *\n * 사용 예 (Next.js server action):\n * ```ts\n * \"use server\";\n * import { submitInquiry } from \"@roottale/cms-client/server\";\n *\n * const result = await submitInquiry({\n * apiKey: process.env.ROOTTALE_API_KEY!, // fetchPosts 와 같은 키\n * fields: {\n * vertical: \"tax\",\n * contactName: name,\n * businessName: company,\n * email,\n * phone,\n * message,\n * privacyConsent: true,\n * },\n * });\n * if (result.ok) showSuccess(); // 그 키의 tenant /crm 에 도착\n * else showError(result.message);\n * ```\n *\n * intake API 는 302 `?ok=1` / `?err=*` 로 응답 — 본 헬퍼가 파싱해 구조화 결과 반환.\n */\n\nimport type { InquiryAttribution } from \"./attribution\";\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\nexport type InquiryVertical = \"consulting\" | \"medical\" | \"tax\" | \"legal\";\n\n/** intake API 의 `err` 코드 → 한국어 사용자 메시지. */\nexport const INQUIRY_ERROR_MESSAGES: Record<string, string> = {\n consent_privacy: \"개인정보 수집·이용 동의가 필요합니다.\",\n consent_overseas: \"의료 분야는 국외이전 동의가 필요합니다.\",\n invalid_vertical: \"업종 분류가 유효하지 않습니다.\",\n missing_fields: \"필수 항목이 비어 있습니다.\",\n invalid_email: \"이메일 형식이 올바르지 않습니다.\",\n invalid_api_key: \"연동 인증에 실패했습니다. API 키를 확인해주세요.\",\n internal: \"전송에 실패했습니다. 잠시 후 다시 시도해주세요.\",\n};\n\nexport interface SubmitInquiryFields {\n vertical: InquiryVertical;\n contactName: string;\n businessName: string;\n email: string;\n phone: string;\n /** 자유 문의 내용 (선택). */\n message?: string;\n /** 상담 분야 라벨 등 부가 메타 (선택). */\n consultationField?: string;\n /** 현재 사이트 URL (선택). */\n currentSiteUrl?: string;\n /** 개인정보 수집·이용 동의 (필수 true). */\n privacyConsent: boolean;\n /** 의료 vertical 의 국외이전 동의 (medical 일 때 필수). */\n overseasTransferConsent?: boolean;\n /**\n * \"patient\"(고객사 방문자 문의, 기본) | \"sales\"(RootTale 자체 B2B 영업 리드).\n * sales 는 medical 국외이전 동의 게이트 면제.\n */\n leadKind?: \"patient\" | \"sales\";\n /**\n * 표준 필드 외에 폼이 받은 임의 항목(key→value)을 있는 그대로 전달. intake 가\n * 암호화 보관하고 CRM 상세에서 노출한다. 예약 키(표준 필드)·`_` 접두 키는 무시.\n * 주의: 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영할 것.\n */\n extras?: Record<string, string>;\n /**\n * 유입 first-touch 어트리뷰션 (ADR-0048 §2, 선택). 브라우저에서\n * `readAttribution()`(`@roottale/cms-client/attribution`)으로 읽어 Server\n * Action 인자로 넘긴 값을 그대로 전달하면 된다. CRM 에 \"이 문의가 어느\n * 글/링크/QR 에서 왔는지\"가 표시된다. null/undefined 면 미동반 제출.\n */\n attribution?: InquiryAttribution | null;\n}\n\n/** submitInquiry 가 전용으로 매핑하는 키 — extras 에서 충돌 시 무시. */\nconst RESERVED_INQUIRY_KEYS = new Set([\n \"vertical\",\n \"contact_name\",\n \"business_name\",\n \"email\",\n \"phone\",\n \"message\",\n \"consultation_field\",\n \"current_site_url\",\n \"privacy_consent\",\n \"overseas_transfer_consent\",\n \"lead_kind\",\n]);\n\nexport interface SubmitInquiryOptions {\n /** `rtlk_cust_*` API key — `fetchPosts` 와 동일한 키. Required. */\n apiKey: string;\n /** API base URL 오버라이드. 기본 `https://api.roottale.com`. */\n baseUrl?: string;\n fields: SubmitInquiryFields;\n signal?: AbortSignal;\n}\n\nexport type SubmitInquiryResult =\n | { ok: true }\n | { ok: false; code: string | null; message: string };\n\n/** `err` 코드를 사용자 메시지로 (미지의 코드는 generic). */\nexport function inquiryErrorMessage(code: string | null | undefined): string {\n return (code && INQUIRY_ERROR_MESSAGES[code]) || INQUIRY_ERROR_MESSAGES.internal;\n}\n\n/**\n * intake API 의 302 Location (`?ok=1` / `?err=*`) 을 구조화 결과로 파싱.\n * 외부 사이트가 ERROR_MESSAGES·파싱을 각자 재구현하지 않도록 공유.\n */\nexport function parseInquiryRedirect(\n location: string | null,\n base: string = DEFAULT_BASE_URL,\n): SubmitInquiryResult {\n if (!location) {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n let url: URL;\n try {\n url = new URL(location, base);\n } catch {\n return { ok: false, code: null, message: inquiryErrorMessage(null) };\n }\n if (url.searchParams.get(\"ok\") === \"1\") return { ok: true };\n const code = url.searchParams.get(\"err\");\n return { ok: false, code, message: inquiryErrorMessage(code) };\n}\n\nexport function formatKoreanPhoneNumber(input: string): string {\n let digits = input.replace(/\\D/g, \"\");\n if (digits.startsWith(\"82\") && digits.length >= 11) {\n digits = `0${digits.slice(2)}`;\n }\n if (digits.length === 11) {\n return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;\n }\n if (digits.length === 10) {\n if (digits.startsWith(\"02\")) {\n return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;\n }\n return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;\n }\n if (digits.length === 8) {\n return `${digits.slice(0, 4)}-${digits.slice(4)}`;\n }\n return input.trim();\n}\n\n/**\n * 리드를 RootTale intake API 로 제출 (server-to-server, Bearer 인증).\n * 키의 tenant 로 라우팅되어 admin(mysite.roottale.com) /crm 에서 관리된다.\n */\nexport async function submitInquiry(\n options: SubmitInquiryOptions,\n): Promise<SubmitInquiryResult> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const f = options.fields;\n\n const body = new FormData();\n body.set(\"vertical\", f.vertical);\n body.set(\"contact_name\", f.contactName);\n body.set(\"business_name\", f.businessName);\n body.set(\"email\", f.email);\n body.set(\"phone\", formatKoreanPhoneNumber(f.phone));\n if (f.message) body.set(\"message\", f.message);\n if (f.consultationField) body.set(\"consultation_field\", f.consultationField);\n if (f.currentSiteUrl) body.set(\"current_site_url\", f.currentSiteUrl);\n // 사용자가 명시 동의한 경우에만 전달 (자동 부여 금지).\n if (f.privacyConsent) body.set(\"privacy_consent\", \"on\");\n if (f.overseasTransferConsent) body.set(\"overseas_transfer_consent\", \"on\");\n if (f.leadKind) body.set(\"lead_kind\", f.leadKind);\n // 유입 어트리뷰션 → intake `attr_*` 필드 (허용 키만, 빈 값 생략).\n if (f.attribution) {\n for (const [key, value] of Object.entries(f.attribution)) {\n if (typeof value !== \"string\" || !value.trim()) continue;\n body.set(`attr_${key}`, value.trim());\n }\n }\n // 표준 외 임의 항목 — 예약 키/`_`접두/빈 값/중복은 건너뛰고 원본 그대로 전달.\n if (f.extras) {\n for (const [rawKey, rawValue] of Object.entries(f.extras)) {\n const key = rawKey.trim();\n const value = String(rawValue ?? \"\").trim();\n if (!key || !value) continue;\n if (key.startsWith(\"_\") || RESERVED_INQUIRY_KEYS.has(key)) continue;\n if (key.startsWith(\"attr_\")) continue; // 어트리뷰션 채널과 충돌 방지\n\n if (body.has(key)) continue;\n body.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/v1/public/inquiries`, {\n method: \"POST\",\n headers: { authorization: `Bearer ${options.apiKey}` },\n body,\n // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.\n redirect: \"manual\",\n signal: options.signal,\n });\n } catch {\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n }\n\n // 잘못된/만료된 키 → 라우트가 401.\n if (response.status === 401) {\n return {\n ok: false,\n code: \"invalid_api_key\",\n message: inquiryErrorMessage(\"invalid_api_key\"),\n };\n }\n\n const location = response.headers.get(\"location\");\n if (location) return parseInquiryRedirect(location, baseUrl);\n\n // redirect 없이 성공 응답이면 ok 로 간주, 그 외엔 generic 실패.\n if (response.ok) return { ok: true };\n return { ok: false, code: \"internal\", message: inquiryErrorMessage(\"internal\") };\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client submitInquiry must not run in the browser. \" +\n \"Call it from a Server Action / Route Handler. \" +\n \"Shipping the API key to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n","/**\n * `@roottale/cms-client/server` — RootTale CMS Public API server-side fetch client.\n *\n * 외부 고객사 사이트가 RootTale CMS 콘텐츠를 fetch 하기 위한 SSR-only client.\n * Bearer (`rtlk_cust_*`) 인증, 브라우저 직접 호출 금지 (ADR-0023 §5.1 #15).\n *\n * 사용 예 (Next.js server component):\n * ```ts\n * import { fetchPosts } from \"@roottale/cms-client/server\";\n *\n * const posts = await fetchPosts({\n * apiKey: process.env.ROOTTALE_API_KEY!,\n * limit: 10,\n * });\n * ```\n *\n * 데이터 모델 = ADR-0034 `posts` (WP 7.0-aligned, type=post|page).\n * Phase 1 scope: list + get. WP import/export, draft preview = 별도 PR.\n */\n\nexport type CmsPostType = \"post\" | \"page\";\n\nexport type CmsTaxonomyKind = \"category\" | \"tag\" | \"custom\";\n\nexport interface CmsTermRef {\n id: string;\n taxonomy: CmsTaxonomyKind;\n slug: string;\n name: string;\n}\n\nexport interface CmsPostContent {\n /** UUIDv7. */\n id: string;\n /** Owning tenant id. */\n tenantId: string;\n /** Owning site id (ADR-0034 P4 dual-key). */\n siteId: string;\n /** WP post type. */\n type: CmsPostType;\n /** Lowercase kebab-case slug (1-120 chars). */\n slug: string;\n title: string;\n excerpt: string | null;\n /** Media id (resolved by renderer / consumer to URL). */\n featuredMediaId: string | null;\n /**\n * featured 이미지 delivery URL (CF Images variant, md 우선). 서버가 `featured_media_id`\n * → `media.variants_json` 를 해석해 제공. 대표 이미지 미설정 시 null.\n */\n featuredImageUrl: string | null;\n authorId: string | null;\n /** Public display name for the post author. Email is not exposed. */\n authorName: string | null;\n /** Public profile image URL for the post author. */\n authorImageUrl: string | null;\n /** Public biography text for the post author. */\n authorBio: string | null;\n /** Tiptap / Gutenberg-compatible Block JSON. Sanitization is renderer's responsibility. */\n bodyJson: Record<string, unknown>;\n /** Public API only exposes `published`. */\n status: \"published\";\n /** Per-post meta (SEO overrides, canonical, og, ...). */\n metaJson: Record<string, unknown>;\n /**\n * ADR-0041 — 포맷된 커스텀 필드(ACF) 값. 서버가 필드 정의(코드 ⊕ DB 그룹)로\n * `meta_json.acf` 를 포맷 — image/file/gallery 는 media variant URL 객체.\n * 해석된 그룹이 없거나 구 서버면 null.\n */\n fields: Record<string, unknown> | null;\n /**\n * Attached taxonomy terms (categories + tags + custom). Server 가 category →\n * tag → custom 순서로 정렬해 보내며, 각 taxonomy 내부는 name asc. attach 가 0 개면\n * 빈 배열. category 만 추출하려면 `terms.filter(t => t.taxonomy === \"category\")`.\n */\n terms: CmsTermRef[];\n /** ISO 8601. */\n publishedAt: string;\n /** ISO 8601. */\n createdAt: string;\n /** ISO 8601. */\n updatedAt: string;\n}\n\nexport interface CmsPostContentPage {\n items: CmsPostContent[];\n nextCursor: string | null;\n hasMore: boolean;\n}\n\nconst DEFAULT_BASE_URL = \"https://api.roottale.com\";\n\n// fleet 가시성 — 자기 버전을 자가 보고 (build-time 주입, tsup define).\n// api-core 가 사이트별 check-in 으로 기록 → internal `GET /v1/internal/sites`.\ndeclare const __CMS_CLIENT_VERSION__: string | undefined;\nconst CLIENT_VERSION =\n typeof __CMS_CLIENT_VERSION__ === \"string\" ? __CMS_CLIENT_VERSION__ : \"dev\";\n\n/** 설치된 cms-client 버전 (build-time 주입) — fleet well-known 라우트 등에서 사용. */\nexport const CMS_CLIENT_VERSION = CLIENT_VERSION;\n\nfunction authHeaders(apiKey: string): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey}`,\n \"x-roottale-client\": `cms-client/${CLIENT_VERSION}`,\n };\n}\n\nexport interface FetchPostsOptions {\n /** `rtlk_cust_*` API key. Required. */\n apiKey: string;\n /** Override the API base URL. Defaults to `https://api.roottale.com`. */\n baseUrl?: string;\n /** Page size, 1-100, default 20. */\n limit?: number;\n /** Opaque pagination cursor returned from previous call. */\n cursor?: string;\n /** Filter by post type (`post` or `page`). Default = both. */\n type?: CmsPostType;\n /** Override the auto-resolved site id (Phase 1 = 1 site/tenant, usually omitted). */\n siteId?: string;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n}\n\nexport interface FetchPostOptions {\n apiKey: string;\n baseUrl?: string;\n /** Slug (kebab-case) or UUIDv7 id. */\n slugOrId: string;\n /** Override the auto-resolved site id. */\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * 외부 사이트가 cms-renderer 에 주입할 RootTale 디자인 토큰.\n * admin /design (mysite.roottale.com) 에서 저장한 값과 동일한 shape.\n * 미설정 필드는 응답에서 누락 → renderer 의 CSS fallback 이 사용된다.\n */\nexport interface RootTaleTheme {\n colors?: {\n primary?: string;\n primaryForeground?: string;\n foreground?: string;\n background?: string;\n secondary?: string;\n secondaryForeground?: string;\n accent?: string;\n accentForeground?: string;\n muted?: string;\n mutedForeground?: string;\n destructive?: string;\n destructiveForeground?: string;\n border?: string;\n input?: string;\n ring?: string;\n };\n fonts?: {\n body?: string;\n display?: string;\n };\n radius?: {\n sm?: string;\n md?: string;\n lg?: string;\n xl?: string;\n };\n}\n\nexport interface FetchThemeOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * tenant 의 RootTale 디자인 토큰을 가져온다. admin /design 의 마지막 저장값\n * (없으면 빈 객체) — renderer 가 이 값을 CSS 변수로 주입한다.\n *\n * 자동 fetch (renderer 가 theme prop 없을 때) 와 명시 호출 모두 지원.\n * 네트워크/서버 에러는 throw — 호출자가 fallback 처리.\n */\nexport async function fetchTheme(options: FetchThemeOptions): Promise<RootTaleTheme> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/theme${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n colors?: RootTaleTheme[\"colors\"];\n fonts?: RootTaleTheme[\"fonts\"];\n radius?: RootTaleTheme[\"radius\"];\n };\n const theme: RootTaleTheme = {};\n if (json.colors) theme.colors = json.colors;\n if (json.fonts) theme.fonts = json.fonts;\n if (json.radius) theme.radius = json.radius;\n return theme;\n}\n\nexport type AnalyticsTagProvider = \"ga4\" | \"clarity\" | \"meta_pixel\" | \"naver\";\n\n/** analytics-runtime `TagConfig` 와 구조 호환 — `buildManifest` 입력. */\nexport interface AnalyticsTagConfig {\n provider: AnalyticsTagProvider;\n id: string;\n enabled: boolean;\n}\n\nexport interface SiteAnalyticsTags {\n siteId: string;\n tags: AnalyticsTagConfig[];\n}\n\nexport interface FetchAnalyticsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * ADR-0038 plane ① — 외부 사이트가 `<head>` 에 주입할 런타임 태그(GA4 measurement\n * id / Clarity project id 등, 비밀 아님). RootTale 관리용 default + 테넌트 고객용\n * 설정을 머지한 결과를 반환한다. analytics-runtime `buildManifest` 입력으로 쓰고,\n * 동의는 항상 fail-closed(런타임이 처리)이므로 키 노출 위험 없음(public id).\n */\nexport async function fetchAnalyticsConfig(\n options: FetchAnalyticsOptions,\n): Promise<SiteAnalyticsTags> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/analytics${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n site_id: string;\n tags?: AnalyticsTagConfig[];\n };\n return { siteId: json.site_id, tags: json.tags ?? [] };\n}\n\n/**\n * 블로그 표시 설정 — admin /design 의 \"블로그 표시\" 에서 저장한 사이트 기본값.\n * renderer 가 prop 미지정 시 이 값을 기본으로 적용 (글별 override 는 post 의\n * metaJson.display 가 우선).\n */\nexport interface RootTaleBlogSettings {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n /** 목차 상단 제목. null = renderer 기본(\"목차\"). */\n tocTitle: string | null;\n /** 작성자 카드 상단 라벨. null = renderer 기본(\"작성자\"). */\n authorCardEyebrow: string | null;\n /** 작성자 카드 설명. null = 설명 없음. */\n authorCardDescription: string | null;\n /** 작성자 프로필 이름. null = 글 작성자 이름 사용. */\n authorProfileName: string | null;\n /** 작성자 프로필 bio. null = 설명 없음. */\n authorProfileBio: string | null;\n /** 작성자 프로필 이미지 URL. null = 이니셜 아바타 사용. */\n authorProfileImageUrl: string | null;\n /** 작성자 프로필 이미지 모서리. */\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n /** 작성자 프로필 이미지 object-position x (%). */\n authorProfileImagePositionX: number;\n /** 작성자 프로필 이미지 object-position y (%). */\n authorProfileImagePositionY: number;\n}\n\nexport interface FetchBlogSettingsOptions {\n apiKey: string;\n baseUrl?: string;\n siteId?: string;\n signal?: AbortSignal;\n}\n\n/**\n * tenant 의 블로그 표시 기본값을 가져온다. 네트워크/서버 에러는 throw —\n * 호출자(renderer)가 안전 기본값으로 fallback.\n */\nexport async function fetchBlogSettings(\n options: FetchBlogSettingsOptions,\n): Promise<RootTaleBlogSettings> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/blog-settings${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n show_table_of_contents?: boolean;\n show_author?: boolean;\n show_date?: boolean;\n show_author_card?: boolean;\n toc_title?: string | null;\n author_card_eyebrow?: string | null;\n author_card_description?: string | null;\n author_profile_name?: string | null;\n author_profile_bio?: string | null;\n author_profile_image_url?: string | null;\n author_profile_image_radius?: string | null;\n author_profile_image_position_x?: number;\n author_profile_image_position_y?: number;\n };\n const radius =\n json.author_profile_image_radius === \"rounded\" ||\n json.author_profile_image_radius === \"square\"\n ? json.author_profile_image_radius\n : \"circle\";\n const percent = (x: unknown): number =>\n typeof x === \"number\" && Number.isFinite(x)\n ? Math.max(0, Math.min(100, Math.round(x)))\n : 50;\n return {\n showTableOfContents: json.show_table_of_contents === true,\n showAuthor: json.show_author !== false,\n showDate: json.show_date !== false,\n showAuthorCard: json.show_author_card !== false,\n tocTitle:\n typeof json.toc_title === \"string\" && json.toc_title.length > 0\n ? json.toc_title\n : null,\n authorCardEyebrow:\n typeof json.author_card_eyebrow === \"string\" &&\n json.author_card_eyebrow.length > 0\n ? json.author_card_eyebrow\n : null,\n authorCardDescription:\n typeof json.author_card_description === \"string\" &&\n json.author_card_description.length > 0\n ? json.author_card_description\n : null,\n authorProfileName:\n typeof json.author_profile_name === \"string\" &&\n json.author_profile_name.length > 0\n ? json.author_profile_name\n : null,\n authorProfileBio:\n typeof json.author_profile_bio === \"string\" &&\n json.author_profile_bio.length > 0\n ? json.author_profile_bio\n : null,\n authorProfileImageUrl:\n typeof json.author_profile_image_url === \"string\" &&\n json.author_profile_image_url.length > 0\n ? json.author_profile_image_url\n : null,\n authorProfileImageRadius: radius,\n authorProfileImagePositionX: percent(json.author_profile_image_position_x),\n authorProfileImagePositionY: percent(json.author_profile_image_position_y),\n };\n}\n\n/** fetch 실패/미설정 시 사용할 안전 기본값 (기존 렌더러 동작). */\nexport const DEFAULT_BLOG_SETTINGS: RootTaleBlogSettings = {\n showTableOfContents: false,\n showAuthor: true,\n showDate: true,\n showAuthorCard: true,\n tocTitle: null,\n authorCardEyebrow: null,\n authorCardDescription: null,\n authorProfileName: null,\n authorProfileBio: null,\n authorProfileImageUrl: null,\n authorProfileImageRadius: \"circle\",\n authorProfileImagePositionX: 50,\n authorProfileImagePositionY: 50,\n};\n\nexport interface ResolvedPostDisplay {\n showTableOfContents: boolean;\n showAuthor: boolean;\n showDate: boolean;\n showAuthorCard: boolean;\n tocTitle: string;\n authorCardEyebrow: string;\n authorCardDescription: string | null;\n authorProfileName: string | null;\n authorProfileBio: string | null;\n authorProfileImageUrl: string | null;\n authorProfileImageRadius: \"circle\" | \"rounded\" | \"square\";\n authorProfileImagePositionX: number;\n authorProfileImagePositionY: number;\n}\n\n/**\n * 글 1건의 실제 표시 여부 결정. 우선순위:\n * 명시 prop > 글별 override(post.metaJson.display) > 사이트 기본값(settings).\n * tocTitle: 명시 prop > settings.tocTitle > \"목차\".\n */\nexport function resolvePostDisplay(\n post: { metaJson?: Record<string, unknown> | null },\n settings: RootTaleBlogSettings,\n explicit?: { showTableOfContents?: boolean; tableOfContentsTitle?: string },\n): ResolvedPostDisplay {\n const display =\n post.metaJson && typeof post.metaJson === \"object\"\n ? ((post.metaJson as Record<string, unknown>).display as\n | Record<string, unknown>\n | undefined)\n : undefined;\n const ov = (key: string): boolean | undefined =>\n display && typeof display[key] === \"boolean\"\n ? (display[key] as boolean)\n : undefined;\n return {\n showTableOfContents:\n explicit?.showTableOfContents ?? ov(\"toc\") ?? settings.showTableOfContents,\n showAuthor: ov(\"author\") ?? settings.showAuthor,\n showDate: ov(\"date\") ?? settings.showDate,\n showAuthorCard: settings.showAuthorCard,\n tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? \"목차\",\n authorCardEyebrow: settings.authorCardEyebrow ?? \"작성자\",\n authorCardDescription: settings.authorCardDescription,\n authorProfileName: settings.authorProfileName,\n authorProfileBio: settings.authorProfileBio,\n authorProfileImageUrl: settings.authorProfileImageUrl,\n authorProfileImageRadius: settings.authorProfileImageRadius,\n authorProfileImagePositionX: settings.authorProfileImagePositionX,\n authorProfileImagePositionY: settings.authorProfileImagePositionY,\n };\n}\n\nexport class CmsApiError extends Error {\n readonly status: number;\n readonly code: string;\n constructor(message: string, status: number, code: string) {\n super(message);\n this.name = \"CmsApiError\";\n this.status = status;\n this.code = code;\n }\n}\n\nexport async function fetchPosts(options: FetchPostsOptions): Promise<CmsPostContentPage> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.limit !== undefined) params.set(\"limit\", String(options.limit));\n if (options.cursor) params.set(\"cursor\", options.cursor);\n if (options.type) params.set(\"type\", options.type);\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as {\n items: CmsPostWire[];\n has_more: boolean;\n next_cursor: string | null;\n };\n return {\n items: json.items.map(fromWire),\n hasMore: json.has_more,\n nextCursor: json.next_cursor,\n };\n}\n\nexport async function fetchPost(options: FetchPostOptions): Promise<CmsPostContent | null> {\n assertServer();\n const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n const params = new URLSearchParams();\n if (options.siteId) params.set(\"site_id\", options.siteId);\n const qs = params.toString();\n const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : \"\"}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: authHeaders(options.apiKey),\n signal: options.signal,\n });\n\n if (response.status === 404) return null;\n if (!response.ok) {\n throw await toApiError(response);\n }\n\n const json = (await response.json()) as CmsPostWire;\n return fromWire(json);\n}\n\ntype CmsPostWire = {\n id: string;\n tenant_id: string;\n site_id: string;\n type: CmsPostType;\n slug: string;\n title: string;\n excerpt: string | null;\n featured_media_id: string | null;\n /** featured 이미지 delivery URL. 구 서버는 미포함 — backward-compat optional. */\n featured_media_url?: string | null;\n author_id: string | null;\n author_name?: string | null;\n author_image_url?: string | null;\n author_bio?: string | null;\n body_json: Record<string, unknown>;\n status: \"published\";\n meta_json: Record<string, unknown>;\n /** ADR-0041 포맷 필드. 구 서버는 미포함 — backward-compat optional. */\n fields?: Record<string, unknown> | null;\n /** 0.2.0 이전 서버는 미포함 — backward-compat 차원에서 optional. */\n terms?: CmsTermRef[];\n published_at: string;\n created_at: string;\n updated_at: string;\n};\n\nfunction fromWire(wire: CmsPostWire): CmsPostContent {\n return {\n id: wire.id,\n tenantId: wire.tenant_id,\n siteId: wire.site_id,\n type: wire.type,\n slug: wire.slug,\n title: wire.title,\n excerpt: wire.excerpt,\n featuredMediaId: wire.featured_media_id,\n featuredImageUrl: wire.featured_media_url ?? null,\n authorId: wire.author_id,\n authorName: wire.author_name ?? null,\n authorImageUrl: wire.author_image_url ?? null,\n authorBio: wire.author_bio ?? null,\n bodyJson: wire.body_json,\n status: wire.status,\n metaJson: wire.meta_json,\n fields: wire.fields ?? null,\n terms: wire.terms ?? [],\n publishedAt: wire.published_at,\n createdAt: wire.created_at,\n updatedAt: wire.updated_at,\n };\n}\n\nasync function toApiError(response: Response): Promise<CmsApiError> {\n let code = \"request_failed\";\n let message = `RootTale CMS API error: ${response.status}`;\n try {\n const body = (await response.json()) as { code?: string; message?: string };\n if (body.code) code = body.code;\n if (body.message) message = body.message;\n } catch {\n /* ignore body parse */\n }\n return new CmsApiError(message, response.status, code);\n}\n\nfunction assertServer(): void {\n if (typeof window !== \"undefined\") {\n throw new Error(\n \"@roottale/cms-client/server must not run in the browser. \" +\n \"Move this call to a Server Component, getServerSideProps, or a Route Handler. \" +\n \"Shipping API keys to the browser breaks the security model (ADR-0023 §5.1 #15).\",\n );\n }\n}\n\n// 프레임워크 무관 SEO 헬퍼 (RSS 2.0 + Schema.org JSON-LD) 재노출.\nexport * from \"./seo\";\n\n// 리드 접수 — 블로그 조회와 동일한 API key 하나로 server-side 제출 (submitInquiry).\nexport * from \"./inquiry\";\n\n// 유입 어트리뷰션 (ADR-0048 §2) — readAttribution() 은 브라우저 전용이므로\n// `@roottale/cms-client/attribution` 서브패스에서 import 할 것. JSON 파서는\n// FormData 기반 Server Action 편의를 위해 서버 엔트리에서도 재노출.\nexport { parseAttributionJson } from \"./attribution\";\nexport type { InquiryAttribution } from \"./attribution\";\n"],"mappings":"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","names":["DEFAULT_BASE_URL","assertServer"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roottale/cms-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RootTale CMS Public API server-side fetch client (Bearer rtlk_cust_* auth). SSR-only — refuses to run in the browser to prevent key leak (ADR-0023 §5.1 #15). Pairs with @roottale/cms-renderer-next / @roottale/cms-renderer-astro.",
|
|
6
6
|
"main": "./dist/server.js",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"import": "./dist/server.js",
|
|
12
12
|
"default": "./dist/server.js"
|
|
13
13
|
},
|
|
14
|
+
"./attribution": {
|
|
15
|
+
"types": "./dist/attribution.d.ts",
|
|
16
|
+
"import": "./dist/attribution.js",
|
|
17
|
+
"default": "./dist/attribution.js"
|
|
18
|
+
},
|
|
14
19
|
"./webhook": {
|
|
15
20
|
"types": "./dist/webhook.d.ts",
|
|
16
21
|
"import": "./dist/webhook.js",
|