@roottale/cms-renderer-next 0.41.1 → 0.42.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/before-after-client.d.ts +15 -0
  3. package/dist/before-after-client.js +8 -0
  4. package/dist/before-after-client.js.map +1 -0
  5. package/dist/chunk-4LQPZDXZ.js +75 -0
  6. package/dist/chunk-4LQPZDXZ.js.map +1 -0
  7. package/dist/chunk-6GQCFFHU.js +540 -0
  8. package/dist/chunk-6GQCFFHU.js.map +1 -0
  9. package/dist/chunk-F3G6RVIR.js +220 -0
  10. package/dist/chunk-F3G6RVIR.js.map +1 -0
  11. package/dist/chunk-H46JUIHZ.js +79 -0
  12. package/dist/chunk-H46JUIHZ.js.map +1 -0
  13. package/dist/chunk-K62RR4UQ.js +53 -0
  14. package/dist/chunk-K62RR4UQ.js.map +1 -0
  15. package/dist/chunk-NUUTTSGT.js +916 -0
  16. package/dist/chunk-NUUTTSGT.js.map +1 -0
  17. package/dist/chunk-RSNUZ4BF.js +3378 -0
  18. package/dist/chunk-RSNUZ4BF.js.map +1 -0
  19. package/dist/chunk-U5AKACPN.js +113 -0
  20. package/dist/chunk-U5AKACPN.js.map +1 -0
  21. package/dist/chunk-WWS43XCT.js +206 -0
  22. package/dist/chunk-WWS43XCT.js.map +1 -0
  23. package/dist/cms-public.css +164 -51
  24. package/dist/concerns-client.d.ts +12 -0
  25. package/dist/concerns-client.js +8 -0
  26. package/dist/concerns-client.js.map +1 -0
  27. package/dist/expertise-client.d.ts +10 -0
  28. package/dist/expertise-client.js +8 -0
  29. package/dist/expertise-client.js.map +1 -0
  30. package/dist/hero-carousel-client.d.ts +36 -0
  31. package/dist/hero-carousel-client.js +8 -0
  32. package/dist/hero-carousel-client.js.map +1 -0
  33. package/dist/routes.d.ts +51 -5
  34. package/dist/routes.js +109 -62
  35. package/dist/routes.js.map +1 -1
  36. package/dist/seo-checks.d.ts +87 -0
  37. package/dist/seo-checks.js +27 -0
  38. package/dist/seo-checks.js.map +1 -0
  39. package/dist/server.d.ts +51 -2
  40. package/dist/server.js +8 -2
  41. package/dist/site-config-DgYGGwu1.d.ts +3910 -0
  42. package/dist/spaces-client.d.ts +10 -0
  43. package/dist/spaces-client.js +8 -0
  44. package/dist/spaces-client.js.map +1 -0
  45. package/dist/templates.css +3246 -194
  46. package/dist/templates.d.ts +20 -1028
  47. package/dist/templates.js +70 -2
  48. package/dist/templates.js.map +1 -1
  49. package/dist/{theme-css-vars-BiyHBsJG.d.ts → theme-css-vars-CD3jcEkp.d.ts} +2 -2
  50. package/dist/ui-react-motion-client.d.ts +73 -0
  51. package/dist/ui-react-motion-client.js +14 -0
  52. package/dist/ui-react-motion-client.js.map +1 -0
  53. package/dist/validate.d.ts +15 -1
  54. package/dist/validate.js +806 -17
  55. package/dist/validate.js.map +1 -1
  56. package/package.json +18 -6
  57. package/dist/chunk-GOHP7RWD.js +0 -1647
  58. package/dist/chunk-GOHP7RWD.js.map +0 -1
  59. package/dist/chunk-IOYBEH6E.js +0 -271
  60. package/dist/chunk-IOYBEH6E.js.map +0 -1
@@ -0,0 +1,916 @@
1
+ // src/templates/site-config.ts
2
+ import { z } from "zod";
3
+ var JsonValue = z.lazy(
4
+ () => z.union([
5
+ z.string(),
6
+ z.number(),
7
+ z.boolean(),
8
+ z.null(),
9
+ z.array(JsonValue),
10
+ z.record(JsonValue)
11
+ ])
12
+ );
13
+ var NavItemSchema = z.object({
14
+ label: z.string(),
15
+ href: z.string(),
16
+ children: z.array(z.object({ label: z.string(), href: z.string() })).optional()
17
+ });
18
+ var CtaSchema = z.object({ label: z.string(), href: z.string() });
19
+ var MetricSchema = z.object({
20
+ value: z.string(),
21
+ unit: z.string().optional(),
22
+ label: z.string(),
23
+ source: z.string().optional(),
24
+ imageSrc: z.string().optional(),
25
+ imageAlt: z.string().optional()
26
+ });
27
+ var SectionBase = z.object({
28
+ /** 안정적 anchor·dedup·preview 매핑용. 페이지 내 유일. */
29
+ id: z.string().optional(),
30
+ /** 빌트인 또는 customSections registry 키. kebab-case. */
31
+ type: z.string(),
32
+ /** 같은 type 의 비주얼 변형. 빌트인 type 은 enum 으로 좁힘. */
33
+ variant: z.string().optional(),
34
+ /** 섹션 스코프 토큰 override (`--rt-*` CSS 변수 → 값). */
35
+ theme: z.record(z.string()).optional(),
36
+ /** 섹션별 직렬화 props. */
37
+ props: z.record(JsonValue).default({})
38
+ });
39
+ var SECTION_VARIANTS = {
40
+ hero: ["split", "image", "minimal", "carousel", "overlay-panel"],
41
+ services: [
42
+ "grid",
43
+ "list",
44
+ "index-rows",
45
+ "split-featured",
46
+ "action-rail",
47
+ "photo-cards"
48
+ ],
49
+ faq: ["default", "two-column", "with-contact", "content-accordion"],
50
+ "cta-banner": ["default", "photo-overlay"],
51
+ trust: [
52
+ "stat-band",
53
+ "logo-cloud",
54
+ "feature-grid",
55
+ "logo-marquee",
56
+ "credential-list",
57
+ "dashboard"
58
+ ],
59
+ contact: ["form-map-5050", "split", "channel-cards", "visit-band"],
60
+ hours: ["table", "compact"],
61
+ pricing: ["table", "cards"],
62
+ process: ["horizontal", "vertical", "timeline-marked"],
63
+ "before-after": ["slider", "side-by-side"],
64
+ testimonials: ["editorial-quote", "quote-rail"],
65
+ team: ["grid", "spotlight", "expanding-stage", "row"],
66
+ expertise: ["tab-stage", "cards"],
67
+ spaces: ["stage", "mosaic"],
68
+ concerns: ["figure-map", "list"],
69
+ footer: ["columns", "minimal"]
70
+ };
71
+ var HeroCarouselSlideSchema = z.object({
72
+ image: z.string(),
73
+ alt: z.string(),
74
+ caption: z.string().optional(),
75
+ eyebrow: z.string().optional(),
76
+ title: z.string().optional(),
77
+ subtitle: z.string().optional(),
78
+ description: z.string().optional(),
79
+ primaryCta: CtaSchema.optional(),
80
+ secondaryCta: CtaSchema.optional(),
81
+ tabLabel: z.string().optional(),
82
+ tabMeta: z.string().optional(),
83
+ metrics: z.array(MetricSchema).max(4).optional()
84
+ });
85
+ var HeroProofMetricsSchema = z.object({
86
+ items: z.array(MetricSchema).min(1).max(4),
87
+ position: z.enum(["inline", "overlay"]).default("inline")
88
+ });
89
+ var HeroSectionSchema = SectionBase.extend({
90
+ type: z.literal("hero"),
91
+ variant: z.enum(SECTION_VARIANTS.hero).default("split"),
92
+ props: z.object({
93
+ eyebrow: z.string().optional(),
94
+ /** `\n` 으로 줄바꿈. */
95
+ title: z.string(),
96
+ description: z.string().optional(),
97
+ imageSrc: z.string().optional(),
98
+ imageAlt: z.string().optional(),
99
+ cta: CtaSchema.optional(),
100
+ /** `carousel` 전용 — 고정 카피 아래 배경만 전환되는 슬라이드. 2-8개. */
101
+ slides: z.array(HeroCarouselSlideSchema).min(2).max(8).optional(),
102
+ /** `carousel` 전용 — 부제(슬라이드와 무관한 고정 카피). */
103
+ subtitle: z.string().optional(),
104
+ /** `carousel` 전용. 기본 true — `useReducedMotion` 시 렌더러가 강제 off. */
105
+ autoplay: z.boolean().optional(),
106
+ /** `carousel` 전용, ms. 기본 6000. */
107
+ intervalMs: z.number().min(3e3).max(1e4).optional(),
108
+ proofMetrics: HeroProofMetricsSchema.optional(),
109
+ scrim: z.enum(["none", "soft", "strong"]).optional()
110
+ })
111
+ }).superRefine((section, ctx) => {
112
+ if (section.variant === "overlay-panel") {
113
+ if (!section.props.imageSrc) {
114
+ ctx.addIssue({
115
+ code: z.ZodIssueCode.custom,
116
+ path: ["props", "imageSrc"],
117
+ message: "overlay-panel hero \uB294 imageSrc \uAC00 \uD544\uC218\uC785\uB2C8\uB2E4."
118
+ });
119
+ }
120
+ if (!section.props.imageAlt) {
121
+ ctx.addIssue({
122
+ code: z.ZodIssueCode.custom,
123
+ path: ["props", "imageAlt"],
124
+ message: "overlay-panel hero \uB294 imageAlt \uAC00 \uD544\uC218\uC785\uB2C8\uB2E4."
125
+ });
126
+ }
127
+ }
128
+ if (section.variant !== "carousel") return;
129
+ if (section.props.title.length < 1 || section.props.title.length > 80) {
130
+ ctx.addIssue({
131
+ code: z.ZodIssueCode.custom,
132
+ path: ["props", "title"],
133
+ message: "carousel hero \uC81C\uBAA9\uC740 1-80\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4."
134
+ });
135
+ }
136
+ if (!section.props.slides || section.props.slides.length < 2) {
137
+ ctx.addIssue({
138
+ code: z.ZodIssueCode.custom,
139
+ path: ["props", "slides"],
140
+ message: "carousel hero \uB294 slides 2-8\uAC1C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
141
+ });
142
+ }
143
+ });
144
+ var RichTextSectionSchema = SectionBase.extend({
145
+ type: z.literal("rich-text"),
146
+ props: z.object({
147
+ heading: z.string().optional(),
148
+ /** `\n\n` 으로 문단 분리. */
149
+ body: z.string()
150
+ })
151
+ });
152
+ var ServicesSectionSchema = SectionBase.extend({
153
+ type: z.literal("services"),
154
+ /**
155
+ * C10 §1-2 — `index-rows`(hairline 인덱스 행) · `split-featured`(비대칭
156
+ * 5/7, 대표 서비스 1개 + 나머지 컴팩트 리스트) 추가. AI-slop 금지 #1/#2
157
+ * (아이콘 그리드·균등 카드) 대체 문법.
158
+ */
159
+ variant: z.enum(SECTION_VARIANTS.services).default("grid"),
160
+ props: z.object({
161
+ heading: z.string().optional(),
162
+ description: z.string().optional(),
163
+ items: z.array(
164
+ z.object({
165
+ title: z.string(),
166
+ subtitle: z.string().optional(),
167
+ description: z.string().optional(),
168
+ href: z.string().optional(),
169
+ iconSrc: z.string().optional(),
170
+ iconAlt: z.string().optional(),
171
+ imageSrc: z.string().optional(),
172
+ imageAlt: z.string().optional(),
173
+ kind: z.string().optional(),
174
+ ctaLabel: z.string().optional(),
175
+ chips: z.array(z.string()).max(6).optional(),
176
+ meta: z.array(z.string()).max(8).optional()
177
+ })
178
+ ).min(1).max(12)
179
+ })
180
+ }).superRefine((section, ctx) => {
181
+ if (section.variant === "split-featured" && section.props.items.length < 2) {
182
+ ctx.addIssue({
183
+ code: z.ZodIssueCode.custom,
184
+ path: ["props", "items"],
185
+ message: "split-featured \uB294 \uB300\uD45C \uC11C\uBE44\uC2A4 1\uAC1C + \uB098\uBA38\uC9C0 \uCD5C\uC18C 1\uAC1C, \uCD1D 2\uAC1C \uC774\uC0C1\uC758 items \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
186
+ });
187
+ }
188
+ section.props.items.forEach((item, index) => {
189
+ if (item.iconSrc && !item.iconAlt) {
190
+ ctx.addIssue({
191
+ code: z.ZodIssueCode.custom,
192
+ path: ["props", "items", index, "iconAlt"],
193
+ message: "iconSrc \uAC00 \uC788\uC73C\uBA74 iconAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
194
+ });
195
+ }
196
+ if (section.variant === "photo-cards" && !item.imageSrc) {
197
+ ctx.addIssue({
198
+ code: z.ZodIssueCode.custom,
199
+ path: ["props", "items", index, "imageSrc"],
200
+ message: "photo-cards \uC11C\uBE44\uC2A4 \uD56D\uBAA9\uC740 imageSrc \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
201
+ });
202
+ }
203
+ if (section.variant === "photo-cards" && !item.imageAlt) {
204
+ ctx.addIssue({
205
+ code: z.ZodIssueCode.custom,
206
+ path: ["props", "items", index, "imageAlt"],
207
+ message: "photo-cards \uC11C\uBE44\uC2A4 \uD56D\uBAA9\uC740 imageAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
208
+ });
209
+ }
210
+ });
211
+ });
212
+ var FooterLogoSchema = z.object({
213
+ src: z.string(),
214
+ alt: z.string()
215
+ });
216
+ var FooterLinkSchema = z.object({
217
+ label: z.string(),
218
+ href: z.string()
219
+ });
220
+ var FooterColumnSchema = z.object({
221
+ title: z.string(),
222
+ links: z.array(FooterLinkSchema).min(1).max(12)
223
+ });
224
+ var FooterContactInfoSchema = z.object({
225
+ address: z.string().optional(),
226
+ phone: z.string().optional(),
227
+ email: z.string().optional()
228
+ });
229
+ var FooterSocialLinkSchema = z.object({
230
+ kind: z.string(),
231
+ href: z.string()
232
+ });
233
+ var FooterSectionSchema = SectionBase.extend({
234
+ type: z.literal("footer"),
235
+ variant: z.enum(SECTION_VARIANTS.footer).default("columns"),
236
+ props: z.object({
237
+ businessName: z.string(),
238
+ logo: FooterLogoSchema.optional(),
239
+ description: z.string().optional(),
240
+ columns: z.array(FooterColumnSchema).max(6).optional(),
241
+ contactInfo: FooterContactInfoSchema.optional(),
242
+ legalInfo: z.string().optional(),
243
+ privacyHref: z.string().optional(),
244
+ termsHref: z.string().optional(),
245
+ socialLinks: z.array(FooterSocialLinkSchema).max(8).optional(),
246
+ copyright: z.string().optional(),
247
+ /** 'hidden' 은 화이트라벨 plan gate 통과 시만 유효 (미지정 = 'default' 렌더). tenant 커스터마이즈 대상 아님 — 에디터 폼에 노출하지 않는다. */
248
+ credit: z.enum(["default", "hidden"]).optional()
249
+ })
250
+ }).superRefine((section, ctx) => {
251
+ if (section.variant === "columns" && (!section.props.columns || section.props.columns.length === 0)) {
252
+ ctx.addIssue({
253
+ code: z.ZodIssueCode.custom,
254
+ path: ["props", "columns"],
255
+ message: "columns footer \uB294 columns 1\uAC1C \uC774\uC0C1\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."
256
+ });
257
+ }
258
+ });
259
+ var FaqItemSchema = z.object({ question: z.string(), answer: z.string() });
260
+ var FaqGroupSchema = z.object({
261
+ title: z.string(),
262
+ items: z.array(FaqItemSchema).min(1).max(12)
263
+ });
264
+ var FaqContactSchema = z.object({
265
+ title: z.string(),
266
+ description: z.string().optional(),
267
+ phone: z.string().optional(),
268
+ cta: CtaSchema.optional()
269
+ });
270
+ var FaqContentItemSchema = z.object({
271
+ title: z.string(),
272
+ summary: z.string().optional(),
273
+ body: z.string(),
274
+ signs: z.array(z.string()).max(8).optional(),
275
+ href: z.string().optional()
276
+ });
277
+ var FaqContentGroupSchema = z.object({
278
+ title: z.string(),
279
+ items: z.array(FaqContentItemSchema).min(1).max(12)
280
+ });
281
+ var FaqSectionSchema = SectionBase.extend({
282
+ type: z.literal("faq"),
283
+ variant: z.enum(SECTION_VARIANTS.faq).default("default"),
284
+ props: z.object({
285
+ heading: z.string().optional(),
286
+ items: z.array(FaqItemSchema).min(1).max(20).optional(),
287
+ /** `two-column` 전용 — 그룹 헤더 + 항목. 없으면 기존 flat items 를 사용한다. */
288
+ groups: z.array(FaqGroupSchema).min(1).max(6).optional(),
289
+ /** `with-contact` 전용 — 우측 sticky 상담 유도 카드. */
290
+ contact: FaqContactSchema.optional(),
291
+ /** `content-accordion` 전용 — FAQPage가 아닌 질환/서비스 설명 accordion. */
292
+ contentGroups: z.array(FaqContentGroupSchema).min(1).max(6).optional(),
293
+ /** FAQPage JSON-LD 주입 (기본 true — SEO 3 pillars). 페이지에 FAQ 섹션이 여럿이면 하나만 true 로. */
294
+ jsonLd: z.boolean().default(true)
295
+ })
296
+ }).superRefine((section, ctx) => {
297
+ const hasContentGroups = Boolean(
298
+ section.props.contentGroups && section.props.contentGroups.length > 0
299
+ );
300
+ if (section.variant === "content-accordion") {
301
+ if (!hasContentGroups) {
302
+ ctx.addIssue({
303
+ code: z.ZodIssueCode.custom,
304
+ path: ["props", "contentGroups"],
305
+ message: "content-accordion faq sections require contentGroups"
306
+ });
307
+ }
308
+ return;
309
+ }
310
+ const hasItems = Boolean(section.props.items && section.props.items.length > 0);
311
+ const hasGroups = Boolean(section.props.groups && section.props.groups.length > 0);
312
+ if (!hasItems && !hasGroups) {
313
+ ctx.addIssue({
314
+ code: z.ZodIssueCode.custom,
315
+ path: ["props", "items"],
316
+ message: "faq sections require items or groups"
317
+ });
318
+ }
319
+ if (section.variant === "with-contact" && !section.props.contact) {
320
+ ctx.addIssue({
321
+ code: z.ZodIssueCode.custom,
322
+ path: ["props", "contact"],
323
+ message: "with-contact faq sections require contact"
324
+ });
325
+ }
326
+ });
327
+ var CtaBannerSectionSchema = SectionBase.extend({
328
+ type: z.literal("cta-banner"),
329
+ /** C10 §1-5 — `photo-overlay`: 풀블리드 사진 + 다크 오버레이 극단 대비 CTA. */
330
+ variant: z.enum(SECTION_VARIANTS["cta-banner"]).default("default"),
331
+ props: z.object({
332
+ title: z.string(),
333
+ description: z.string().optional(),
334
+ cta: CtaSchema.optional(),
335
+ /** `photo-overlay` 전용 — 필수(사진 없으면 렌더 거부). */
336
+ image: z.string().optional(),
337
+ imageAlt: z.string().optional()
338
+ })
339
+ }).superRefine((section, ctx) => {
340
+ if (section.variant !== "photo-overlay") return;
341
+ if (!section.props.image) {
342
+ ctx.addIssue({
343
+ code: z.ZodIssueCode.custom,
344
+ path: ["props", "image"],
345
+ message: "photo-overlay cta-banner \uB294 image \uAC00 \uD544\uC218\uC785\uB2C8\uB2E4."
346
+ });
347
+ }
348
+ if (!section.props.imageAlt) {
349
+ ctx.addIssue({
350
+ code: z.ZodIssueCode.custom,
351
+ path: ["props", "imageAlt"],
352
+ message: "photo-overlay cta-banner \uB294 imageAlt \uAC00 \uD544\uC218\uC785\uB2C8\uB2E4."
353
+ });
354
+ }
355
+ });
356
+ var BlogListSectionSchema = SectionBase.extend({
357
+ type: z.literal("blog-list"),
358
+ props: z.object({
359
+ heading: z.string().optional(),
360
+ limit: z.number().int().positive().optional(),
361
+ /** 포스트 링크 베이스. 기본 `/blog`. */
362
+ blogBasePath: z.string().optional(),
363
+ emptyMessage: z.string().optional()
364
+ })
365
+ });
366
+ var LeadFormSectionSchema = SectionBase.extend({
367
+ type: z.literal("lead-form"),
368
+ props: z.object({
369
+ /** 사이트 자체 server 엔드포인트 (예: `/api/inquiry`). intake API 직접 지정 금지. */
370
+ action: z.string(),
371
+ vertical: z.enum(["consulting", "medical", "tax", "legal"]).optional(),
372
+ heading: z.string().optional(),
373
+ description: z.string().optional(),
374
+ submitLabel: z.string().optional()
375
+ })
376
+ });
377
+ var TrustMetricSchema = MetricSchema;
378
+ var TrustCredentialSchema = z.object({
379
+ name: z.string(),
380
+ issuer: z.string().optional(),
381
+ kind: z.enum(["license", "education", "association"]).optional(),
382
+ image: z.string().optional(),
383
+ /** `credential-list` 전용 — hairline 행의 tabular 연도. */
384
+ year: z.string().optional()
385
+ });
386
+ var TrustLogoSchema = z.object({
387
+ src: z.string(),
388
+ alt: z.string(),
389
+ href: z.string().optional()
390
+ });
391
+ var LegacyTrustLogoSchema = z.object({
392
+ image: z.string(),
393
+ alt: z.string(),
394
+ href: z.string().optional()
395
+ }).transform((logo) => ({
396
+ src: logo.image,
397
+ alt: logo.alt,
398
+ ...logo.href ? { href: logo.href } : {}
399
+ }));
400
+ var TrustLogoSlotSchema = z.union([TrustLogoSchema, LegacyTrustLogoSchema]);
401
+ var TrustChartSchema = z.object({
402
+ type: z.enum(["bar", "line"]).default("bar"),
403
+ title: z.string(),
404
+ description: z.string().optional(),
405
+ points: z.array(
406
+ z.object({
407
+ label: z.string(),
408
+ value: z.number(),
409
+ display: z.string().optional()
410
+ })
411
+ ).min(1).max(12)
412
+ });
413
+ var TrustSectionSchema = SectionBase.extend({
414
+ type: z.literal("trust"),
415
+ /**
416
+ * C10 §1-3/1-4 — `logo-marquee`(C8 Marquee 소비, 흑백/저채도 로고 스트립) ·
417
+ * `credential-list`(연도 tabular + hairline 자격 리스트) 추가.
418
+ */
419
+ variant: z.enum(SECTION_VARIANTS.trust).default("stat-band"),
420
+ props: z.object({
421
+ heading: z.string().optional(),
422
+ metrics: z.array(TrustMetricSchema).min(2).max(4).optional(),
423
+ credentials: z.array(TrustCredentialSchema).max(8).optional(),
424
+ logos: z.array(TrustLogoSlotSchema).max(8).optional(),
425
+ /** `logo-marquee` 전용 — logo-cloud 와 동일한 `{src, alt, href?}` 슬롯. 최소 3개. */
426
+ marqueeLogos: z.array(TrustLogoSlotSchema).min(3).max(16).optional(),
427
+ chart: TrustChartSchema.optional(),
428
+ disclaimer: z.string().optional()
429
+ })
430
+ }).superRefine((section, ctx) => {
431
+ if (section.variant === "stat-band" && !section.props.metrics) {
432
+ ctx.addIssue({
433
+ code: z.ZodIssueCode.custom,
434
+ path: ["props", "metrics"],
435
+ message: "stat-band trust sections require 2-4 metrics"
436
+ });
437
+ }
438
+ if (section.variant === "logo-cloud" && !section.props.logos) {
439
+ ctx.addIssue({
440
+ code: z.ZodIssueCode.custom,
441
+ path: ["props", "logos"],
442
+ message: "logo-cloud trust sections require logos"
443
+ });
444
+ }
445
+ if ((section.variant === "feature-grid" || section.variant === "credential-list") && !section.props.credentials) {
446
+ ctx.addIssue({
447
+ code: z.ZodIssueCode.custom,
448
+ path: ["props", "credentials"],
449
+ message: `${section.variant} trust sections require credentials`
450
+ });
451
+ }
452
+ if (section.variant === "logo-marquee" && !section.props.marqueeLogos) {
453
+ ctx.addIssue({
454
+ code: z.ZodIssueCode.custom,
455
+ path: ["props", "marqueeLogos"],
456
+ message: "logo-marquee trust sections require marqueeLogos (min 3)"
457
+ });
458
+ }
459
+ if (section.variant === "dashboard" && !section.props.chart) {
460
+ ctx.addIssue({
461
+ code: z.ZodIssueCode.custom,
462
+ path: ["props", "chart"],
463
+ message: "dashboard trust sections require chart"
464
+ });
465
+ }
466
+ });
467
+ var ContactHoursSchema = z.object({
468
+ day: z.string(),
469
+ open: z.string(),
470
+ close: z.string(),
471
+ note: z.string().optional()
472
+ });
473
+ var ContactChannelSchema = z.object({
474
+ label: z.string(),
475
+ description: z.string().optional(),
476
+ value: z.string().optional(),
477
+ href: z.string().optional(),
478
+ actionLabel: z.string().optional(),
479
+ iconSrc: z.string().optional(),
480
+ iconAlt: z.string().optional(),
481
+ kind: z.string().optional()
482
+ });
483
+ var ContactMapSchema = z.object({
484
+ facadeImageSrc: z.string().optional(),
485
+ facadeImageAlt: z.string().optional(),
486
+ provider: z.string().optional(),
487
+ embedUrl: z.string().optional(),
488
+ externalUrl: z.string().optional(),
489
+ lat: z.number().optional(),
490
+ lng: z.number().optional()
491
+ });
492
+ var ContactActionSchema = z.object({
493
+ label: z.string(),
494
+ href: z.string(),
495
+ kind: z.string().optional()
496
+ });
497
+ var ContactSectionSchema = SectionBase.extend({
498
+ type: z.literal("contact"),
499
+ variant: z.enum(SECTION_VARIANTS.contact).default("form-map-5050"),
500
+ props: z.object({
501
+ heading: z.string().optional(),
502
+ form: z.object({
503
+ enabled: z.boolean(),
504
+ vertical: z.enum(["medical", "tax", "legal", "consulting"]).optional()
505
+ }),
506
+ phone: z.string().optional(),
507
+ address: z.string().optional(),
508
+ hours: z.array(ContactHoursSchema).max(8).optional(),
509
+ mapEmbedUrl: z.string().optional(),
510
+ map: ContactMapSchema.optional(),
511
+ actions: z.array(ContactActionSchema).max(6).optional(),
512
+ /** `channel-cards` 전용 — 카드당 하나의 행동. */
513
+ channels: z.array(ContactChannelSchema).min(3).max(6).optional()
514
+ })
515
+ }).superRefine((section, ctx) => {
516
+ if (section.variant !== "channel-cards") return;
517
+ if (!section.props.channels) {
518
+ ctx.addIssue({
519
+ code: z.ZodIssueCode.custom,
520
+ path: ["props", "channels"],
521
+ message: "channel-cards contact sections require 3-6 channels"
522
+ });
523
+ }
524
+ section.props.channels?.forEach((channel, index) => {
525
+ if (channel.iconSrc && !channel.iconAlt) {
526
+ ctx.addIssue({
527
+ code: z.ZodIssueCode.custom,
528
+ path: ["props", "channels", index, "iconAlt"],
529
+ message: "iconSrc \uAC00 \uC788\uC73C\uBA74 iconAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
530
+ });
531
+ }
532
+ });
533
+ });
534
+ var PracticeDaySchema = z.enum([
535
+ "monday",
536
+ "tuesday",
537
+ "wednesday",
538
+ "thursday",
539
+ "friday",
540
+ "saturday",
541
+ "sunday"
542
+ ]);
543
+ var HoursBadgeSlotsSchema = z.object({
544
+ lunch: z.string().optional(),
545
+ evening: z.string().optional(),
546
+ closed: z.string().optional(),
547
+ extra: z.array(z.string()).max(4).optional()
548
+ });
549
+ var HoursRowSchema = z.object({
550
+ day: PracticeDaySchema,
551
+ label: z.string(),
552
+ hours: z.string(),
553
+ lunchHours: z.string().optional(),
554
+ badges: HoursBadgeSlotsSchema.optional(),
555
+ note: z.string().optional()
556
+ });
557
+ var HoursSectionSchema = SectionBase.extend({
558
+ type: z.literal("hours"),
559
+ variant: z.enum(SECTION_VARIANTS.hours).default("table"),
560
+ props: z.object({
561
+ eyebrow: z.string().optional(),
562
+ heading: z.string().optional(),
563
+ description: z.string().optional(),
564
+ rows: z.array(HoursRowSchema).min(1).max(14),
565
+ note: z.string().optional(),
566
+ disclaimer: z.string().optional()
567
+ })
568
+ });
569
+ var PricingItemSchema = z.object({
570
+ name: z.string(),
571
+ description: z.string().optional(),
572
+ price: z.string(),
573
+ condition: z.string().optional()
574
+ });
575
+ var PricingGroupSchema = z.object({
576
+ label: z.string(),
577
+ items: z.array(PricingItemSchema).min(1).max(12)
578
+ });
579
+ var PricingSectionSchema = SectionBase.extend({
580
+ type: z.literal("pricing"),
581
+ variant: z.enum(SECTION_VARIANTS.pricing).default("table"),
582
+ props: z.object({
583
+ eyebrow: z.string().optional(),
584
+ heading: z.string().optional(),
585
+ description: z.string().optional(),
586
+ groups: z.array(PricingGroupSchema).min(1).max(6),
587
+ disclaimer: z.string().min(1)
588
+ })
589
+ });
590
+ var ProcessTimelineItemSchema = z.object({
591
+ eyebrow: z.string().optional(),
592
+ title: z.string(),
593
+ description: z.string(),
594
+ duration: z.string().optional(),
595
+ status: z.enum(["complete", "current", "pending"]).optional(),
596
+ statusLabel: z.string().optional(),
597
+ mark: z.enum(["check", "dot", "none"]).optional(),
598
+ imageSrc: z.string().optional(),
599
+ imageAlt: z.string().optional()
600
+ });
601
+ var ProcessSectionSchema = SectionBase.extend({
602
+ type: z.literal("process"),
603
+ variant: z.enum(SECTION_VARIANTS.process).default("horizontal"),
604
+ props: z.object({
605
+ eyebrow: z.string().optional(),
606
+ heading: z.string().optional(),
607
+ description: z.string().optional(),
608
+ items: z.array(ProcessTimelineItemSchema).min(2).max(8),
609
+ connector: z.enum(["line", "rail"]).optional(),
610
+ disclaimer: z.string().optional()
611
+ })
612
+ }).superRefine((section, ctx) => {
613
+ section.props.items.forEach((item, index) => {
614
+ if (item.imageSrc && !item.imageAlt) {
615
+ ctx.addIssue({
616
+ code: z.ZodIssueCode.custom,
617
+ path: ["props", "items", index, "imageAlt"],
618
+ message: "imageSrc \uAC00 \uC788\uC73C\uBA74 imageAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
619
+ });
620
+ }
621
+ });
622
+ });
623
+ var BeforeAfterImageSchema = z.object({
624
+ src: z.string(),
625
+ alt: z.string()
626
+ });
627
+ var BeforeAfterSectionSchema = SectionBase.extend({
628
+ type: z.literal("before-after"),
629
+ variant: z.enum(SECTION_VARIANTS["before-after"]).default("slider"),
630
+ props: z.object({
631
+ beforeImage: BeforeAfterImageSchema,
632
+ afterImage: BeforeAfterImageSchema,
633
+ beforeLabel: z.string().default("\uC2DC\uACF5 \uC804"),
634
+ afterLabel: z.string().default("\uC2DC\uACF5 \uD6C4"),
635
+ heading: z.string().optional(),
636
+ description: z.string().optional(),
637
+ disclaimer: z.string().optional()
638
+ })
639
+ });
640
+ var TestimonialItemSchema = z.object({
641
+ quote: z.string().min(1).max(280),
642
+ author: z.string().min(1),
643
+ /** 케이스 라벨(예: "허리디스크 · 비수술 치료 3개월"). */
644
+ context: z.string().optional(),
645
+ rating: z.number().min(0).max(5).optional(),
646
+ source: z.string().optional(),
647
+ visitedAt: z.string().optional(),
648
+ item: z.string().optional(),
649
+ reply: z.string().optional()
650
+ });
651
+ var TestimonialsSectionSchema = SectionBase.extend({
652
+ type: z.literal("testimonials"),
653
+ /**
654
+ * `editorial-quote`(기본): 대형 인용 타이포 에디토리얼 — 1개면 중앙 정렬
655
+ * 스테이지, 2개 이상이면 세로 스택(hairline 구분). `quote-rail`: 가로
656
+ * 스크롤 레일(hairline 좌측 보더 컬럼, 균등 그림자 카드 금지).
657
+ */
658
+ variant: z.enum(SECTION_VARIANTS.testimonials).default("editorial-quote"),
659
+ props: z.object({
660
+ heading: z.string(),
661
+ description: z.string().optional(),
662
+ items: z.array(TestimonialItemSchema).min(1).max(12),
663
+ marquee: z.boolean().optional(),
664
+ /** 법정 고지 슬롯("개인 경험이며 효과는 다를 수 있습니다" 류). */
665
+ disclaimer: z.string().optional()
666
+ })
667
+ });
668
+ var TeamMemberSchema = z.object({
669
+ name: z.string().min(1),
670
+ title: z.string().min(1),
671
+ photoSrc: z.string().optional(),
672
+ photoAlt: z.string().optional(),
673
+ specialties: z.array(z.string().min(1)).min(1).max(6).optional(),
674
+ credentials: z.array(z.string().min(1)).min(1).max(8).optional(),
675
+ bio: z.string().max(360).optional(),
676
+ href: z.string().optional(),
677
+ ctaLabel: z.string().optional()
678
+ }).superRefine((member, ctx) => {
679
+ if (member.photoSrc && !member.photoAlt) {
680
+ ctx.addIssue({
681
+ code: z.ZodIssueCode.custom,
682
+ path: ["photoAlt"],
683
+ message: "photoSrc \uAC00 \uC788\uC73C\uBA74 photoAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
684
+ });
685
+ }
686
+ });
687
+ var TeamSectionSchema = SectionBase.extend({
688
+ type: z.literal("team"),
689
+ variant: z.enum(SECTION_VARIANTS.team).default("grid"),
690
+ props: z.object({
691
+ heading: z.string().optional(),
692
+ description: z.string().optional(),
693
+ members: z.array(TeamMemberSchema).min(2).max(8)
694
+ })
695
+ });
696
+ var ExpertiseItemSchema = z.object({
697
+ id: z.string().min(1),
698
+ label: z.string().min(1),
699
+ kicker: z.string().optional(),
700
+ title: z.string().min(1),
701
+ body: z.string().min(1),
702
+ imageSrc: z.string().optional(),
703
+ imageAlt: z.string().optional(),
704
+ tags: z.array(z.string().min(1)).max(8).optional(),
705
+ href: z.string().optional(),
706
+ ctaLabel: z.string().optional()
707
+ }).superRefine((item, ctx) => {
708
+ if (item.imageSrc && !item.imageAlt) {
709
+ ctx.addIssue({
710
+ code: z.ZodIssueCode.custom,
711
+ path: ["imageAlt"],
712
+ message: "imageSrc \uAC00 \uC788\uC73C\uBA74 imageAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
713
+ });
714
+ }
715
+ });
716
+ var ExpertiseSectionSchema = SectionBase.extend({
717
+ type: z.literal("expertise"),
718
+ variant: z.enum(SECTION_VARIANTS.expertise).default("tab-stage"),
719
+ props: z.object({
720
+ heading: z.string().optional(),
721
+ description: z.string().optional(),
722
+ items: z.array(ExpertiseItemSchema).min(2).max(6)
723
+ })
724
+ });
725
+ var SpaceItemSchema = z.object({
726
+ id: z.string().min(1),
727
+ label: z.string().min(1),
728
+ kicker: z.string().optional(),
729
+ title: z.string().optional(),
730
+ description: z.string().optional(),
731
+ imageSrc: z.string().min(1),
732
+ imageAlt: z.string().min(1),
733
+ tags: z.array(z.string().min(1)).max(8).optional()
734
+ });
735
+ var SpacesSectionSchema = SectionBase.extend({
736
+ type: z.literal("spaces"),
737
+ variant: z.enum(SECTION_VARIANTS.spaces).default("stage"),
738
+ props: z.object({
739
+ heading: z.string().optional(),
740
+ description: z.string().optional(),
741
+ spaces: z.array(SpaceItemSchema).min(2).max(8),
742
+ disclaimer: z.string().optional()
743
+ })
744
+ });
745
+ var ConcernRegionSchema = z.object({
746
+ id: z.string().min(1),
747
+ label: z.string().min(1),
748
+ x: z.number().min(0).max(100).optional(),
749
+ y: z.number().min(0).max(100).optional(),
750
+ title: z.string().min(1),
751
+ description: z.string().min(1),
752
+ items: z.array(z.string().min(1)).max(8).optional(),
753
+ href: z.string().optional(),
754
+ ctaLabel: z.string().optional()
755
+ });
756
+ var ConcernsSectionSchema = SectionBase.extend({
757
+ type: z.literal("concerns"),
758
+ variant: z.enum(SECTION_VARIANTS.concerns).default("figure-map"),
759
+ props: z.object({
760
+ heading: z.string().optional(),
761
+ description: z.string().optional(),
762
+ figureImage: z.string().optional(),
763
+ figureAlt: z.string().optional(),
764
+ regions: z.array(ConcernRegionSchema).min(3).max(10),
765
+ disclaimer: z.string().min(1)
766
+ })
767
+ }).superRefine((section, ctx) => {
768
+ if (section.variant !== "figure-map") return;
769
+ if (!section.props.figureImage) {
770
+ ctx.addIssue({
771
+ code: z.ZodIssueCode.custom,
772
+ path: ["props", "figureImage"],
773
+ message: "figure-map concerns \uB294 figureImage \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
774
+ });
775
+ }
776
+ if (!section.props.figureAlt) {
777
+ ctx.addIssue({
778
+ code: z.ZodIssueCode.custom,
779
+ path: ["props", "figureAlt"],
780
+ message: "figure-map concerns \uB294 figureAlt \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
781
+ });
782
+ }
783
+ section.props.regions.forEach((region, index) => {
784
+ if (typeof region.x !== "number") {
785
+ ctx.addIssue({
786
+ code: z.ZodIssueCode.custom,
787
+ path: ["props", "regions", index, "x"],
788
+ message: "figure-map concerns region \uC740 x \uC88C\uD45C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
789
+ });
790
+ }
791
+ if (typeof region.y !== "number") {
792
+ ctx.addIssue({
793
+ code: z.ZodIssueCode.custom,
794
+ path: ["props", "regions", index, "y"],
795
+ message: "figure-map concerns region \uC740 y \uC88C\uD45C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
796
+ });
797
+ }
798
+ });
799
+ });
800
+ var CustomSectionSchema = SectionBase;
801
+ var BUILTIN_SECTION_SCHEMAS = {
802
+ hero: HeroSectionSchema,
803
+ "rich-text": RichTextSectionSchema,
804
+ services: ServicesSectionSchema,
805
+ faq: FaqSectionSchema,
806
+ "cta-banner": CtaBannerSectionSchema,
807
+ "blog-list": BlogListSectionSchema,
808
+ "lead-form": LeadFormSectionSchema,
809
+ trust: TrustSectionSchema,
810
+ contact: ContactSectionSchema,
811
+ hours: HoursSectionSchema,
812
+ pricing: PricingSectionSchema,
813
+ process: ProcessSectionSchema,
814
+ "before-after": BeforeAfterSectionSchema,
815
+ testimonials: TestimonialsSectionSchema,
816
+ team: TeamSectionSchema,
817
+ expertise: ExpertiseSectionSchema,
818
+ spaces: SpacesSectionSchema,
819
+ concerns: ConcernsSectionSchema,
820
+ footer: FooterSectionSchema
821
+ };
822
+ var BUILTIN_SECTION_TYPES = Object.keys(
823
+ BUILTIN_SECTION_SCHEMAS
824
+ );
825
+ var SectionSchema = z.object({ type: z.string() }).passthrough().transform((section, ctx) => {
826
+ const schema = BUILTIN_SECTION_SCHEMAS[section.type] ?? CustomSectionSchema;
827
+ const result = schema.safeParse(section);
828
+ if (!result.success) {
829
+ for (const issue of result.error.issues) ctx.addIssue(issue);
830
+ return z.NEVER;
831
+ }
832
+ return result.data;
833
+ });
834
+ var CollectionSchema = z.object({
835
+ /** 안정 식별자 (라우트/가드/파생 키). */
836
+ key: z.string(),
837
+ /** 작성자 카드·nav 라벨. */
838
+ label: z.string(),
839
+ /** 라우트 base path (예: "/notice" | "/blog"). 앞 슬래시 포함. */
840
+ basePath: z.string(),
841
+ /** 레이아웃 레지스트리 키 (board=게시판, grid=카드 그리드, …). 미지 키는 렌더러 fallback. */
842
+ layout: z.string().default("grid"),
843
+ /** 이 스트림에 속하는 category slug 들. */
844
+ categories: z.array(z.string()).default([]),
845
+ /** RSS 피드 포함. */
846
+ feed: z.boolean().default(false),
847
+ /** `${basePath}/categories/{slug}` 아카이브 생성. */
848
+ archives: z.boolean().default(false),
849
+ /** 글별 동적 OG. */
850
+ og: z.boolean().default(false),
851
+ /** 작성 UX: 하위 카테고리 1개 필수(미선택 발행 차단). */
852
+ requireSubcategory: z.boolean().default(false),
853
+ /** 작성 UX: 상단고정 토글 노출. */
854
+ pinning: z.boolean().default(false),
855
+ /** nav/카드 아이콘 키 (선택). */
856
+ icon: z.string().optional()
857
+ });
858
+ var SiteConfigSchema = z.object({
859
+ site: z.object({
860
+ /** canonical origin. 생략 시 소비측이 `SITE_URL` env 등으로 해석. */
861
+ url: z.string().url().optional(),
862
+ title: z.string(),
863
+ description: z.string().optional(),
864
+ locale: z.string().default("ko-KR"),
865
+ og: z.object({ image: z.string().optional() }).optional()
866
+ }),
867
+ /**
868
+ * 콘텐츠 스트림 분리 (ADR-0060). 비우면 단일 /blog(레거시 동작). 선언 순서 =
869
+ * 라우팅 우선순위. `routes.ts` 팩토리에 `collections` 로 전달해 sitemap/feed/
870
+ * 아카이브를 스트림별로 파생.
871
+ */
872
+ collections: z.array(CollectionSchema).default([]),
873
+ /** 사이트 전역 토큰 override (`--rt-*` CSS 변수 → 값). 비우면 admin /design SSoT. */
874
+ theme: z.record(z.string()).default({}),
875
+ nav: z.array(NavItemSchema).default([]),
876
+ /** path("/", "/services", …) → 섹션 배열. */
877
+ pages: z.record(z.array(SectionSchema))
878
+ });
879
+ function parseSiteConfig(input) {
880
+ return SiteConfigSchema.parse(input);
881
+ }
882
+ function defineSiteConfig(input) {
883
+ return parseSiteConfig(input);
884
+ }
885
+
886
+ export {
887
+ NavItemSchema,
888
+ SECTION_VARIANTS,
889
+ HeroSectionSchema,
890
+ RichTextSectionSchema,
891
+ ServicesSectionSchema,
892
+ FooterSectionSchema,
893
+ FaqSectionSchema,
894
+ CtaBannerSectionSchema,
895
+ BlogListSectionSchema,
896
+ LeadFormSectionSchema,
897
+ TrustSectionSchema,
898
+ ContactSectionSchema,
899
+ HoursSectionSchema,
900
+ PricingSectionSchema,
901
+ ProcessSectionSchema,
902
+ BeforeAfterSectionSchema,
903
+ TestimonialsSectionSchema,
904
+ TeamSectionSchema,
905
+ ExpertiseSectionSchema,
906
+ SpacesSectionSchema,
907
+ ConcernsSectionSchema,
908
+ CustomSectionSchema,
909
+ BUILTIN_SECTION_SCHEMAS,
910
+ BUILTIN_SECTION_TYPES,
911
+ SectionSchema,
912
+ SiteConfigSchema,
913
+ parseSiteConfig,
914
+ defineSiteConfig
915
+ };
916
+ //# sourceMappingURL=chunk-NUUTTSGT.js.map