@roottale/cms-client 0.63.0 → 0.64.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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # @roottale/cms-client
2
2
 
3
+ ## 0.64.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5ccf503: 한 사이트의 여러 문의 폼을 발행 버전별로 연결할 수 있도록 공통 폼 정의·검증 계약과 `fetchInquiryForm`·`submitFormInquiry` 서버 SDK를 추가합니다. 구조화 답변·동의·중복 재시도·접수증과 같은 사이트의 견적/AS 폼 연동 예제를 제공합니다. 기존 문의 SDK는 유지합니다.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [5ccf503]
12
+ - @roottale/inquiry-forms@0.2.0
13
+
3
14
  ## 0.63.0
4
15
 
5
16
  ## 0.62.0
package/dist/server.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { InquiryFormDefinition, InquiryAnswer } from '@roottale/inquiry-forms';
2
+ export { InquiryAnswer, InquiryFormDefinition, InquiryFormField, InquiryKind } from '@roottale/inquiry-forms';
1
3
  import { InquiryAttribution } from './attribution.js';
2
4
  export { parseAttributionJson } from './attribution.js';
3
5
 
@@ -1162,6 +1164,71 @@ declare function fetchSiteInquiries(options: FetchSiteInquiriesOptions): Promise
1162
1164
  declare function fetchSiteInquiry(options: FetchSiteInquiryOptions): Promise<SiteInquiryDetail | null>;
1163
1165
  declare function submitSiteInquiry(options: SubmitSiteInquiryOptions): Promise<SubmitSiteInquiryResult>;
1164
1166
 
1167
+ /** A published form belongs to exactly one site. Versions are immutable. */
1168
+ interface PublishedInquiryForm {
1169
+ formId: string;
1170
+ siteId: string;
1171
+ formKey: string;
1172
+ version: number;
1173
+ definition: InquiryFormDefinition;
1174
+ }
1175
+ interface FetchInquiryFormOptions {
1176
+ apiKey: string;
1177
+ baseUrl?: string;
1178
+ /** Required for a tenant-wide key; a site-bound key already identifies its site. */
1179
+ siteId?: string;
1180
+ formId: string;
1181
+ signal?: AbortSignal;
1182
+ }
1183
+ interface SubmitFormInquiryFields {
1184
+ siteId?: string;
1185
+ /** Version that the visitor saw, including its consent notice. */
1186
+ version: number;
1187
+ answers: Record<string, InquiryAnswer>;
1188
+ privacyConsent: boolean;
1189
+ /** Caller-generated key. Reuse the same key and payload after an uncertain response. */
1190
+ idempotencyKey: string;
1191
+ placement?: string;
1192
+ turnstileToken?: string;
1193
+ attribution?: InquiryAttribution | null;
1194
+ /** readJourney() array; the API validates entries and enforces the site's privacy mode. */
1195
+ journey?: readonly unknown[];
1196
+ }
1197
+ interface SubmitFormInquiryOptions {
1198
+ apiKey: string;
1199
+ baseUrl?: string;
1200
+ formId: string;
1201
+ fields: SubmitFormInquiryFields;
1202
+ signal?: AbortSignal;
1203
+ }
1204
+ interface FormInquiryReceipt {
1205
+ id: string;
1206
+ inquiryNo: number;
1207
+ receivedAt: string;
1208
+ /** True when this is the receipt of an already saved submission. */
1209
+ replayed: boolean;
1210
+ }
1211
+ type SubmitFormInquiryResult = {
1212
+ ok: true;
1213
+ receipt: FormInquiryReceipt;
1214
+ } | {
1215
+ ok: false;
1216
+ /** Transport/invalid_response can mean the server saved the inquiry before the response was lost. */
1217
+ kind: "api" | "transport" | "invalid_response";
1218
+ code: string;
1219
+ status: number | null;
1220
+ message: string;
1221
+ fieldErrors: Record<string, string[]>;
1222
+ formErrors: string[];
1223
+ };
1224
+ /** Server-only. Always reads the current published form; 404 returns null. */
1225
+ declare function fetchInquiryForm(options: FetchInquiryFormOptions): Promise<PublishedInquiryForm | null>;
1226
+ /**
1227
+ * Server-only JSON intake. A successful HTTP status is insufficient without a valid receipt.
1228
+ * Does not retry or generate keys; the caller keeps its key until the receipt is confirmed.
1229
+ */
1230
+ declare function submitFormInquiry(options: SubmitFormInquiryOptions): Promise<SubmitFormInquiryResult>;
1231
+
1165
1232
  /**
1166
1233
  * 글 미리보기 — 어드민 편집기가 발급한 토큰으로 초안을 **발행 글과 같은 모양**으로
1167
1234
  * 받아 온다. 고객 사이트는 글 상세 템플릿을 그대로 재사용해 미리보기를 그리면
@@ -1564,4 +1631,4 @@ declare function formatKoreanPhoneNumber(input: string): string;
1564
1631
  */
1565
1632
  declare function submitInquiry(options: SubmitInquiryOptions): Promise<SubmitInquiryResult>;
1566
1633
 
1567
- export { type ActionDockAction, type ActionDockKind, type ActionDockPosition, type ActionDockSettings, type AnalyticsTagConfig, type AnalyticsTagProvider, type AnnouncementItem, type AnnouncementSettings, type AnnouncementTone, type AnnouncementVariant, type ArticleSchemaInput, BLOG_SETTINGS_CACHE_TAG, BUSINESS_CACHE_TAG, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, COLLECTIONS_CACHE_TAG, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsCategoryCount, type CmsCategoryCounts, type CmsContentModel, type CmsExposureCampaign, type CmsExposureDecision, type CmsFieldMetaEntry, type CmsPostContent, type CmsPostContentPage, type CmsPostPreviewContent, type CmsPostPreviewInfo, type CmsPostTranslationRef, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsSitePattern, type CmsSitePatternPresentation, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, DEFAULT_SITE_PATTERN_PRESENTATION, EXPOSURE_CACHE_TAG, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCategoryCountsOptions, type FetchCollectionsOptions, type FetchContentModelsOptions, type FetchExposureDecisionOptions, type FetchExposureOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostPreviewOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchSiteInquiriesOptions, type FetchSiteInquiryOptions, type FetchSitePatternsOptions, type FetchThemeOptions, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryBoardSettings, type InquiryVertical, LOCAL_BUSINESS_MAX_SERVICES, LOCAL_BUSINESS_SERVICE_MAX_LENGTH, type LocalBusinessProfileInput, MENUS_CACHE_TAG, POST_FOOTER_PATTERN_SLOT, type ResolvedPostDisplay, type RootTaleAuthor, type RootTaleBlogPostCta, type RootTaleBlogSettings, type RootTaleBusinessProfile, type RootTaleMenu, type RootTaleMenuItem, type RootTaleRedirect, type RootTaleSiteProfile, type RootTaleSitemapSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, SETTINGS_CACHE_TAGS, SITE_PATTERNS_CACHE_TAG, type SearchPostsOptions, type SiteAnalyticsTags, type SiteInquiryDetail, type SiteInquiryListItem, type SiteInquiryListPage, type SiteInquiryStatus, type SiteNavColumn, type SiteNavCta, type SiteNavGroup, type SiteNavLink, type SiteNavSettings, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, type SubmitSiteInquiryFields, type SubmitSiteInquiryOptions, type SubmitSiteInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, collectionPageSchema, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCategoryCounts, fetchCollections, fetchCollectionsSafeDefault, fetchContentModels, fetchExposure, fetchExposureDecision, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPostPreview, fetchPosts, fetchRedirects, fetchSiteInquiries, fetchSiteInquiry, fetchSitePatterns, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, isPreviewExpiredError, jsonLdJson, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, profilePageSchema, resolvePostDisplay, resolveSearchHitPath, rssItemsFromPosts, searchPosts, selectRelatedPosts, selectSitePatternForSlot, sitePatternPresentationAttributes, submitInquiry, submitSiteInquiry, websiteSchema };
1634
+ export { type ActionDockAction, type ActionDockKind, type ActionDockPosition, type ActionDockSettings, type AnalyticsTagConfig, type AnalyticsTagProvider, type AnnouncementItem, type AnnouncementSettings, type AnnouncementTone, type AnnouncementVariant, type ArticleSchemaInput, BLOG_SETTINGS_CACHE_TAG, BUSINESS_CACHE_TAG, type BusinessDayCode, type BusinessOpeningHours, type BusinessType, CMS_CLIENT_VERSION, COLLECTIONS_CACHE_TAG, CmsApiError, type CmsArchiveRelatedGroup, type CmsArchiveRelatedPost, type CmsCategoryCount, type CmsCategoryCounts, type CmsContentModel, type CmsExposureCampaign, type CmsExposureDecision, type CmsFieldMetaEntry, type CmsPostContent, type CmsPostContentPage, type CmsPostPreviewContent, type CmsPostPreviewInfo, type CmsPostTranslationRef, type CmsPostType, type CmsRouteCollection, type CmsSearchHit, type CmsSitePattern, type CmsSitePatternPresentation, type CmsTaxonomyKind, type CmsTermRef, DEFAULT_BLOG_SETTINGS, DEFAULT_SITE_PATTERN_PRESENTATION, EXPOSURE_CACHE_TAG, type FetchAnalyticsOptions, type FetchAuthorsOptions, type FetchBlogSettingsOptions, type FetchBusinessProfileOptions, type FetchCategoryCountsOptions, type FetchCollectionsOptions, type FetchContentModelsOptions, type FetchExposureDecisionOptions, type FetchExposureOptions, type FetchInquiryFormOptions, type FetchMenuOptions, type FetchMenusOptions, type FetchPageBySlugOptions, type FetchPostOptions, type FetchPostPreviewOptions, type FetchPostsOptions, type FetchRedirectsOptions, type FetchSiteInquiriesOptions, type FetchSiteInquiryOptions, type FetchSitePatternsOptions, type FetchThemeOptions, type FormInquiryReceipt, INQUIRY_ERROR_MESSAGES, InquiryAttribution, type InquiryBoardSettings, type InquiryVertical, LOCAL_BUSINESS_MAX_SERVICES, LOCAL_BUSINESS_SERVICE_MAX_LENGTH, type LocalBusinessProfileInput, MENUS_CACHE_TAG, POST_FOOTER_PATTERN_SLOT, type PublishedInquiryForm, type ResolvedPostDisplay, type RootTaleAuthor, type RootTaleBlogPostCta, type RootTaleBlogSettings, type RootTaleBusinessProfile, type RootTaleMenu, type RootTaleMenuItem, type RootTaleRedirect, type RootTaleSiteProfile, type RootTaleSitemapSettings, type RootTaleTheme, type RssFeedInput, type RssItemInput, SETTINGS_CACHE_TAGS, SITE_PATTERNS_CACHE_TAG, type SearchPostsOptions, type SiteAnalyticsTags, type SiteInquiryDetail, type SiteInquiryListItem, type SiteInquiryListPage, type SiteInquiryStatus, type SiteNavColumn, type SiteNavCta, type SiteNavGroup, type SiteNavLink, type SiteNavSettings, type SubmitFormInquiryFields, type SubmitFormInquiryOptions, type SubmitFormInquiryResult, type SubmitInquiryFields, type SubmitInquiryOptions, type SubmitInquiryResult, type SubmitSiteInquiryFields, type SubmitSiteInquiryOptions, type SubmitSiteInquiryResult, THEME_CACHE_TAG, articleSchema, breadcrumbSchema, businessProfileFromWire, collectionPageSchema, contentIdMeta, faqSchema, fetchAnalyticsConfig, fetchAuthors, fetchBlogSettings, fetchBusinessProfile, fetchCategoryCounts, fetchCollections, fetchCollectionsSafeDefault, fetchContentModels, fetchExposure, fetchExposureDecision, fetchInquiryForm, fetchMenu, fetchMenus, fetchPageBySlug, fetchPost, fetchPostPreview, fetchPosts, fetchRedirects, fetchSiteInquiries, fetchSiteInquiry, fetchSitePatterns, fetchTheme, formatKoreanPhoneNumber, generateRssXml, inquiryErrorMessage, isPreviewExpiredError, jsonLdJson, jsonLdScript, localBusinessSchema, organizationSchema, parseInquiryRedirect, profilePageSchema, resolvePostDisplay, resolveSearchHitPath, rssItemsFromPosts, searchPosts, selectRelatedPosts, selectSitePatternForSlot, sitePatternPresentationAttributes, submitFormInquiry, submitInquiry, submitSiteInquiry, websiteSchema };
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
 
5
5
  // src/server-base.ts
6
6
  var DEFAULT_BASE_URL = "https://api.roottale.com";
7
- var CLIENT_VERSION = true ? "0.63.0" : "dev";
7
+ var CLIENT_VERSION = true ? "0.64.0" : "dev";
8
8
  var CMS_CLIENT_VERSION = CLIENT_VERSION;
9
9
  function authHeaders(apiKey) {
10
10
  return {
@@ -953,6 +953,137 @@ function fromDetailWire(item) {
953
953
  };
954
954
  }
955
955
 
956
+ // src/server-inquiry-forms.ts
957
+ import { inquiryFormDefinitionSchema } from "@roottale/inquiry-forms";
958
+ async function fetchInquiryForm(options) {
959
+ assertServer();
960
+ const params = new URLSearchParams();
961
+ if (options.siteId) params.set("site_id", options.siteId);
962
+ const query = params.toString();
963
+ const response = await fetch(`${formUrl(options)}${query ? `?${query}` : ""}`, {
964
+ headers: authHeaders(options.apiKey),
965
+ cache: "no-store",
966
+ redirect: "manual",
967
+ signal: options.signal
968
+ });
969
+ if (response.status === 404) return null;
970
+ if (!response.ok) throw await toApiError(response);
971
+ const body = await readJson(response);
972
+ if (!isRecord(body) || body.form_id !== options.formId || !isId(body.site_id) || options.siteId !== void 0 && body.site_id !== options.siteId || !isNonEmptyString(body.form_key) || !isPositiveInteger(body.version)) {
973
+ throw invalidFormResponse(response.status);
974
+ }
975
+ const definition = inquiryFormDefinitionSchema.safeParse(body.definition);
976
+ if (!definition.success) throw invalidFormResponse(response.status);
977
+ return {
978
+ formId: options.formId,
979
+ siteId: body.site_id,
980
+ formKey: body.form_key,
981
+ version: body.version,
982
+ definition: definition.data
983
+ };
984
+ }
985
+ async function submitFormInquiry(options) {
986
+ assertServer();
987
+ const fields = options.fields;
988
+ let response;
989
+ try {
990
+ response = await fetch(`${formUrl(options)}/submissions`, {
991
+ method: "POST",
992
+ headers: { ...authHeaders(options.apiKey), "content-type": "application/json" },
993
+ body: JSON.stringify({
994
+ site_id: fields.siteId,
995
+ version: fields.version,
996
+ answers: fields.answers,
997
+ privacy_consent: fields.privacyConsent,
998
+ idempotency_key: fields.idempotencyKey,
999
+ placement: fields.placement,
1000
+ turnstile_token: fields.turnstileToken,
1001
+ attribution: fields.attribution ?? void 0,
1002
+ journey: fields.journey
1003
+ }),
1004
+ cache: "no-store",
1005
+ redirect: "manual",
1006
+ signal: options.signal
1007
+ });
1008
+ } catch {
1009
+ return failure(
1010
+ "transport",
1011
+ "network_error",
1012
+ null,
1013
+ "\uC811\uC218 \uACB0\uACFC\uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uAC19\uC740 \uB0B4\uC6A9\uC73C\uB85C \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
1014
+ );
1015
+ }
1016
+ const body = await readJson(response);
1017
+ if (!response.ok) {
1018
+ const code = isRecord(body) && isNonEmptyString(body.code) ? body.code : codeForStatus(response.status);
1019
+ const message = isRecord(body) && isNonEmptyString(body.message) ? body.message : "\uBB38\uC758 \uC804\uC1A1\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC785\uB825 \uB0B4\uC6A9\uACFC \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.";
1020
+ const details = isRecord(body) && isRecord(body.details) ? body.details : void 0;
1021
+ return {
1022
+ ...failure("api", code, response.status, message),
1023
+ fieldErrors: parseFieldErrors(details?.field_errors),
1024
+ formErrors: stringArray(details?.form_errors)
1025
+ };
1026
+ }
1027
+ if (!isRecord(body) || !isId(body.id) || !isPositiveInteger(body.inquiry_no) || !isTimestamp(body.received_at) || typeof body.replayed !== "boolean") {
1028
+ return failure(
1029
+ "invalid_response",
1030
+ "invalid_response",
1031
+ response.status,
1032
+ "\uC811\uC218\uC99D\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uAC19\uC740 \uB0B4\uC6A9\uC73C\uB85C \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
1033
+ );
1034
+ }
1035
+ return {
1036
+ ok: true,
1037
+ receipt: { id: body.id, inquiryNo: body.inquiry_no, receivedAt: body.received_at, replayed: body.replayed }
1038
+ };
1039
+ }
1040
+ function formUrl(options) {
1041
+ const base = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1042
+ return `${base}/v1/public/inquiry-forms/${encodeURIComponent(options.formId)}`;
1043
+ }
1044
+ function failure(kind, code, status, message) {
1045
+ return { ok: false, kind, code, status, message, fieldErrors: {}, formErrors: [] };
1046
+ }
1047
+ function invalidFormResponse(status) {
1048
+ return new CmsApiError("RootTale inquiry form response is invalid", status, "invalid_response");
1049
+ }
1050
+ async function readJson(response) {
1051
+ try {
1052
+ return await response.json();
1053
+ } catch {
1054
+ return null;
1055
+ }
1056
+ }
1057
+ function isRecord(value) {
1058
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1059
+ }
1060
+ function isNonEmptyString(value) {
1061
+ return typeof value === "string" && value.trim().length > 0;
1062
+ }
1063
+ function isId(value) {
1064
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1065
+ }
1066
+ function isPositiveInteger(value) {
1067
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1068
+ }
1069
+ function isTimestamp(value) {
1070
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
1071
+ }
1072
+ function stringArray(value) {
1073
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1074
+ }
1075
+ function parseFieldErrors(value) {
1076
+ if (!isRecord(value)) return {};
1077
+ return Object.fromEntries(Object.entries(value).map(([key, errors]) => [key, stringArray(errors)]));
1078
+ }
1079
+ function codeForStatus(status) {
1080
+ if (status === 401) return "invalid_api_key";
1081
+ if (status === 403) return "insufficient_scope";
1082
+ if (status === 404) return "not_found";
1083
+ if (status === 429) return "rate_limited";
1084
+ return "request_failed";
1085
+ }
1086
+
956
1087
  // src/server-preview.ts
957
1088
  function isPreviewExpiredError(error) {
958
1089
  return error instanceof CmsApiError && (error.code === "preview_expired" || error.status === 410);
@@ -1523,6 +1654,7 @@ export {
1523
1654
  fetchContentModels,
1524
1655
  fetchExposure,
1525
1656
  fetchExposureDecision,
1657
+ fetchInquiryForm,
1526
1658
  fetchMenu,
1527
1659
  fetchMenus,
1528
1660
  fetchPageBySlug,
@@ -1552,6 +1684,7 @@ export {
1552
1684
  selectRelatedPosts,
1553
1685
  selectSitePatternForSlot,
1554
1686
  sitePatternPresentationAttributes,
1687
+ submitFormInquiry,
1555
1688
  submitInquiry,
1556
1689
  submitSiteInquiry,
1557
1690
  websiteSchema