@roottale/cms-client 0.31.0 → 0.35.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/server.js CHANGED
@@ -2,346 +2,49 @@ import {
2
2
  parseAttributionJson
3
3
  } from "./chunk-FRCLSU55.js";
4
4
 
5
- // src/seo.ts
6
- function generateRssXml(input) {
7
- const lang = input.language ?? "ko-KR";
8
- const lastBuild = input.items.length > 0 ? toRfc822(input.items[0].pubDate) : "";
9
- const lines = [];
10
- lines.push('<?xml version="1.0" encoding="UTF-8"?>');
11
- lines.push(
12
- '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">'
13
- );
14
- lines.push("<channel>");
15
- lines.push(`<title>${escapeXml(input.title)}</title>`);
16
- lines.push(`<link>${escapeXml(input.link)}</link>`);
17
- lines.push(
18
- `<atom:link href="${escapeXml(input.feedUrl)}" rel="self" type="application/rss+xml" />`
19
- );
20
- lines.push(
21
- `<description>${escapeXml(input.description ?? input.title)}</description>`
22
- );
23
- lines.push(`<language>${escapeXml(lang)}</language>`);
24
- if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);
25
- for (const item of input.items) {
26
- const guid = item.guid ?? item.link;
27
- lines.push("<item>");
28
- lines.push(`<title>${escapeXml(item.title)}</title>`);
29
- lines.push(`<link>${escapeXml(item.link)}</link>`);
30
- lines.push(`<guid isPermaLink="false">${escapeXml(guid)}</guid>`);
31
- if (item.description) {
32
- lines.push(`<description>${cdata(item.description)}</description>`);
33
- }
34
- const pub = toRfc822(item.pubDate);
35
- if (pub) lines.push(`<pubDate>${pub}</pubDate>`);
36
- lines.push("</item>");
37
- }
38
- lines.push("</channel>");
39
- lines.push("</rss>");
40
- return lines.join("\n");
41
- }
42
- function rssItemsFromPosts(posts, opts) {
43
- return posts.map((p) => ({
44
- title: p.title,
45
- link: opts.postUrl(p),
46
- guid: opts.postUrl(p),
47
- description: p.excerpt,
48
- pubDate: p.publishedAt
49
- }));
50
- }
51
- function toRfc822(value) {
52
- if (!value) return "";
53
- const d = value instanceof Date ? value : new Date(value);
54
- return Number.isNaN(d.getTime()) ? "" : d.toUTCString();
55
- }
56
- function articleSchema(input) {
57
- const schema = {
58
- "@context": "https://schema.org",
59
- "@type": "Article",
60
- headline: input.headline,
61
- mainEntityOfPage: { "@type": "WebPage", "@id": input.url }
62
- };
63
- if (input.description) schema.description = input.description;
64
- if (input.image) schema.image = input.image;
65
- const pub = toIso(input.datePublished);
66
- if (pub) schema.datePublished = pub;
67
- const mod = toIso(input.dateModified);
68
- if (mod) schema.dateModified = mod;
69
- if (input.authorName)
70
- schema.author = { "@type": "Person", name: input.authorName };
71
- if (input.publisherName) {
72
- schema.publisher = {
73
- "@type": "Organization",
74
- name: input.publisherName,
75
- ...input.publisherLogo ? { logo: { "@type": "ImageObject", url: input.publisherLogo } } : {}
76
- };
77
- }
78
- return schema;
79
- }
80
- function breadcrumbSchema(items) {
81
- return {
82
- "@context": "https://schema.org",
83
- "@type": "BreadcrumbList",
84
- itemListElement: items.map((it, i) => ({
85
- "@type": "ListItem",
86
- position: i + 1,
87
- name: it.name,
88
- item: it.url
89
- }))
90
- };
91
- }
92
- function organizationSchema(input) {
93
- return {
94
- "@context": "https://schema.org",
95
- "@type": "Organization",
96
- name: input.name,
97
- url: input.url,
98
- ...input.logo ? { logo: input.logo } : {},
99
- ...input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}
100
- };
101
- }
102
- var DAY_OF_WEEK = {
103
- Mo: "Monday",
104
- Tu: "Tuesday",
105
- We: "Wednesday",
106
- Th: "Thursday",
107
- Fr: "Friday",
108
- Sa: "Saturday",
109
- Su: "Sunday"
110
- };
111
- function localBusinessSchema(profile, opts) {
112
- const input = profile;
113
- const types = Array.from(
114
- /* @__PURE__ */ new Set([input.businessType || "LocalBusiness", "LocalBusiness"])
115
- );
116
- const schema = {
117
- "@context": "https://schema.org",
118
- "@type": types,
119
- name: input.name,
120
- url: opts.url
121
- };
122
- if (input.legalName) schema.legalName = input.legalName;
123
- if (opts.image) schema.image = opts.image;
124
- if (input.telephone) schema.telephone = input.telephone;
125
- if (input.email) schema.email = input.email;
126
- if (input.address) {
127
- schema.address = {
128
- "@type": "PostalAddress",
129
- streetAddress: input.address.streetAddress,
130
- addressLocality: input.address.addressLocality,
131
- ...input.address.addressRegion ? { addressRegion: input.address.addressRegion } : {},
132
- ...input.address.postalCode ? { postalCode: input.address.postalCode } : {},
133
- addressCountry: "KR"
134
- };
135
- }
136
- if (input.geo) {
137
- schema.geo = {
138
- "@type": "GeoCoordinates",
139
- latitude: input.geo.latitude,
140
- longitude: input.geo.longitude
141
- };
142
- }
143
- const openingHours = (input.openingHours ?? []).filter(
144
- (row) => row.days.length > 0 && row.opens && row.closes
145
- );
146
- if (openingHours.length > 0) {
147
- schema.openingHoursSpecification = openingHours.map((row) => ({
148
- "@type": "OpeningHoursSpecification",
149
- dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),
150
- opens: row.opens,
151
- closes: row.closes
152
- }));
153
- }
154
- if (input.priceRange) schema.priceRange = input.priceRange;
155
- if (input.areaServed && input.areaServed.length > 0) {
156
- schema.areaServed = input.areaServed;
157
- }
158
- const sameAs = Object.values(input.profiles ?? {}).filter(
159
- (u) => typeof u === "string" && u.length > 0
160
- );
161
- if (sameAs.length > 0) schema.sameAs = sameAs;
162
- return schema;
163
- }
164
- function websiteSchema(input) {
165
- return {
166
- "@context": "https://schema.org",
167
- "@type": "WebSite",
168
- name: input.name,
169
- url: input.url,
170
- ...input.searchUrl ? {
171
- potentialAction: {
172
- "@type": "SearchAction",
173
- target: `${input.searchUrl}{search_term_string}`,
174
- "query-input": "required name=search_term_string"
175
- }
176
- } : {}
177
- };
178
- }
179
- function faqSchema(qa) {
5
+ // src/server-base.ts
6
+ var DEFAULT_BASE_URL = "https://api.roottale.com";
7
+ var CLIENT_VERSION = true ? "0.35.0" : "dev";
8
+ var CMS_CLIENT_VERSION = CLIENT_VERSION;
9
+ function authHeaders(apiKey) {
180
10
  return {
181
- "@context": "https://schema.org",
182
- "@type": "FAQPage",
183
- mainEntity: qa.map((x) => ({
184
- "@type": "Question",
185
- name: x.question,
186
- acceptedAnswer: { "@type": "Answer", text: x.answer }
187
- }))
11
+ authorization: `Bearer ${apiKey}`,
12
+ "x-roottale-client": `cms-client/${CLIENT_VERSION}`
188
13
  };
189
14
  }
190
- function jsonLdScript(schema) {
191
- const json = JSON.stringify(schema).replace(/</g, "\\u003c");
192
- return `<script type="application/ld+json">${json}</script>`;
193
- }
194
- function toIso(value) {
195
- if (!value) return null;
196
- const d = value instanceof Date ? value : new Date(value);
197
- return Number.isNaN(d.getTime()) ? null : d.toISOString();
198
- }
199
- function escapeXml(value) {
200
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
201
- }
202
- function cdata(value) {
203
- return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
204
- }
205
-
206
- // src/inquiry.ts
207
- var DEFAULT_BASE_URL = "https://api.roottale.com";
208
- var INQUIRY_ERROR_MESSAGES = {
209
- consent_privacy: "\uAC1C\uC778\uC815\uBCF4 \uC218\uC9D1\xB7\uC774\uC6A9 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
210
- consent_overseas: "\uC758\uB8CC \uBD84\uC57C\uB294 \uAD6D\uC678\uC774\uC804 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
211
- invalid_vertical: "\uC5C5\uC885 \uBD84\uB958\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
212
- missing_fields: "\uD544\uC218 \uD56D\uBAA9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
213
- invalid_email: "\uC774\uBA54\uC77C \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
214
- invalid_api_key: "\uC5F0\uB3D9 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
215
- internal: "\uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
216
- };
217
- var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
218
- "vertical",
219
- "contact_name",
220
- "business_name",
221
- "email",
222
- "phone",
223
- "message",
224
- "consultation_field",
225
- "current_site_url",
226
- "privacy_consent",
227
- "overseas_transfer_consent",
228
- "lead_kind"
229
- ]);
230
- function inquiryErrorMessage(code) {
231
- return code && INQUIRY_ERROR_MESSAGES[code] || INQUIRY_ERROR_MESSAGES.internal;
232
- }
233
- function parseInquiryRedirect(location, base = DEFAULT_BASE_URL) {
234
- if (!location) {
235
- return { ok: false, code: null, message: inquiryErrorMessage(null) };
236
- }
237
- let url;
238
- try {
239
- url = new URL(location, base);
240
- } catch {
241
- return { ok: false, code: null, message: inquiryErrorMessage(null) };
242
- }
243
- if (url.searchParams.get("ok") === "1") return { ok: true };
244
- const code = url.searchParams.get("err");
245
- return { ok: false, code, message: inquiryErrorMessage(code) };
246
- }
247
- function formatKoreanPhoneNumber(input) {
248
- let digits = input.replace(/\D/g, "");
249
- if (digits.startsWith("82") && digits.length >= 11) {
250
- digits = `0${digits.slice(2)}`;
251
- }
252
- if (digits.length === 11) {
253
- return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
254
- }
255
- if (digits.length === 10) {
256
- if (digits.startsWith("02")) {
257
- return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;
258
- }
259
- return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
260
- }
261
- if (digits.length === 8) {
262
- return `${digits.slice(0, 4)}-${digits.slice(4)}`;
263
- }
264
- return input.trim();
265
- }
266
- async function submitInquiry(options) {
267
- assertServer();
268
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
269
- const f = options.fields;
270
- const body = new FormData();
271
- body.set("vertical", f.vertical);
272
- body.set("contact_name", f.contactName);
273
- body.set("business_name", f.businessName);
274
- body.set("email", f.email);
275
- body.set("phone", formatKoreanPhoneNumber(f.phone));
276
- if (f.message) body.set("message", f.message);
277
- if (f.consultationField) body.set("consultation_field", f.consultationField);
278
- if (f.currentSiteUrl) body.set("current_site_url", f.currentSiteUrl);
279
- if (f.privacyConsent) body.set("privacy_consent", "on");
280
- if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
281
- if (f.leadKind) body.set("lead_kind", f.leadKind);
282
- if (f.attribution) {
283
- for (const [key, value] of Object.entries(f.attribution)) {
284
- if (typeof value !== "string" || !value.trim()) continue;
285
- body.set(`attr_${key}`, value.trim());
286
- }
287
- }
288
- if (f.extras) {
289
- for (const [rawKey, rawValue] of Object.entries(f.extras)) {
290
- const key = rawKey.trim();
291
- const value = String(rawValue ?? "").trim();
292
- if (!key || !value) continue;
293
- if (key.startsWith("_") || RESERVED_INQUIRY_KEYS.has(key)) continue;
294
- if (key.startsWith("attr_")) continue;
295
- if (body.has(key)) continue;
296
- body.set(key, value);
297
- }
298
- }
299
- let response;
300
- try {
301
- response = await fetch(`${baseUrl}/v1/public/inquiries`, {
302
- method: "POST",
303
- headers: { authorization: `Bearer ${options.apiKey}` },
304
- body,
305
- // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.
306
- redirect: "manual",
307
- signal: options.signal
308
- });
309
- } catch {
310
- return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
15
+ var CmsApiError = class extends Error {
16
+ status;
17
+ code;
18
+ constructor(message, status, code) {
19
+ super(message);
20
+ this.name = "CmsApiError";
21
+ this.status = status;
22
+ this.code = code;
311
23
  }
312
- if (response.status === 401) {
313
- return {
314
- ok: false,
315
- code: "invalid_api_key",
316
- message: inquiryErrorMessage("invalid_api_key")
317
- };
24
+ };
25
+ async function toApiError(response) {
26
+ let code = "request_failed";
27
+ let message = `RootTale CMS API error: ${response.status}`;
28
+ try {
29
+ const body = await response.json();
30
+ if (body.code) code = body.code;
31
+ if (body.message) message = body.message;
32
+ } catch {
318
33
  }
319
- const location = response.headers.get("location");
320
- if (location) return parseInquiryRedirect(location, baseUrl);
321
- if (response.ok) return { ok: true };
322
- return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
34
+ return new CmsApiError(message, response.status, code);
323
35
  }
324
36
  function assertServer() {
325
37
  if (typeof window !== "undefined") {
326
38
  throw new Error(
327
- "@roottale/cms-client submitInquiry must not run in the browser. Call it from a Server Action / Route Handler. Shipping the API key to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
39
+ "@roottale/cms-client/server must not run in the browser. Move this call to a Server Component, getServerSideProps, or a Route Handler. Shipping API keys to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
328
40
  );
329
41
  }
330
42
  }
331
43
 
332
- // src/server.ts
333
- var DEFAULT_BASE_URL2 = "https://api.roottale.com";
334
- var CLIENT_VERSION = true ? "0.31.0" : "dev";
335
- var CMS_CLIENT_VERSION = CLIENT_VERSION;
336
- function authHeaders(apiKey) {
337
- return {
338
- authorization: `Bearer ${apiKey}`,
339
- "x-roottale-client": `cms-client/${CLIENT_VERSION}`
340
- };
341
- }
44
+ // src/server-posts-theme.ts
342
45
  async function fetchTheme(options) {
343
- assertServer2();
344
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
46
+ assertServer();
47
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
345
48
  const params = new URLSearchParams();
346
49
  if (options.siteId) params.set("site_id", options.siteId);
347
50
  const qs = params.toString();
@@ -362,8 +65,8 @@ async function fetchTheme(options) {
362
65
  return theme;
363
66
  }
364
67
  async function fetchAnalyticsConfig(options) {
365
- assertServer2();
366
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
68
+ assertServer();
69
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
367
70
  const params = new URLSearchParams();
368
71
  if (options.siteId) params.set("site_id", options.siteId);
369
72
  const qs = params.toString();
@@ -379,9 +82,81 @@ async function fetchAnalyticsConfig(options) {
379
82
  const json = await response.json();
380
83
  return { siteId: json.site_id, tags: json.tags ?? [] };
381
84
  }
85
+ async function fetchPosts(options) {
86
+ assertServer();
87
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
88
+ const params = new URLSearchParams();
89
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
90
+ if (options.cursor) params.set("cursor", options.cursor);
91
+ if (options.type) params.set("type", options.type);
92
+ if (options.siteId) params.set("site_id", options.siteId);
93
+ const qs = params.toString();
94
+ const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
95
+ const response = await fetch(url, {
96
+ method: "GET",
97
+ headers: authHeaders(options.apiKey),
98
+ signal: options.signal
99
+ });
100
+ if (!response.ok) {
101
+ throw await toApiError(response);
102
+ }
103
+ const json = await response.json();
104
+ return {
105
+ items: json.items.map(fromWire),
106
+ hasMore: json.has_more,
107
+ nextCursor: json.next_cursor
108
+ };
109
+ }
110
+ async function fetchPost(options) {
111
+ assertServer();
112
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
113
+ const params = new URLSearchParams();
114
+ if (options.siteId) params.set("site_id", options.siteId);
115
+ const qs = params.toString();
116
+ const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
117
+ const response = await fetch(url, {
118
+ method: "GET",
119
+ headers: authHeaders(options.apiKey),
120
+ signal: options.signal
121
+ });
122
+ if (response.status === 404) return null;
123
+ if (!response.ok) {
124
+ throw await toApiError(response);
125
+ }
126
+ const json = await response.json();
127
+ return fromWire(json);
128
+ }
129
+ function fromWire(wire) {
130
+ return {
131
+ id: wire.id,
132
+ tenantId: wire.tenant_id,
133
+ siteId: wire.site_id,
134
+ type: wire.type,
135
+ collectionKey: wire.collection_key ?? null,
136
+ slug: wire.slug,
137
+ title: wire.title,
138
+ excerpt: wire.excerpt,
139
+ featuredMediaId: wire.featured_media_id,
140
+ featuredImageUrl: wire.featured_media_url ?? null,
141
+ authorId: wire.author_id,
142
+ authorName: wire.author_name ?? null,
143
+ authorImageUrl: wire.author_image_url ?? null,
144
+ authorBio: wire.author_bio ?? null,
145
+ bodyJson: wire.body_json,
146
+ status: wire.status,
147
+ metaJson: wire.meta_json,
148
+ fields: wire.fields ?? null,
149
+ terms: wire.terms ?? [],
150
+ publishedAt: wire.published_at,
151
+ createdAt: wire.created_at,
152
+ updatedAt: wire.updated_at
153
+ };
154
+ }
155
+
156
+ // src/server-blog.ts
382
157
  async function fetchBlogSettings(options) {
383
- assertServer2();
384
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
158
+ assertServer();
159
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
385
160
  const params = new URLSearchParams();
386
161
  if (options.siteId) params.set("site_id", options.siteId);
387
162
  const qs = params.toString();
@@ -414,8 +189,8 @@ async function fetchBlogSettings(options) {
414
189
  };
415
190
  }
416
191
  async function fetchCollections(options) {
417
- assertServer2();
418
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
192
+ assertServer();
193
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
419
194
  const params = new URLSearchParams();
420
195
  if (options.siteId) params.set("site_id", options.siteId);
421
196
  const qs = params.toString();
@@ -467,6 +242,27 @@ var DEFAULT_BLOG_SETTINGS = {
467
242
  authorProfileImagePositionX: 50,
468
243
  authorProfileImagePositionY: 50
469
244
  };
245
+ function resolvePostDisplay(post, settings, explicit) {
246
+ const display = post.metaJson && typeof post.metaJson === "object" ? post.metaJson.display : void 0;
247
+ const ov = (key) => display && typeof display[key] === "boolean" ? display[key] : void 0;
248
+ return {
249
+ showTableOfContents: explicit?.showTableOfContents ?? ov("toc") ?? settings.showTableOfContents,
250
+ showAuthor: ov("author") ?? settings.showAuthor,
251
+ showDate: ov("date") ?? settings.showDate,
252
+ showAuthorCard: settings.showAuthorCard,
253
+ tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? "\uBAA9\uCC28",
254
+ authorCardEyebrow: settings.authorCardEyebrow ?? "\uC791\uC131\uC790",
255
+ authorCardDescription: settings.authorCardDescription,
256
+ authorProfileName: settings.authorProfileName,
257
+ authorProfileBio: settings.authorProfileBio,
258
+ authorProfileImageUrl: settings.authorProfileImageUrl,
259
+ authorProfileImageRadius: settings.authorProfileImageRadius,
260
+ authorProfileImagePositionX: settings.authorProfileImagePositionX,
261
+ authorProfileImagePositionY: settings.authorProfileImagePositionY
262
+ };
263
+ }
264
+
265
+ // src/server-business.ts
470
266
  function businessProfileFromWire(json) {
471
267
  if (json.configured === false) return null;
472
268
  if (typeof json.name !== "string" || json.name.length === 0) return null;
@@ -483,242 +279,479 @@ function businessProfileFromWire(json) {
483
279
  openingHours.push({ days: row.days, opens: row.opens, closes: row.closes });
484
280
  }
485
281
  }
486
- return {
487
- name: json.name,
488
- legalName: json.legal_name ?? null,
489
- businessType: typeof json.business_type === "string" && json.business_type.length > 0 ? json.business_type : "LocalBusiness",
490
- telephone: json.telephone ?? null,
491
- email: json.email ?? null,
492
- address,
493
- geo,
494
- openingHours,
495
- priceRange: json.price_range ?? null,
496
- areaServed: json.area_served ?? [],
497
- profiles: {
498
- naverPlace: json.profiles?.naver_place ?? null,
499
- googleBusiness: json.profiles?.google_business ?? null,
500
- kakaoChannel: json.profiles?.kakao_channel ?? null,
501
- instagram: json.profiles?.instagram ?? null,
502
- naverBlog: json.profiles?.naver_blog ?? null
503
- }
504
- };
282
+ return {
283
+ name: json.name,
284
+ legalName: json.legal_name ?? null,
285
+ businessType: typeof json.business_type === "string" && json.business_type.length > 0 ? json.business_type : "LocalBusiness",
286
+ telephone: json.telephone ?? null,
287
+ email: json.email ?? null,
288
+ address,
289
+ geo,
290
+ openingHours,
291
+ priceRange: json.price_range ?? null,
292
+ areaServed: json.area_served ?? [],
293
+ profiles: {
294
+ naverPlace: json.profiles?.naver_place ?? null,
295
+ googleBusiness: json.profiles?.google_business ?? null,
296
+ kakaoChannel: json.profiles?.kakao_channel ?? null,
297
+ instagram: json.profiles?.instagram ?? null,
298
+ naverBlog: json.profiles?.naver_blog ?? null
299
+ }
300
+ };
301
+ }
302
+ async function fetchBusinessProfile(options) {
303
+ assertServer();
304
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
305
+ const params = new URLSearchParams();
306
+ if (options.siteId) params.set("site_id", options.siteId);
307
+ const qs = params.toString();
308
+ const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : ""}`;
309
+ const response = await fetch(url, {
310
+ method: "GET",
311
+ headers: authHeaders(options.apiKey),
312
+ signal: options.signal
313
+ });
314
+ if (response.status === 404) return null;
315
+ if (!response.ok) {
316
+ throw await toApiError(response);
317
+ }
318
+ const json = await response.json();
319
+ return businessProfileFromWire(json);
320
+ }
321
+
322
+ // src/server-discovery.ts
323
+ async function searchPosts(options) {
324
+ assertServer();
325
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
326
+ const params = new URLSearchParams();
327
+ params.set("q", options.query);
328
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
329
+ if (options.type) params.set("type", options.type);
330
+ if (options.siteId) params.set("site_id", options.siteId);
331
+ const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;
332
+ const response = await fetch(url, {
333
+ method: "GET",
334
+ headers: authHeaders(options.apiKey),
335
+ signal: options.signal
336
+ });
337
+ if (response.status === 404) return [];
338
+ if (!response.ok) {
339
+ throw await toApiError(response);
340
+ }
341
+ const json = await response.json();
342
+ return json.items.map((w) => ({
343
+ id: w.id,
344
+ type: w.type,
345
+ title: w.title,
346
+ slug: w.slug,
347
+ excerpt: w.excerpt,
348
+ featuredImageUrl: w.featured_media_url ?? null,
349
+ publishedAt: w.published_at
350
+ }));
351
+ }
352
+ function menuItemFromWire(w) {
353
+ const item = { id: w.id, label: w.label, url: w.url };
354
+ if (w.new_tab) item.newTab = true;
355
+ if (w.children && w.children.length > 0) {
356
+ item.children = w.children.map(menuItemFromWire);
357
+ }
358
+ return item;
359
+ }
360
+ function menuFromWire(w) {
361
+ return {
362
+ id: w.id,
363
+ name: w.name,
364
+ slug: w.slug,
365
+ items: (w.items ?? []).map(menuItemFromWire),
366
+ updatedAt: w.updated_at ?? null
367
+ };
368
+ }
369
+ async function fetchMenu(options) {
370
+ assertServer();
371
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
372
+ const params = new URLSearchParams();
373
+ if (options.siteId) params.set("site_id", options.siteId);
374
+ const qs = params.toString();
375
+ const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : ""}`;
376
+ const response = await fetch(url, {
377
+ method: "GET",
378
+ headers: authHeaders(options.apiKey),
379
+ signal: options.signal
380
+ });
381
+ if (response.status === 404) return null;
382
+ if (!response.ok) {
383
+ throw await toApiError(response);
384
+ }
385
+ const json = await response.json();
386
+ return menuFromWire(json);
387
+ }
388
+ async function fetchMenus(options) {
389
+ assertServer();
390
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
391
+ const params = new URLSearchParams();
392
+ if (options.siteId) params.set("site_id", options.siteId);
393
+ const qs = params.toString();
394
+ const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : ""}`;
395
+ const response = await fetch(url, {
396
+ method: "GET",
397
+ headers: authHeaders(options.apiKey),
398
+ signal: options.signal
399
+ });
400
+ if (response.status === 404) return [];
401
+ if (!response.ok) {
402
+ throw await toApiError(response);
403
+ }
404
+ const json = await response.json();
405
+ return (json.items ?? []).map(menuFromWire);
505
406
  }
506
- async function fetchBusinessProfile(options) {
507
- assertServer2();
508
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
407
+ async function fetchRedirects(options) {
408
+ assertServer();
409
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
509
410
  const params = new URLSearchParams();
510
411
  if (options.siteId) params.set("site_id", options.siteId);
511
412
  const qs = params.toString();
512
- const url = `${baseUrl}/v1/cms/public/business-profile${qs ? `?${qs}` : ""}`;
413
+ const url = `${baseUrl}/v1/cms/public/redirects${qs ? `?${qs}` : ""}`;
513
414
  const response = await fetch(url, {
514
415
  method: "GET",
515
416
  headers: authHeaders(options.apiKey),
516
417
  signal: options.signal
517
418
  });
518
- if (response.status === 404) return null;
419
+ if (response.status === 404) return [];
519
420
  if (!response.ok) {
520
421
  throw await toApiError(response);
521
422
  }
522
423
  const json = await response.json();
523
- return businessProfileFromWire(json);
424
+ return (json.items ?? []).map((w) => ({
425
+ id: w.id,
426
+ fromPath: w.from_path,
427
+ toTarget: w.to_target,
428
+ statusCode: w.status_code === 302 ? 302 : 301
429
+ }));
524
430
  }
525
- function resolvePostDisplay(post, settings, explicit) {
526
- const display = post.metaJson && typeof post.metaJson === "object" ? post.metaJson.display : void 0;
527
- const ov = (key) => display && typeof display[key] === "boolean" ? display[key] : void 0;
431
+
432
+ // src/seo.ts
433
+ function generateRssXml(input) {
434
+ const lang = input.language ?? "ko-KR";
435
+ const lastBuild = input.items.length > 0 ? toRfc822(input.items[0].pubDate) : "";
436
+ const lines = [];
437
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
438
+ lines.push(
439
+ '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">'
440
+ );
441
+ lines.push("<channel>");
442
+ lines.push(`<title>${escapeXml(input.title)}</title>`);
443
+ lines.push(`<link>${escapeXml(input.link)}</link>`);
444
+ lines.push(
445
+ `<atom:link href="${escapeXml(input.feedUrl)}" rel="self" type="application/rss+xml" />`
446
+ );
447
+ lines.push(
448
+ `<description>${escapeXml(input.description ?? input.title)}</description>`
449
+ );
450
+ lines.push(`<language>${escapeXml(lang)}</language>`);
451
+ if (lastBuild) lines.push(`<lastBuildDate>${lastBuild}</lastBuildDate>`);
452
+ for (const item of input.items) {
453
+ const guid = item.guid ?? item.link;
454
+ lines.push("<item>");
455
+ lines.push(`<title>${escapeXml(item.title)}</title>`);
456
+ lines.push(`<link>${escapeXml(item.link)}</link>`);
457
+ lines.push(`<guid isPermaLink="false">${escapeXml(guid)}</guid>`);
458
+ if (item.description) {
459
+ lines.push(`<description>${cdata(item.description)}</description>`);
460
+ }
461
+ const pub = toRfc822(item.pubDate);
462
+ if (pub) lines.push(`<pubDate>${pub}</pubDate>`);
463
+ lines.push("</item>");
464
+ }
465
+ lines.push("</channel>");
466
+ lines.push("</rss>");
467
+ return lines.join("\n");
468
+ }
469
+ function rssItemsFromPosts(posts, opts) {
470
+ return posts.map((p) => ({
471
+ title: p.title,
472
+ link: opts.postUrl(p),
473
+ guid: opts.postUrl(p),
474
+ description: p.excerpt,
475
+ pubDate: p.publishedAt
476
+ }));
477
+ }
478
+ function toRfc822(value) {
479
+ if (!value) return "";
480
+ const d = value instanceof Date ? value : new Date(value);
481
+ return Number.isNaN(d.getTime()) ? "" : d.toUTCString();
482
+ }
483
+ function articleSchema(input) {
484
+ const schema = {
485
+ "@context": "https://schema.org",
486
+ "@type": "Article",
487
+ headline: input.headline,
488
+ mainEntityOfPage: { "@type": "WebPage", "@id": input.url }
489
+ };
490
+ if (input.description) schema.description = input.description;
491
+ if (input.image) schema.image = input.image;
492
+ const pub = toIso(input.datePublished);
493
+ if (pub) schema.datePublished = pub;
494
+ const mod = toIso(input.dateModified);
495
+ if (mod) schema.dateModified = mod;
496
+ if (input.authorName)
497
+ schema.author = { "@type": "Person", name: input.authorName };
498
+ if (input.publisherName) {
499
+ schema.publisher = {
500
+ "@type": "Organization",
501
+ name: input.publisherName,
502
+ ...input.publisherLogo ? { logo: { "@type": "ImageObject", url: input.publisherLogo } } : {}
503
+ };
504
+ }
505
+ return schema;
506
+ }
507
+ function breadcrumbSchema(items) {
528
508
  return {
529
- showTableOfContents: explicit?.showTableOfContents ?? ov("toc") ?? settings.showTableOfContents,
530
- showAuthor: ov("author") ?? settings.showAuthor,
531
- showDate: ov("date") ?? settings.showDate,
532
- showAuthorCard: settings.showAuthorCard,
533
- tocTitle: explicit?.tableOfContentsTitle ?? settings.tocTitle ?? "\uBAA9\uCC28",
534
- authorCardEyebrow: settings.authorCardEyebrow ?? "\uC791\uC131\uC790",
535
- authorCardDescription: settings.authorCardDescription,
536
- authorProfileName: settings.authorProfileName,
537
- authorProfileBio: settings.authorProfileBio,
538
- authorProfileImageUrl: settings.authorProfileImageUrl,
539
- authorProfileImageRadius: settings.authorProfileImageRadius,
540
- authorProfileImagePositionX: settings.authorProfileImagePositionX,
541
- authorProfileImagePositionY: settings.authorProfileImagePositionY
509
+ "@context": "https://schema.org",
510
+ "@type": "BreadcrumbList",
511
+ itemListElement: items.map((it, i) => ({
512
+ "@type": "ListItem",
513
+ position: i + 1,
514
+ name: it.name,
515
+ item: it.url
516
+ }))
517
+ };
518
+ }
519
+ function organizationSchema(input) {
520
+ return {
521
+ "@context": "https://schema.org",
522
+ "@type": "Organization",
523
+ name: input.name,
524
+ url: input.url,
525
+ ...input.logo ? { logo: input.logo } : {},
526
+ ...input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}
527
+ };
528
+ }
529
+ var DAY_OF_WEEK = {
530
+ Mo: "Monday",
531
+ Tu: "Tuesday",
532
+ We: "Wednesday",
533
+ Th: "Thursday",
534
+ Fr: "Friday",
535
+ Sa: "Saturday",
536
+ Su: "Sunday"
537
+ };
538
+ function localBusinessSchema(profile, opts) {
539
+ const input = profile;
540
+ const types = Array.from(
541
+ /* @__PURE__ */ new Set([input.businessType || "LocalBusiness", "LocalBusiness"])
542
+ );
543
+ const schema = {
544
+ "@context": "https://schema.org",
545
+ "@type": types,
546
+ name: input.name,
547
+ url: opts.url
548
+ };
549
+ if (input.legalName) schema.legalName = input.legalName;
550
+ if (opts.image) schema.image = opts.image;
551
+ if (input.telephone) schema.telephone = input.telephone;
552
+ if (input.email) schema.email = input.email;
553
+ if (input.address) {
554
+ schema.address = {
555
+ "@type": "PostalAddress",
556
+ streetAddress: input.address.streetAddress,
557
+ addressLocality: input.address.addressLocality,
558
+ ...input.address.addressRegion ? { addressRegion: input.address.addressRegion } : {},
559
+ ...input.address.postalCode ? { postalCode: input.address.postalCode } : {},
560
+ addressCountry: "KR"
561
+ };
562
+ }
563
+ if (input.geo) {
564
+ schema.geo = {
565
+ "@type": "GeoCoordinates",
566
+ latitude: input.geo.latitude,
567
+ longitude: input.geo.longitude
568
+ };
569
+ }
570
+ const openingHours = (input.openingHours ?? []).filter(
571
+ (row) => row.days.length > 0 && row.opens && row.closes
572
+ );
573
+ if (openingHours.length > 0) {
574
+ schema.openingHoursSpecification = openingHours.map((row) => ({
575
+ "@type": "OpeningHoursSpecification",
576
+ dayOfWeek: row.days.map((d) => DAY_OF_WEEK[d] ?? d),
577
+ opens: row.opens,
578
+ closes: row.closes
579
+ }));
580
+ }
581
+ if (input.priceRange) schema.priceRange = input.priceRange;
582
+ if (input.areaServed && input.areaServed.length > 0) {
583
+ schema.areaServed = input.areaServed;
584
+ }
585
+ const sameAs = Object.values(input.profiles ?? {}).filter(
586
+ (u) => typeof u === "string" && u.length > 0
587
+ );
588
+ if (sameAs.length > 0) schema.sameAs = sameAs;
589
+ return schema;
590
+ }
591
+ function websiteSchema(input) {
592
+ return {
593
+ "@context": "https://schema.org",
594
+ "@type": "WebSite",
595
+ name: input.name,
596
+ url: input.url,
597
+ ...input.searchUrl ? {
598
+ potentialAction: {
599
+ "@type": "SearchAction",
600
+ target: `${input.searchUrl}{search_term_string}`,
601
+ "query-input": "required name=search_term_string"
602
+ }
603
+ } : {}
604
+ };
605
+ }
606
+ function faqSchema(qa) {
607
+ return {
608
+ "@context": "https://schema.org",
609
+ "@type": "FAQPage",
610
+ mainEntity: qa.map((x) => ({
611
+ "@type": "Question",
612
+ name: x.question,
613
+ acceptedAnswer: { "@type": "Answer", text: x.answer }
614
+ }))
542
615
  };
543
616
  }
544
- var CmsApiError = class extends Error {
545
- status;
546
- code;
547
- constructor(message, status, code) {
548
- super(message);
549
- this.name = "CmsApiError";
550
- this.status = status;
551
- this.code = code;
617
+ function jsonLdScript(schema) {
618
+ const json = JSON.stringify(schema).replace(/</g, "\\u003c");
619
+ return `<script type="application/ld+json">${json}</script>`;
620
+ }
621
+ function toIso(value) {
622
+ if (!value) return null;
623
+ const d = value instanceof Date ? value : new Date(value);
624
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
625
+ }
626
+ function escapeXml(value) {
627
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
628
+ }
629
+ function cdata(value) {
630
+ return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
631
+ }
632
+
633
+ // src/inquiry.ts
634
+ var DEFAULT_BASE_URL2 = "https://api.roottale.com";
635
+ var INQUIRY_ERROR_MESSAGES = {
636
+ consent_privacy: "\uAC1C\uC778\uC815\uBCF4 \uC218\uC9D1\xB7\uC774\uC6A9 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
637
+ consent_overseas: "\uC758\uB8CC \uBD84\uC57C\uB294 \uAD6D\uC678\uC774\uC804 \uB3D9\uC758\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
638
+ invalid_vertical: "\uC5C5\uC885 \uBD84\uB958\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
639
+ missing_fields: "\uD544\uC218 \uD56D\uBAA9\uC774 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
640
+ invalid_email: "\uC774\uBA54\uC77C \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
641
+ invalid_api_key: "\uC5F0\uB3D9 \uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
642
+ internal: "\uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
643
+ };
644
+ var RESERVED_INQUIRY_KEYS = /* @__PURE__ */ new Set([
645
+ "vertical",
646
+ "contact_name",
647
+ "business_name",
648
+ "email",
649
+ "phone",
650
+ "message",
651
+ "consultation_field",
652
+ "current_site_url",
653
+ "privacy_consent",
654
+ "overseas_transfer_consent",
655
+ "lead_kind"
656
+ ]);
657
+ function inquiryErrorMessage(code) {
658
+ return code && INQUIRY_ERROR_MESSAGES[code] || INQUIRY_ERROR_MESSAGES.internal;
659
+ }
660
+ function parseInquiryRedirect(location, base = DEFAULT_BASE_URL2) {
661
+ if (!location) {
662
+ return { ok: false, code: null, message: inquiryErrorMessage(null) };
552
663
  }
553
- };
554
- async function fetchPosts(options) {
555
- assertServer2();
556
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
557
- const params = new URLSearchParams();
558
- if (options.limit !== void 0) params.set("limit", String(options.limit));
559
- if (options.cursor) params.set("cursor", options.cursor);
560
- if (options.type) params.set("type", options.type);
561
- if (options.siteId) params.set("site_id", options.siteId);
562
- const qs = params.toString();
563
- const url = `${baseUrl}/v1/cms/public/posts${qs ? `?${qs}` : ""}`;
564
- const response = await fetch(url, {
565
- method: "GET",
566
- headers: authHeaders(options.apiKey),
567
- signal: options.signal
568
- });
569
- if (!response.ok) {
570
- throw await toApiError(response);
664
+ let url;
665
+ try {
666
+ url = new URL(location, base);
667
+ } catch {
668
+ return { ok: false, code: null, message: inquiryErrorMessage(null) };
571
669
  }
572
- const json = await response.json();
573
- return {
574
- items: json.items.map(fromWire),
575
- hasMore: json.has_more,
576
- nextCursor: json.next_cursor
577
- };
670
+ if (url.searchParams.get("ok") === "1") return { ok: true };
671
+ const code = url.searchParams.get("err");
672
+ return { ok: false, code, message: inquiryErrorMessage(code) };
578
673
  }
579
- async function fetchPost(options) {
580
- assertServer2();
581
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
582
- const params = new URLSearchParams();
583
- if (options.siteId) params.set("site_id", options.siteId);
584
- const qs = params.toString();
585
- const url = `${baseUrl}/v1/cms/public/posts/${encodeURIComponent(options.slugOrId)}${qs ? `?${qs}` : ""}`;
586
- const response = await fetch(url, {
587
- method: "GET",
588
- headers: authHeaders(options.apiKey),
589
- signal: options.signal
590
- });
591
- if (response.status === 404) return null;
592
- if (!response.ok) {
593
- throw await toApiError(response);
674
+ function formatKoreanPhoneNumber(input) {
675
+ let digits = input.replace(/\D/g, "");
676
+ if (digits.startsWith("82") && digits.length >= 11) {
677
+ digits = `0${digits.slice(2)}`;
594
678
  }
595
- const json = await response.json();
596
- return fromWire(json);
597
- }
598
- async function searchPosts(options) {
599
- assertServer2();
600
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
601
- const params = new URLSearchParams();
602
- params.set("q", options.query);
603
- if (options.limit !== void 0) params.set("limit", String(options.limit));
604
- if (options.type) params.set("type", options.type);
605
- if (options.siteId) params.set("site_id", options.siteId);
606
- const url = `${baseUrl}/v1/cms/public/search?${params.toString()}`;
607
- const response = await fetch(url, {
608
- method: "GET",
609
- headers: authHeaders(options.apiKey),
610
- signal: options.signal
611
- });
612
- if (response.status === 404) return [];
613
- if (!response.ok) {
614
- throw await toApiError(response);
679
+ if (digits.length === 11) {
680
+ return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
615
681
  }
616
- const json = await response.json();
617
- return json.items.map((w) => ({
618
- id: w.id,
619
- type: w.type,
620
- title: w.title,
621
- slug: w.slug,
622
- excerpt: w.excerpt,
623
- featuredImageUrl: w.featured_media_url ?? null,
624
- publishedAt: w.published_at
625
- }));
626
- }
627
- function menuItemFromWire(w) {
628
- const item = { id: w.id, label: w.label, url: w.url };
629
- if (w.new_tab) item.newTab = true;
630
- if (w.children && w.children.length > 0) {
631
- item.children = w.children.map(menuItemFromWire);
682
+ if (digits.length === 10) {
683
+ if (digits.startsWith("02")) {
684
+ return `${digits.slice(0, 2)}-${digits.slice(2, 6)}-${digits.slice(6)}`;
685
+ }
686
+ return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
632
687
  }
633
- return item;
634
- }
635
- function menuFromWire(w) {
636
- return {
637
- id: w.id,
638
- name: w.name,
639
- slug: w.slug,
640
- items: (w.items ?? []).map(menuItemFromWire),
641
- updatedAt: w.updated_at ?? null
642
- };
643
- }
644
- async function fetchMenu(options) {
645
- assertServer2();
646
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
647
- const params = new URLSearchParams();
648
- if (options.siteId) params.set("site_id", options.siteId);
649
- const qs = params.toString();
650
- const url = `${baseUrl}/v1/cms/public/menus/${encodeURIComponent(options.slug)}${qs ? `?${qs}` : ""}`;
651
- const response = await fetch(url, {
652
- method: "GET",
653
- headers: authHeaders(options.apiKey),
654
- signal: options.signal
655
- });
656
- if (response.status === 404) return null;
657
- if (!response.ok) {
658
- throw await toApiError(response);
688
+ if (digits.length === 8) {
689
+ return `${digits.slice(0, 4)}-${digits.slice(4)}`;
659
690
  }
660
- const json = await response.json();
661
- return menuFromWire(json);
691
+ return input.trim();
662
692
  }
663
- async function fetchMenus(options) {
693
+ async function submitInquiry(options) {
664
694
  assertServer2();
665
695
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
666
- const params = new URLSearchParams();
667
- if (options.siteId) params.set("site_id", options.siteId);
668
- const qs = params.toString();
669
- const url = `${baseUrl}/v1/cms/public/menus${qs ? `?${qs}` : ""}`;
670
- const response = await fetch(url, {
671
- method: "GET",
672
- headers: authHeaders(options.apiKey),
673
- signal: options.signal
674
- });
675
- if (response.status === 404) return [];
676
- if (!response.ok) {
677
- throw await toApiError(response);
696
+ const f = options.fields;
697
+ const body = new FormData();
698
+ body.set("vertical", f.vertical);
699
+ body.set("contact_name", f.contactName);
700
+ body.set("business_name", f.businessName);
701
+ body.set("email", f.email);
702
+ body.set("phone", formatKoreanPhoneNumber(f.phone));
703
+ if (f.message) body.set("message", f.message);
704
+ if (f.consultationField) body.set("consultation_field", f.consultationField);
705
+ if (f.currentSiteUrl) body.set("current_site_url", f.currentSiteUrl);
706
+ if (f.privacyConsent) body.set("privacy_consent", "on");
707
+ if (f.overseasTransferConsent) body.set("overseas_transfer_consent", "on");
708
+ if (f.leadKind) body.set("lead_kind", f.leadKind);
709
+ if (f.attribution) {
710
+ for (const [key, value] of Object.entries(f.attribution)) {
711
+ if (typeof value !== "string" || !value.trim()) continue;
712
+ body.set(`attr_${key}`, value.trim());
713
+ }
678
714
  }
679
- const json = await response.json();
680
- return (json.items ?? []).map(menuFromWire);
681
- }
682
- function fromWire(wire) {
683
- return {
684
- id: wire.id,
685
- tenantId: wire.tenant_id,
686
- siteId: wire.site_id,
687
- type: wire.type,
688
- slug: wire.slug,
689
- title: wire.title,
690
- excerpt: wire.excerpt,
691
- featuredMediaId: wire.featured_media_id,
692
- featuredImageUrl: wire.featured_media_url ?? null,
693
- authorId: wire.author_id,
694
- authorName: wire.author_name ?? null,
695
- authorImageUrl: wire.author_image_url ?? null,
696
- authorBio: wire.author_bio ?? null,
697
- bodyJson: wire.body_json,
698
- status: wire.status,
699
- metaJson: wire.meta_json,
700
- fields: wire.fields ?? null,
701
- terms: wire.terms ?? [],
702
- publishedAt: wire.published_at,
703
- createdAt: wire.created_at,
704
- updatedAt: wire.updated_at
705
- };
706
- }
707
- async function toApiError(response) {
708
- let code = "request_failed";
709
- let message = `RootTale CMS API error: ${response.status}`;
715
+ if (f.extras) {
716
+ for (const [rawKey, rawValue] of Object.entries(f.extras)) {
717
+ const key = rawKey.trim();
718
+ const value = String(rawValue ?? "").trim();
719
+ if (!key || !value) continue;
720
+ if (key.startsWith("_") || RESERVED_INQUIRY_KEYS.has(key)) continue;
721
+ if (key.startsWith("attr_")) continue;
722
+ if (body.has(key)) continue;
723
+ body.set(key, value);
724
+ }
725
+ }
726
+ let response;
710
727
  try {
711
- const body = await response.json();
712
- if (body.code) code = body.code;
713
- if (body.message) message = body.message;
728
+ response = await fetch(`${baseUrl}/v1/public/inquiries`, {
729
+ method: "POST",
730
+ headers: { authorization: `Bearer ${options.apiKey}` },
731
+ body,
732
+ // intake 는 302 로 결과 신호 — 자동 추적 금지하고 Location 직접 읽음.
733
+ redirect: "manual",
734
+ signal: options.signal
735
+ });
714
736
  } catch {
737
+ return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
715
738
  }
716
- return new CmsApiError(message, response.status, code);
739
+ if (response.status === 401) {
740
+ return {
741
+ ok: false,
742
+ code: "invalid_api_key",
743
+ message: inquiryErrorMessage("invalid_api_key")
744
+ };
745
+ }
746
+ const location = response.headers.get("location");
747
+ if (location) return parseInquiryRedirect(location, baseUrl);
748
+ if (response.ok) return { ok: true };
749
+ return { ok: false, code: "internal", message: inquiryErrorMessage("internal") };
717
750
  }
718
751
  function assertServer2() {
719
752
  if (typeof window !== "undefined") {
720
753
  throw new Error(
721
- "@roottale/cms-client/server must not run in the browser. Move this call to a Server Component, getServerSideProps, or a Route Handler. Shipping API keys to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
754
+ "@roottale/cms-client submitInquiry must not run in the browser. Call it from a Server Action / Route Handler. Shipping the API key to the browser breaks the security model (ADR-0023 \xA75.1 #15)."
722
755
  );
723
756
  }
724
757
  }
@@ -740,6 +773,7 @@ export {
740
773
  fetchMenus,
741
774
  fetchPost,
742
775
  fetchPosts,
776
+ fetchRedirects,
743
777
  fetchTheme,
744
778
  formatKoreanPhoneNumber,
745
779
  generateRssXml,