@roottale/cms-renderer-next 0.47.0 → 0.51.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 (40) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/blog-filter-BKCGM0qd.d.ts +29 -0
  3. package/dist/{chunk-6MFHA555.js → chunk-27RT6B3O.js} +1 -1
  4. package/dist/chunk-27RT6B3O.js.map +1 -0
  5. package/dist/chunk-2QU4JU46.js +3927 -0
  6. package/dist/chunk-2QU4JU46.js.map +1 -0
  7. package/dist/chunk-4RQ3SEDL.js +106 -0
  8. package/dist/chunk-4RQ3SEDL.js.map +1 -0
  9. package/dist/{chunk-OMXEU4LS.js → chunk-QYNEOFL7.js} +140 -136
  10. package/dist/chunk-QYNEOFL7.js.map +1 -0
  11. package/dist/{chunk-FUA5H6JE.js → chunk-U2IM6OCQ.js} +46 -2
  12. package/dist/chunk-U2IM6OCQ.js.map +1 -0
  13. package/dist/cms-public.css +137 -18
  14. package/dist/concerns-client.d.ts +1 -1
  15. package/dist/expertise-client.d.ts +1 -1
  16. package/dist/expertise-client.js +1 -1
  17. package/dist/hero-carousel-client.js +1 -1
  18. package/dist/routes.d.ts +377 -21
  19. package/dist/routes.js +273 -106
  20. package/dist/routes.js.map +1 -1
  21. package/dist/seo-asset-hosts.d.ts +36 -0
  22. package/dist/seo-asset-hosts.js +391 -0
  23. package/dist/seo-asset-hosts.js.map +1 -0
  24. package/dist/server.d.ts +34 -216
  25. package/dist/server.js +7 -7
  26. package/dist/{site-config-Dicv57oY.d.ts → site-config-DNpvlqA5.d.ts} +201 -201
  27. package/dist/spaces-client.d.ts +1 -1
  28. package/dist/team-expanding-stage-client.d.ts +1 -1
  29. package/dist/team-expanding-stage-client.js +1 -1
  30. package/dist/templates.d.ts +64 -25
  31. package/dist/templates.js +14 -7
  32. package/dist/templates.js.map +1 -1
  33. package/package.json +7 -2
  34. package/dist/chunk-6MFHA555.js.map +0 -1
  35. package/dist/chunk-FUA5H6JE.js.map +0 -1
  36. package/dist/chunk-FZK3EXYZ.js +0 -103
  37. package/dist/chunk-FZK3EXYZ.js.map +0 -1
  38. package/dist/chunk-OMXEU4LS.js.map +0 -1
  39. package/dist/chunk-ZHXYCABN.js +0 -3584
  40. package/dist/chunk-ZHXYCABN.js.map +0 -1
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ // src/templates/team-expanding-stage-client.tsx
4
+ import { useState } from "react";
5
+ import { useReducedMotion } from "motion/react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ function memberInitials(name) {
8
+ const parts = name.trim().split(/\s+/).filter(Boolean);
9
+ if (parts.length === 0) return "?";
10
+ const initials = parts.slice(0, 2).map((part) => part.at(0) ?? "").join("");
11
+ return initials || "?";
12
+ }
13
+ function TeamExpandingStageClient({
14
+ members
15
+ }) {
16
+ const [activeIndex, setActiveIndex] = useState(0);
17
+ const reducedMotion = useReducedMotion();
18
+ if (members.length === 0) return null;
19
+ const activate = (index) => setActiveIndex(index);
20
+ const onItemKeyDown = (event, index) => {
21
+ if ((event.key === "Enter" || event.key === " ") && index !== activeIndex) {
22
+ event.preventDefault();
23
+ activate(index);
24
+ }
25
+ };
26
+ return /* @__PURE__ */ jsx(
27
+ "ul",
28
+ {
29
+ className: "rt-tpl-team-expanding-stage",
30
+ "data-reduced-motion": reducedMotion ? "true" : "false",
31
+ children: members.map((member, index) => {
32
+ const active = index === activeIndex;
33
+ return (
34
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- hover 는 마우스 보조 경로다(키보드는 아래 onFocus 가 받는다).
35
+ /* @__PURE__ */ jsxs(
36
+ "li",
37
+ {
38
+ className: "rt-tpl-team-expanding-item",
39
+ "data-active": active ? "true" : "false",
40
+ tabIndex: 0,
41
+ onMouseEnter: () => activate(index),
42
+ onFocus: () => activate(index),
43
+ onClick: (event) => {
44
+ if (!active) event.preventDefault();
45
+ activate(index);
46
+ },
47
+ onKeyDown: (event) => onItemKeyDown(event, index),
48
+ children: [
49
+ /* @__PURE__ */ jsx("span", { className: "rt-tpl-team-expanding-index", "aria-hidden": "true", children: String(index + 1).padStart(2, "0") }),
50
+ member.photoSrc ? /* @__PURE__ */ jsx(
51
+ "img",
52
+ {
53
+ className: "rt-tpl-team-expanding-photo",
54
+ src: member.photoSrc,
55
+ alt: member.photoAlt ?? "",
56
+ loading: "lazy"
57
+ }
58
+ ) : /* @__PURE__ */ jsx(
59
+ "div",
60
+ {
61
+ className: "rt-tpl-team-expanding-photo rt-tpl-team-expanding-photo--initials",
62
+ "aria-hidden": "true",
63
+ children: memberInitials(member.name)
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsxs("div", { className: "rt-tpl-team-expanding-name", "aria-hidden": "true", children: [
67
+ /* @__PURE__ */ jsx("b", { children: member.name }),
68
+ /* @__PURE__ */ jsx("span", { children: member.title })
69
+ ] }),
70
+ /* @__PURE__ */ jsxs("div", { className: "rt-tpl-team-expanding-detail", children: [
71
+ /* @__PURE__ */ jsx("p", { className: "rt-tpl-team-expanding-role", children: member.title }),
72
+ /* @__PURE__ */ jsx("h3", { className: "rt-tpl-team-name", children: member.name }),
73
+ member.specialties ? /* @__PURE__ */ jsx(
74
+ "ul",
75
+ {
76
+ className: "rt-tpl-team-specialties",
77
+ "aria-label": `${member.name} \uC804\uBB38 \uBD84\uC57C`,
78
+ children: member.specialties.map((specialty) => /* @__PURE__ */ jsx("li", { children: specialty }, specialty))
79
+ }
80
+ ) : null,
81
+ member.credentials ? /* @__PURE__ */ jsx("ul", { className: "rt-tpl-team-credentials", children: member.credentials.map((credential) => /* @__PURE__ */ jsx("li", { children: credential }, credential)) }) : null,
82
+ member.bio ? /* @__PURE__ */ jsx("p", { className: "rt-tpl-team-bio", children: member.bio }) : null,
83
+ member.href ? /* @__PURE__ */ jsx(
84
+ "a",
85
+ {
86
+ className: "rt-tpl-team-cta",
87
+ href: member.href,
88
+ tabIndex: active ? 0 : -1,
89
+ children: member.ctaLabel ?? "\uD504\uB85C\uD544 \uBCF4\uAE30"
90
+ }
91
+ ) : null
92
+ ] })
93
+ ]
94
+ },
95
+ member.name
96
+ )
97
+ );
98
+ })
99
+ }
100
+ );
101
+ }
102
+
103
+ export {
104
+ TeamExpandingStageClient
105
+ };
106
+ //# sourceMappingURL=chunk-4RQ3SEDL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/team-expanding-stage-client.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type KeyboardEvent, type ReactElement } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\n\nimport type { TeamSection } from \"./site-config.js\";\n\ntype TeamMember = TeamSection[\"props\"][\"members\"][number];\n\nfunction memberInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n const initials = parts\n .slice(0, 2)\n .map((part) => part.at(0) ?? \"\")\n .join(\"\");\n return initials || \"?\";\n}\n\n/**\n * `team` `expanding-stage` variant 의 실모션 client island (D3 크래프트).\n * chambarun `.staff-stage`/`.staff-panel` 스펙(mockup/assets/site.js·site.css)\n * 이식 — hover/focus/tap 한 인물이 확장되며 자격 정보가 드러난다.\n *\n * 현재 `sections.tsx` `TemplateTeamExpandingStage` 는 이 컴포넌트를 아직\n * import 하지 않는다(정적 격자만 렌더) — `sections.tsx` 는 이 작업 소유 밖이라\n * 와이어링은 별도 보고 대상(README/보고 참조). 이 파일은 그 자리에 그대로\n * 끼워 넣을 수 있도록 `members` 하나만 받는 독립 컴포넌트로 설계했다.\n *\n * 접근성(계약, DESIGN.md \"Medical Staff Cards\" 원칙 확장):\n * - 각 패널은 `tabIndex=0` 컨테이너(hover/focus/click 시 확장) — chambarun\n * 원본과 동일하게 명시적 role 은 두지 않는다(내부에 실제 CTA 링크가 있어\n * `role=\"button\"` 을 얹으면 중첩 인터랙티브 요소가 된다).\n * - 자격정보·경력·CTA 는 접힌 상태에서도 DOM 에 그대로 존재한다(opacity 로만\n * 가림) — 스크린리더는 hover 없이 전체 내용을 읽을 수 있다(\"decorative hover\n * layers do not replace content\").\n * - CTA 링크는 비활성 패널일 때 `tabIndex=-1` — 보이지 않는 링크가 키보드\n * 포커스를 가로채지 않는다(chambarun 원본에는 없던, React 상태로만 가능한\n * 개선).\n * - `prefers-reduced-motion: reduce` → 전환 자체가 CSS 에서 제거된다\n * (templates.css, `@media (prefers-reduced-motion: no-preference)` 안에만\n * transition 선언 — ADR-0073 K3 C1 과 동일 관행). `data-reduced-motion`\n * 속성은 시각 회귀 테스트/QA 스크린샷 식별용.\n */\nexport function TeamExpandingStageClient({\n members,\n}: {\n readonly members: readonly TeamMember[];\n}): ReactElement | null {\n const [activeIndex, setActiveIndex] = useState(0);\n const reducedMotion = useReducedMotion();\n\n if (members.length === 0) return null;\n\n const activate = (index: number) => setActiveIndex(index);\n\n const onItemKeyDown = (event: KeyboardEvent<HTMLLIElement>, index: number) => {\n if ((event.key === \"Enter\" || event.key === \" \") && index !== activeIndex) {\n event.preventDefault();\n activate(index);\n }\n };\n\n return (\n <ul\n className=\"rt-tpl-team-expanding-stage\"\n data-reduced-motion={reducedMotion ? \"true\" : \"false\"}\n >\n {members.map((member, index) => {\n const active = index === activeIndex;\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- hover 는 마우스 보조 경로다(키보드는 아래 onFocus 가 받는다).\n <li\n key={member.name}\n className=\"rt-tpl-team-expanding-item\"\n data-active={active ? \"true\" : \"false\"}\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- 키보드 사용자가 팀원 카드를 넘겨보려면 초점을 받아야 한다(WCAG 2.1.1).\n tabIndex={0}\n onMouseEnter={() => activate(index)}\n onFocus={() => activate(index)}\n onClick={(event) => {\n // 첫 탭(hover/focus 없이 바로 터치)은 확장만 — CTA 로 즉시\n // 이동하지 않는다. 이미 활성(=두 번째 탭)이면 기본 클릭 동작\n // (링크 없는 li 자체는 아무 것도 안 함, 내부 CTA 는 그대로 이동).\n if (!active) event.preventDefault();\n activate(index);\n }}\n onKeyDown={(event) => onItemKeyDown(event, index)}\n >\n <span className=\"rt-tpl-team-expanding-index\" aria-hidden=\"true\">\n {String(index + 1).padStart(2, \"0\")}\n </span>\n {member.photoSrc ? (\n <img\n className=\"rt-tpl-team-expanding-photo\"\n src={member.photoSrc}\n alt={member.photoAlt ?? \"\"}\n loading=\"lazy\"\n />\n ) : (\n <div\n className=\"rt-tpl-team-expanding-photo rt-tpl-team-expanding-photo--initials\"\n aria-hidden=\"true\"\n >\n {memberInitials(member.name)}\n </div>\n )}\n <div className=\"rt-tpl-team-expanding-name\" aria-hidden=\"true\">\n <b>{member.name}</b>\n <span>{member.title}</span>\n </div>\n <div className=\"rt-tpl-team-expanding-detail\">\n <p className=\"rt-tpl-team-expanding-role\">{member.title}</p>\n <h3 className=\"rt-tpl-team-name\">{member.name}</h3>\n {member.specialties ? (\n <ul\n className=\"rt-tpl-team-specialties\"\n aria-label={`${member.name} 전문 분야`}\n >\n {member.specialties.map((specialty) => (\n <li key={specialty}>{specialty}</li>\n ))}\n </ul>\n ) : null}\n {member.credentials ? (\n <ul className=\"rt-tpl-team-credentials\">\n {member.credentials.map((credential) => (\n <li key={credential}>{credential}</li>\n ))}\n </ul>\n ) : null}\n {member.bio ? <p className=\"rt-tpl-team-bio\">{member.bio}</p> : null}\n {member.href ? (\n <a\n className=\"rt-tpl-team-cta\"\n href={member.href}\n tabIndex={active ? 0 : -1}\n >\n {member.ctaLabel ?? \"프로필 보기\"}\n </a>\n ) : null}\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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","names":[]}
@@ -236,6 +236,7 @@ function HeroCarouselViewport({
236
236
  }
237
237
  emblaApi?.scrollTo(index, reducedMotion);
238
238
  },
239
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- 위와 같은 이유(안정화된 메서드만 의존).
239
240
  [isCurtain, curtain.goTo, emblaApi, reducedMotion]
240
241
  );
241
242
  const onKeyDown = useCallback3(
@@ -262,153 +263,156 @@ function HeroCarouselViewport({
262
263
  onSelect: scrollTo,
263
264
  orientation: "horizontal"
264
265
  });
265
- return /* @__PURE__ */ jsxs2(
266
- "div",
267
- {
268
- ref: rootRef,
269
- className: "rt-tpl-hero-carousel",
270
- role: "region",
271
- "aria-roledescription": "carousel",
272
- "aria-label": "\uBC30\uACBD \uC774\uBBF8\uC9C0 \uCE90\uB7EC\uC140",
273
- tabIndex: 0,
274
- onKeyDown,
275
- "data-reduced-motion": reducedMotion ? "true" : "false",
276
- "data-scrim": scrim ?? "soft",
277
- "data-transition": transition,
278
- onMouseEnter: autoplayGate.rootHandlers.onMouseEnter,
279
- onMouseLeave: autoplayGate.rootHandlers.onMouseLeave,
280
- onFocus: autoplayGate.rootHandlers.onFocus,
281
- onBlur: autoplayGate.rootHandlers.onBlur,
282
- children: [
283
- isCurtain ? /* @__PURE__ */ jsx3(
284
- CurtainTrack,
285
- {
286
- slides,
287
- selectedIndex: curtain.selectedIndex,
288
- leavingIndex: curtain.leavingIndex
289
- }
290
- ) : /* @__PURE__ */ jsx3(
291
- SlideTrack,
292
- {
293
- viewportRef: emblaRef,
294
- slides,
295
- selectedIndex: emblaSelectedIndex
296
- }
297
- ),
298
- /* @__PURE__ */ jsx3(
299
- "button",
300
- {
301
- type: "button",
302
- className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev",
303
- "aria-label": "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC",
304
- onClick: scrollPrev,
305
- children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "M15 5 8 12l7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
306
- }
307
- ),
308
- /* @__PURE__ */ jsx3(
309
- "button",
310
- {
311
- type: "button",
312
- className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next",
313
- "aria-label": "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC",
314
- onClick: scrollNext,
315
- children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "m9 5 7 7-7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
316
- }
317
- ),
318
- hasSlidePanel && selectedSlide ? /* @__PURE__ */ jsxs2(
319
- "div",
320
- {
321
- className: "rt-tpl-hero-carousel-slide-panel",
322
- children: [
323
- selectedSlide.eyebrow ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-eyebrow", children: selectedSlide.eyebrow }) : null,
324
- selectedSlide.title ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-title", children: selectedSlide.title }) : null,
325
- selectedSlide.subtitle ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-subtitle", children: selectedSlide.subtitle }) : null,
326
- selectedSlide.description ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-desc", children: selectedSlide.description }) : null,
327
- selectedSlide.metrics && selectedSlide.metrics.length > 0 ? /* @__PURE__ */ jsx3("dl", { className: "rt-tpl-hero-carousel-slide-metrics", children: selectedSlide.metrics.map((metric) => /* @__PURE__ */ jsxs2("div", { children: [
328
- /* @__PURE__ */ jsx3("dt", { children: metric.label }),
329
- /* @__PURE__ */ jsxs2("dd", { children: [
330
- metric.value,
331
- metric.unit ? /* @__PURE__ */ jsx3("span", { children: metric.unit }) : null
332
- ] }),
333
- metric.source ? /* @__PURE__ */ jsx3("dd", { children: metric.source }) : null
334
- ] }, `${metric.value}-${metric.label}`)) }) : null,
335
- selectedSlide.primaryCta || selectedSlide.secondaryCta ? /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-slide-actions", children: [
336
- selectedSlide.primaryCta ? /* @__PURE__ */ jsx3(
337
- "a",
338
- {
339
- className: "rt-tpl-btn rt-tpl-btn--primary",
340
- href: selectedSlide.primaryCta.href,
341
- "data-track": "cta_hero",
342
- children: selectedSlide.primaryCta.label
343
- }
344
- ) : null,
345
- selectedSlide.secondaryCta ? /* @__PURE__ */ jsx3(
346
- "a",
347
- {
348
- className: "rt-tpl-btn rt-tpl-btn--secondary",
349
- href: selectedSlide.secondaryCta.href,
350
- "data-track": "cta_hero",
351
- children: selectedSlide.secondaryCta.label
352
- }
353
- ) : null
354
- ] }) : null
355
- ]
356
- },
357
- isCurtain ? `panel-${selectedIndex}` : "panel"
358
- ) : null,
359
- /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-controls", children: [
266
+ return (
267
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- WAI-ARIA 캐러셀 패턴. region 이 좌우 키·포인터를 받고 실제 이동은 안쪽 컨트롤이 담당한다.
268
+ /* @__PURE__ */ jsxs2(
269
+ "div",
270
+ {
271
+ ref: rootRef,
272
+ className: "rt-tpl-hero-carousel",
273
+ role: "region",
274
+ "aria-roledescription": "carousel",
275
+ "aria-label": "\uBC30\uACBD \uC774\uBBF8\uC9C0 \uCE90\uB7EC\uC140",
276
+ tabIndex: 0,
277
+ onKeyDown,
278
+ "data-reduced-motion": reducedMotion ? "true" : "false",
279
+ "data-scrim": scrim ?? "soft",
280
+ "data-transition": transition,
281
+ onMouseEnter: autoplayGate.rootHandlers.onMouseEnter,
282
+ onMouseLeave: autoplayGate.rootHandlers.onMouseLeave,
283
+ onFocus: autoplayGate.rootHandlers.onFocus,
284
+ onBlur: autoplayGate.rootHandlers.onBlur,
285
+ children: [
286
+ isCurtain ? /* @__PURE__ */ jsx3(
287
+ CurtainTrack,
288
+ {
289
+ slides,
290
+ selectedIndex: curtain.selectedIndex,
291
+ leavingIndex: curtain.leavingIndex
292
+ }
293
+ ) : /* @__PURE__ */ jsx3(
294
+ SlideTrack,
295
+ {
296
+ viewportRef: emblaRef,
297
+ slides,
298
+ selectedIndex: emblaSelectedIndex
299
+ }
300
+ ),
360
301
  /* @__PURE__ */ jsx3(
361
- "div",
302
+ "button",
362
303
  {
363
- className: hasTabLabels ? "rt-tpl-hero-carousel-dots rt-tpl-hero-carousel-dots--tabs" : "rt-tpl-hero-carousel-dots",
364
- role: "tablist",
365
- "aria-label": "\uC2AC\uB77C\uC774\uB4DC \uC120\uD0DD",
366
- onKeyDown: onTablistKeyDown,
367
- children: slides.map((_slide, index) => /* @__PURE__ */ jsx3(
368
- "button",
369
- {
370
- ...getTabProps(index),
371
- type: "button",
372
- role: "tab",
373
- className: "rt-tpl-hero-carousel-dot",
374
- "aria-selected": index === selectedIndex,
375
- "aria-label": `${index + 1}\uBC88 \uC2AC\uB77C\uC774\uB4DC\uB85C \uC774\uB3D9`,
376
- "data-active": index === selectedIndex ? "true" : "false",
377
- onClick: () => scrollTo(index),
378
- children: hasTabLabels ? /* @__PURE__ */ jsxs2(Fragment, { children: [
379
- /* @__PURE__ */ jsx3("span", { children: _slide.tabLabel ?? `${index + 1}` }),
380
- _slide.tabMeta ? /* @__PURE__ */ jsx3("small", { children: _slide.tabMeta }) : null
381
- ] }) : null
382
- },
383
- index
384
- ))
304
+ type: "button",
305
+ className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev",
306
+ "aria-label": "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC",
307
+ onClick: scrollPrev,
308
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "M15 5 8 12l7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
385
309
  }
386
310
  ),
387
- /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-controls-right", children: [
388
- showPlayPause ? /* @__PURE__ */ jsx3(
389
- "button",
311
+ /* @__PURE__ */ jsx3(
312
+ "button",
313
+ {
314
+ type: "button",
315
+ className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next",
316
+ "aria-label": "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC",
317
+ onClick: scrollNext,
318
+ children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "m9 5 7 7-7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
319
+ }
320
+ ),
321
+ hasSlidePanel && selectedSlide ? /* @__PURE__ */ jsxs2(
322
+ "div",
323
+ {
324
+ className: "rt-tpl-hero-carousel-slide-panel",
325
+ children: [
326
+ selectedSlide.eyebrow ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-eyebrow", children: selectedSlide.eyebrow }) : null,
327
+ selectedSlide.title ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-title", children: selectedSlide.title }) : null,
328
+ selectedSlide.subtitle ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-subtitle", children: selectedSlide.subtitle }) : null,
329
+ selectedSlide.description ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-desc", children: selectedSlide.description }) : null,
330
+ selectedSlide.metrics && selectedSlide.metrics.length > 0 ? /* @__PURE__ */ jsx3("dl", { className: "rt-tpl-hero-carousel-slide-metrics", children: selectedSlide.metrics.map((metric) => /* @__PURE__ */ jsxs2("div", { children: [
331
+ /* @__PURE__ */ jsx3("dt", { children: metric.label }),
332
+ /* @__PURE__ */ jsxs2("dd", { children: [
333
+ metric.value,
334
+ metric.unit ? /* @__PURE__ */ jsx3("span", { children: metric.unit }) : null
335
+ ] }),
336
+ metric.source ? /* @__PURE__ */ jsx3("dd", { children: metric.source }) : null
337
+ ] }, `${metric.value}-${metric.label}`)) }) : null,
338
+ selectedSlide.primaryCta || selectedSlide.secondaryCta ? /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-slide-actions", children: [
339
+ selectedSlide.primaryCta ? /* @__PURE__ */ jsx3(
340
+ "a",
341
+ {
342
+ className: "rt-tpl-btn rt-tpl-btn--primary",
343
+ href: selectedSlide.primaryCta.href,
344
+ "data-track": "cta_hero",
345
+ children: selectedSlide.primaryCta.label
346
+ }
347
+ ) : null,
348
+ selectedSlide.secondaryCta ? /* @__PURE__ */ jsx3(
349
+ "a",
350
+ {
351
+ className: "rt-tpl-btn rt-tpl-btn--secondary",
352
+ href: selectedSlide.secondaryCta.href,
353
+ "data-track": "cta_hero",
354
+ children: selectedSlide.secondaryCta.label
355
+ }
356
+ ) : null
357
+ ] }) : null
358
+ ]
359
+ },
360
+ isCurtain ? `panel-${selectedIndex}` : "panel"
361
+ ) : null,
362
+ /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-controls", children: [
363
+ /* @__PURE__ */ jsx3(
364
+ "div",
390
365
  {
391
- type: "button",
392
- className: "rt-tpl-hero-carousel-playpause",
393
- "aria-pressed": autoplayGate.userPaused,
394
- "aria-label": autoplayGate.userPaused ? "\uC790\uB3D9 \uC804\uD658 \uC7AC\uC0DD" : "\uC790\uB3D9 \uC804\uD658 \uC77C\uC2DC\uC815\uC9C0",
395
- onClick: autoplayGate.togglePaused,
396
- children: autoplayGate.userPaused ? /* @__PURE__ */ jsx3(PlayIcon, {}) : /* @__PURE__ */ jsx3(PauseIcon, {})
366
+ className: hasTabLabels ? "rt-tpl-hero-carousel-dots rt-tpl-hero-carousel-dots--tabs" : "rt-tpl-hero-carousel-dots",
367
+ role: "tablist",
368
+ "aria-label": "\uC2AC\uB77C\uC774\uB4DC \uC120\uD0DD",
369
+ onKeyDown: onTablistKeyDown,
370
+ children: slides.map((_slide, index) => /* @__PURE__ */ jsx3(
371
+ "button",
372
+ {
373
+ ...getTabProps(index),
374
+ type: "button",
375
+ role: "tab",
376
+ className: "rt-tpl-hero-carousel-dot",
377
+ "aria-selected": index === selectedIndex,
378
+ "aria-label": `${index + 1}\uBC88 \uC2AC\uB77C\uC774\uB4DC\uB85C \uC774\uB3D9`,
379
+ "data-active": index === selectedIndex ? "true" : "false",
380
+ onClick: () => scrollTo(index),
381
+ children: hasTabLabels ? /* @__PURE__ */ jsxs2(Fragment, { children: [
382
+ /* @__PURE__ */ jsx3("span", { children: _slide.tabLabel ?? `${index + 1}` }),
383
+ _slide.tabMeta ? /* @__PURE__ */ jsx3("small", { children: _slide.tabMeta }) : null
384
+ ] }) : null
385
+ },
386
+ index
387
+ ))
397
388
  }
398
- ) : null,
399
- /* @__PURE__ */ jsxs2("p", { className: "rt-tpl-hero-carousel-counter", "aria-hidden": "true", children: [
400
- /* @__PURE__ */ jsx3("span", { className: "rt-tpl-hero-carousel-counter-current", children: selectedIndex + 1 }),
401
- "/",
402
- /* @__PURE__ */ jsx3("span", { className: "rt-tpl-hero-carousel-counter-total", children: slides.length })
389
+ ),
390
+ /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-controls-right", children: [
391
+ showPlayPause ? /* @__PURE__ */ jsx3(
392
+ "button",
393
+ {
394
+ type: "button",
395
+ className: "rt-tpl-hero-carousel-playpause",
396
+ "aria-pressed": autoplayGate.userPaused,
397
+ "aria-label": autoplayGate.userPaused ? "\uC790\uB3D9 \uC804\uD658 \uC7AC\uC0DD" : "\uC790\uB3D9 \uC804\uD658 \uC77C\uC2DC\uC815\uC9C0",
398
+ onClick: autoplayGate.togglePaused,
399
+ children: autoplayGate.userPaused ? /* @__PURE__ */ jsx3(PlayIcon, {}) : /* @__PURE__ */ jsx3(PauseIcon, {})
400
+ }
401
+ ) : null,
402
+ /* @__PURE__ */ jsxs2("p", { className: "rt-tpl-hero-carousel-counter", "aria-hidden": "true", children: [
403
+ /* @__PURE__ */ jsx3("span", { className: "rt-tpl-hero-carousel-counter-current", children: selectedIndex + 1 }),
404
+ "/",
405
+ /* @__PURE__ */ jsx3("span", { className: "rt-tpl-hero-carousel-counter-total", children: slides.length })
406
+ ] })
403
407
  ] })
404
408
  ] })
405
- ] })
406
- ]
407
- }
409
+ ]
410
+ }
411
+ )
408
412
  );
409
413
  }
410
414
 
411
415
  export {
412
416
  HeroCarouselViewport
413
417
  };
414
- //# sourceMappingURL=chunk-OMXEU4LS.js.map
418
+ //# sourceMappingURL=chunk-QYNEOFL7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/hero-carousel-client.tsx","../src/templates/hero-carousel-curtain.tsx","../src/templates/hero-carousel-hooks.ts","../src/templates/hero-carousel-slide-track.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `hero` `carousel` variant — 배경 사진 전환 메커니즘만 담당하는 client\n * island (M6 서버 우선: 인터랙션 최소 단위 격리). 카피 오버레이(h1/subtitle/\n * eyebrow/CTA)는 `TemplateHero`(RSC, sections.tsx)가 서버에서 그린다 — 이\n * 컴포넌트는 배경 슬라이드 + 도트 + 화살표 + \"N/M\" 카운터만 렌더한다.\n *\n * 빌드 계약: 본 파일은 tsup 의 독립 entry(`hero-carousel-client`)로 번들되고\n * `esbuild-plugin-preserve-directives` 가 \"use client\" 지시어를 해당 청크\n * 최상단에 보존한다 — `templates.js`(서버 전용, 나머지 4개 hero variant +\n * rich-text/services/faq/cta-banner/trust/contact) 로 병합되지 않는다\n * (tsup.config.ts 참고).\n *\n * 모션 규율(M5 + ADR-0086 P2 D3/D4, ds-rule-pack.md):\n * - `transition`: \"slide\"(기본, embla 가로 스크롤) | \"curtain\"(clip-path 와이프\n * — chambarun 이식, `hero-carousel-curtain.tsx`). variant 는 그대로\n * \"carousel\" 하나 — \"variant=구조, motion=인상\" 규칙(신규 variant 금지).\n * - `prefers-reduced-motion: reduce` → autoplay 강제 off + 전환은 즉시(embla\n * `jump=true` 무전환 / curtain 은 leaving 단계 생략) + Ken Burns·신 스윕·카피\n * 스태거 없음.\n * - autoplay 정지 사유 4종(D4): hover, focus-within, pause/재생 버튼, Page\n * Visibility API(탭 비활성). 합성은 `hero-carousel-hooks.ts`\n * `useAutoplayGate`.\n * - 좌/우 화살표 키보드 이동(포커스가 캐러셀 안에 있을 때), 도트는\n * `role=\"tablist\"`/`role=\"tab\"` + roving tabindex(방향키/Home/End, D4) —\n * 공용 `useRovingTablist`(`expertise-client.tsx` 와 동일 계약 재사용).\n * - WAI-ARIA carousel 패턴: 컨테이너 `role=\"region\"` +\n * `aria-roledescription=\"carousel\"`, 슬라이드별 `aria-roledescription=\"slide\"`\n * + `aria-label=\"Slide N of M\"`.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactElement } from \"react\";\nimport useEmblaCarousel from \"embla-carousel-react\";\n\nimport { useCurtainCarousel, CurtainTrack } from \"./hero-carousel-curtain.js\";\nimport { useAutoplayGate, usePrefersReducedMotion } from \"./hero-carousel-hooks.js\";\nimport { SlideTrack } from \"./hero-carousel-slide-track.js\";\nimport type { HeroCarouselViewportProps } from \"./hero-carousel-types.js\";\nimport { useRovingTablist } from \"./use-roving-tablist.js\";\n\nexport type {\n HeroCarouselSlideInput,\n HeroCarouselTransition,\n HeroCarouselViewportProps,\n} from \"./hero-carousel-types.js\";\n\nfunction PauseIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" />\n </svg>\n );\n}\n\nfunction PlayIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M8 5v14l11-7z\" fill=\"currentColor\" />\n </svg>\n );\n}\n\nexport function HeroCarouselViewport({\n slides,\n autoplay,\n intervalMs,\n scrim,\n transition = \"slide\",\n}: HeroCarouselViewportProps) {\n const isCurtain = transition === \"curtain\";\n const reducedMotion = usePrefersReducedMotion();\n const rootRef = useRef<HTMLDivElement | null>(null);\n\n const [emblaRef, emblaApi] = useEmblaCarousel({\n loop: true,\n duration: reducedMotion ? 1 : 20,\n });\n const [emblaSelectedIndex, setEmblaSelectedIndex] = useState(0);\n const curtain = useCurtainCarousel(slides.length, reducedMotion);\n\n const onEmblaSelect = useCallback(() => {\n if (!emblaApi) return;\n setEmblaSelectedIndex(emblaApi.selectedScrollSnap());\n }, [emblaApi]);\n\n useEffect(() => {\n if (!emblaApi) return;\n // eslint-disable-next-line react-hooks/set-state-in-effect -- 캐러셀 인스턴스가 준비된 뒤 현재 위치를 한 번 읽어 맞춘다(이후는 select 이벤트가 갱신). 외부 라이브러리 상태 구독이다.\n onEmblaSelect();\n emblaApi.on(\"select\", onEmblaSelect);\n emblaApi.on(\"reInit\", onEmblaSelect);\n return () => {\n emblaApi.off(\"select\", onEmblaSelect);\n emblaApi.off(\"reInit\", onEmblaSelect);\n };\n }, [emblaApi, onEmblaSelect]);\n\n const selectedIndex = isCurtain ? curtain.selectedIndex : emblaSelectedIndex;\n\n const autoplayGate = useAutoplayGate({ autoplay, reducedMotion, rootRef });\n\n useEffect(() => {\n if (!autoplayGate.shouldAutoplay) return;\n if (!isCurtain && !emblaApi) return;\n const id = window.setInterval(() => {\n if (isCurtain) curtain.next();\n else emblaApi?.scrollNext();\n }, intervalMs);\n return () => window.clearInterval(id);\n // curtain 객체 전체가 아니라 쓰는 메서드만 의존성에 둔다 — 메서드는\n // useCallback 으로 안정화돼 있고(hero-carousel-curtain.tsx), 객체는 매 렌더\n // 새로 만들어져 전체를 넣으면 타이머가 매번 다시 걸린다.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoplayGate.shouldAutoplay, intervalMs, isCurtain, curtain.next, emblaApi]);\n\n const scrollPrev = useCallback(() => {\n if (isCurtain) {\n curtain.prev();\n return;\n }\n emblaApi?.scrollPrev(reducedMotion);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- 위와 같은 이유(안정화된 메서드만 의존).\n }, [isCurtain, curtain.prev, emblaApi, reducedMotion]);\n\n const scrollNext = useCallback(() => {\n if (isCurtain) {\n curtain.next();\n return;\n }\n emblaApi?.scrollNext(reducedMotion);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- 위와 같은 이유(안정화된 메서드만 의존).\n }, [isCurtain, curtain.next, emblaApi, reducedMotion]);\n\n const scrollTo = useCallback(\n (index: number) => {\n if (isCurtain) {\n curtain.goTo(index);\n return;\n }\n emblaApi?.scrollTo(index, reducedMotion);\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps -- 위와 같은 이유(안정화된 메서드만 의존).\n [isCurtain, curtain.goTo, emblaApi, reducedMotion],\n );\n\n const onKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n scrollPrev();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n scrollNext();\n }\n },\n [scrollPrev, scrollNext],\n );\n\n const selectedSlide = slides[selectedIndex];\n const hasSlidePanel = Boolean(\n selectedSlide &&\n (selectedSlide.eyebrow ||\n selectedSlide.title ||\n selectedSlide.subtitle ||\n selectedSlide.description ||\n selectedSlide.primaryCta ||\n selectedSlide.secondaryCta ||\n (selectedSlide.metrics && selectedSlide.metrics.length > 0)),\n );\n const hasTabLabels = slides.some((slide) => slide.tabLabel || slide.tabMeta);\n const showPlayPause = autoplay && !reducedMotion;\n\n const { getTabProps, onKeyDown: onTablistKeyDown } = useRovingTablist<HTMLButtonElement>({\n count: slides.length,\n selectedIndex,\n onSelect: scrollTo,\n orientation: \"horizontal\",\n });\n\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- WAI-ARIA 캐러셀 패턴. region 이 좌우 키·포인터를 받고 실제 이동은 안쪽 컨트롤이 담당한다.\n <div\n ref={rootRef}\n className=\"rt-tpl-hero-carousel\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n aria-label=\"배경 이미지 캐러셀\"\n tabIndex={0}\n onKeyDown={onKeyDown}\n data-reduced-motion={reducedMotion ? \"true\" : \"false\"}\n data-scrim={scrim ?? \"soft\"}\n data-transition={transition}\n onMouseEnter={autoplayGate.rootHandlers.onMouseEnter}\n onMouseLeave={autoplayGate.rootHandlers.onMouseLeave}\n onFocus={autoplayGate.rootHandlers.onFocus}\n onBlur={autoplayGate.rootHandlers.onBlur}\n >\n {isCurtain ? (\n <CurtainTrack\n slides={slides}\n selectedIndex={curtain.selectedIndex}\n leavingIndex={curtain.leavingIndex}\n />\n ) : (\n <SlideTrack\n viewportRef={emblaRef}\n slides={slides}\n selectedIndex={emblaSelectedIndex}\n />\n )}\n\n <button\n type=\"button\"\n className=\"rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev\"\n aria-label=\"이전 슬라이드\"\n onClick={scrollPrev}\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M15 5 8 12l7 7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n <button\n type=\"button\"\n className=\"rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next\"\n aria-label=\"다음 슬라이드\"\n onClick={scrollNext}\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"m9 5 7 7-7 7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n\n {hasSlidePanel && selectedSlide ? (\n <div\n key={isCurtain ? `panel-${selectedIndex}` : \"panel\"}\n className=\"rt-tpl-hero-carousel-slide-panel\"\n >\n {selectedSlide.eyebrow ? (\n <p className=\"rt-tpl-hero-carousel-slide-eyebrow\">\n {selectedSlide.eyebrow}\n </p>\n ) : null}\n {selectedSlide.title ? (\n <p className=\"rt-tpl-hero-carousel-slide-title\">\n {selectedSlide.title}\n </p>\n ) : null}\n {selectedSlide.subtitle ? (\n <p className=\"rt-tpl-hero-carousel-slide-subtitle\">\n {selectedSlide.subtitle}\n </p>\n ) : null}\n {selectedSlide.description ? (\n <p className=\"rt-tpl-hero-carousel-slide-desc\">\n {selectedSlide.description}\n </p>\n ) : null}\n {selectedSlide.metrics && selectedSlide.metrics.length > 0 ? (\n <dl className=\"rt-tpl-hero-carousel-slide-metrics\">\n {selectedSlide.metrics.map((metric) => (\n <div key={`${metric.value}-${metric.label}`}>\n <dt>{metric.label}</dt>\n <dd>\n {metric.value}\n {metric.unit ? <span>{metric.unit}</span> : null}\n </dd>\n {metric.source ? <dd>{metric.source}</dd> : null}\n </div>\n ))}\n </dl>\n ) : null}\n {selectedSlide.primaryCta || selectedSlide.secondaryCta ? (\n <div className=\"rt-tpl-hero-carousel-slide-actions\">\n {selectedSlide.primaryCta ? (\n <a\n className=\"rt-tpl-btn rt-tpl-btn--primary\"\n href={selectedSlide.primaryCta.href}\n data-track=\"cta_hero\"\n >\n {selectedSlide.primaryCta.label}\n </a>\n ) : null}\n {selectedSlide.secondaryCta ? (\n <a\n className=\"rt-tpl-btn rt-tpl-btn--secondary\"\n href={selectedSlide.secondaryCta.href}\n data-track=\"cta_hero\"\n >\n {selectedSlide.secondaryCta.label}\n </a>\n ) : null}\n </div>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"rt-tpl-hero-carousel-controls\">\n {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus -- WAI-ARIA 탭 패턴(roving tabindex). 초점은 개별 인디케이터가 갖고 컨테이너는 키 이벤트만 위임받는다. */}\n <div\n className={\n hasTabLabels\n ? \"rt-tpl-hero-carousel-dots rt-tpl-hero-carousel-dots--tabs\"\n : \"rt-tpl-hero-carousel-dots\"\n }\n role=\"tablist\"\n aria-label=\"슬라이드 선택\"\n onKeyDown={onTablistKeyDown}\n >\n {slides.map((_slide, index) => (\n <button\n key={index}\n {...getTabProps(index)}\n type=\"button\"\n role=\"tab\"\n className=\"rt-tpl-hero-carousel-dot\"\n aria-selected={index === selectedIndex}\n aria-label={`${index + 1}번 슬라이드로 이동`}\n data-active={index === selectedIndex ? \"true\" : \"false\"}\n onClick={() => scrollTo(index)}\n >\n {hasTabLabels ? (\n <>\n <span>{_slide.tabLabel ?? `${index + 1}`}</span>\n {_slide.tabMeta ? <small>{_slide.tabMeta}</small> : null}\n </>\n ) : null}\n </button>\n ))}\n </div>\n <div className=\"rt-tpl-hero-carousel-controls-right\">\n {showPlayPause ? (\n <button\n type=\"button\"\n className=\"rt-tpl-hero-carousel-playpause\"\n aria-pressed={autoplayGate.userPaused}\n aria-label={\n autoplayGate.userPaused ? \"자동 전환 재생\" : \"자동 전환 일시정지\"\n }\n onClick={autoplayGate.togglePaused}\n >\n {autoplayGate.userPaused ? <PlayIcon /> : <PauseIcon />}\n </button>\n ) : null}\n <p className=\"rt-tpl-hero-carousel-counter\" aria-hidden=\"true\">\n <span className=\"rt-tpl-hero-carousel-counter-current\">\n {selectedIndex + 1}\n </span>\n /\n <span className=\"rt-tpl-hero-carousel-counter-total\">{slides.length}</span>\n </p>\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * `curtain` transition — chambarun 이식(ADR-0086 P2 D3).\n * 모션 스펙 원본: chambarun mockup `assets/site.css` `@keyframes curtainReveal`\n * (clip-path 와이프) + `.curtain-sheen`(신 스윕) + `.hero-copy>*` 스태거, 엔진은\n * `assets/site.js` 36-85행 `show(n)`(is-active/is-leaving 상태 머신, 980ms 뒤\n * is-leaving 정리). 타이밍·이징·클립 경로는 원본 값을 그대로 유지하고 색은\n * 하드코딩 헥스 대신 `--rt-*` 토큰으로 치환했다(CSS, `templates.css` 참고).\n *\n * embla 를 쓰지 않는다 — embla 는 가로 스크롤 트랙 물리 이동이 전제라\n * clip-path 와이프(절대배치 스택)와 구조가 다르다. `hero-carousel-client.tsx`\n * 400줄 상한 분할.\n */\nimport { useCallback, useRef, useState } from \"react\";\nimport type { ReactElement } from \"react\";\n\nimport type { HeroCarouselSlideInput } from \"./hero-carousel-types.js\";\n\n/** chambarun site.js — is-leaving 클래스 정리 타이밍(커튼 리빌 .92s + 여유). */\nconst LEAVING_CLEAR_MS = 980;\n\nexport interface CurtainCarouselController {\n readonly selectedIndex: number;\n readonly leavingIndex: number | null;\n readonly goTo: (index: number) => void;\n readonly next: () => void;\n readonly prev: () => void;\n}\n\n/**\n * `curtain` 전용 인덱스 상태 머신 — chambarun `show(n)` 이식. `reducedMotion`\n * 이면 leaving 단계를 만들지 않는다(D4 — 커튼 대신 즉시 전환).\n */\nexport function useCurtainCarousel(\n slideCount: number,\n reducedMotion: boolean,\n): CurtainCarouselController {\n const [selectedIndex, setSelectedIndex] = useState(0);\n const [leavingIndex, setLeavingIndex] = useState<number | null>(null);\n const selectedRef = useRef(0);\n const runRef = useRef(0);\n const clearTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const goTo = useCallback(\n (nextRaw: number) => {\n if (slideCount < 1) return;\n const nextIndex = ((nextRaw % slideCount) + slideCount) % slideCount;\n const prevIndex = selectedRef.current;\n if (nextIndex === prevIndex) return;\n selectedRef.current = nextIndex;\n setSelectedIndex(nextIndex);\n clearTimeout(clearTimerRef.current);\n if (reducedMotion) {\n setLeavingIndex(null);\n return;\n }\n setLeavingIndex(prevIndex);\n runRef.current += 1;\n const runId = runRef.current;\n clearTimerRef.current = setTimeout(() => {\n if (runId === runRef.current) setLeavingIndex(null);\n }, LEAVING_CLEAR_MS);\n },\n [slideCount, reducedMotion],\n );\n\n const next = useCallback(() => goTo(selectedRef.current + 1), [goTo]);\n const prev = useCallback(() => goTo(selectedRef.current - 1), [goTo]);\n\n return { selectedIndex, leavingIndex, goTo, next, prev };\n}\n\nexport interface CurtainTrackProps {\n readonly slides: readonly HeroCarouselSlideInput[];\n readonly selectedIndex: number;\n readonly leavingIndex: number | null;\n}\n\n/**\n * 절대배치 스택 트랙(chambarun `.slide{position:absolute;inset:0}`) — 각\n * 슬라이드가 `data-active`/`data-leaving` 로 clip-path 와이프·오버랩을\n * 제어한다(CSS `templates.css` `[data-transition=\"curtain\"]` 스코프). 활성\n * 슬라이드에만 신 스윕 엘리먼트를 렌더(비활성 슬라이드는 DOM 비용 절감).\n */\nexport function CurtainTrack({\n slides,\n selectedIndex,\n leavingIndex,\n}: CurtainTrackProps): ReactElement {\n return (\n <div className=\"rt-tpl-hero-carousel-viewport\">\n <div className=\"rt-tpl-hero-carousel-track\">\n {slides.map((slide, index) => {\n const active = index === selectedIndex;\n const leaving = index === leavingIndex;\n return (\n <div\n key={`${slide.image}-${index}`}\n className=\"rt-tpl-hero-carousel-slide\"\n aria-roledescription=\"slide\"\n aria-label={`Slide ${index + 1} of ${slides.length}`}\n aria-hidden={active ? undefined : true}\n data-active={active ? \"true\" : \"false\"}\n data-leaving={leaving ? \"true\" : \"false\"}\n >\n <img\n className=\"rt-tpl-hero-carousel-img\"\n src={slide.image}\n alt={slide.alt}\n data-active={active ? \"true\" : \"false\"}\n loading={index === 0 ? \"eager\" : \"lazy\"}\n />\n {active ? (\n <span className=\"rt-tpl-hero-carousel-sheen\" aria-hidden=\"true\" />\n ) : null}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * `hero` `carousel` 공유 훅 — reduced-motion·document visibility·autoplay\n * 정지 사유 합성(D4 접근성 계약, ADR-0086 P2). `hero-carousel-client.tsx` 400줄\n * 상한 분할.\n */\nimport { useCallback, useState } from \"react\";\nimport type { FocusEvent, RefObject } from \"react\";\nimport { useEffect } from \"react\";\n\nexport function usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n setReduced(query.matches);\n const onChange = (event: MediaQueryListEvent) => setReduced(event.matches);\n query.addEventListener(\"change\", onChange);\n return () => query.removeEventListener(\"change\", onChange);\n }, []);\n return reduced;\n}\n\nexport function useDocumentVisible(): boolean {\n const [visible, setVisible] = useState(true);\n useEffect(() => {\n const onVisibility = () => setVisible(document.visibilityState === \"visible\");\n onVisibility();\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => document.removeEventListener(\"visibilitychange\", onVisibility);\n }, []);\n return visible;\n}\n\nexport interface AutoplayGate {\n /** 실제 autoplay 인터벌을 걸어도 되는지 — 모든 정지 사유의 합성 결과. */\n readonly shouldAutoplay: boolean;\n /** pause/재생 버튼이 반영하는 사용자 의도(수동 일시정지). */\n readonly userPaused: boolean;\n readonly togglePaused: () => void;\n /** 루트 엘리먼트에 그대로 스프레드 — hover/focus-within 일시정지. */\n readonly rootHandlers: {\n readonly onMouseEnter: () => void;\n readonly onMouseLeave: () => void;\n readonly onFocus: () => void;\n readonly onBlur: (event: FocusEvent<HTMLElement>) => void;\n };\n}\n\n/**\n * autoplay pause 사유 4종(D4): hover, focus-within, 사용자 버튼, 문서 비가시.\n * `reducedMotion` 은 autoplay 자체를 아예 시작하지 않는 별도 계약이라 이 훅\n * 밖(`autoplay && !reducedMotion`)에서 이미 걸러지지만, 재확인 차원에서 인자로\n * 받아 합성식에도 포함한다.\n */\nexport function useAutoplayGate(params: {\n readonly autoplay: boolean;\n readonly reducedMotion: boolean;\n readonly rootRef: RefObject<HTMLElement | null>;\n}): AutoplayGate {\n const { autoplay, reducedMotion, rootRef } = params;\n const tabVisible = useDocumentVisible();\n const [hovered, setHovered] = useState(false);\n const [focusedWithin, setFocusedWithin] = useState(false);\n const [userPaused, setUserPaused] = useState(false);\n\n const onBlur = useCallback(\n (event: FocusEvent<HTMLElement>) => {\n const next = event.relatedTarget as Node | null;\n if (!next || !rootRef.current?.contains(next)) {\n setFocusedWithin(false);\n }\n },\n [rootRef],\n );\n\n const shouldAutoplay =\n autoplay &&\n !reducedMotion &&\n tabVisible &&\n !userPaused &&\n !hovered &&\n !focusedWithin;\n\n return {\n shouldAutoplay,\n userPaused,\n togglePaused: () => setUserPaused((value) => !value),\n rootHandlers: {\n onMouseEnter: () => setHovered(true),\n onMouseLeave: () => setHovered(false),\n onFocus: () => setFocusedWithin(true),\n onBlur,\n },\n };\n}\n","\"use client\";\n\n/**\n * `slide` transition(기본값) 트랙 — 기존 embla 가로 스크롤 배경 전환, 회귀 없음.\n * `hero-carousel-client.tsx` 400줄 상한 분할(`curtain` transition 은\n * `hero-carousel-curtain.tsx`).\n */\nimport type { ReactElement } from \"react\";\nimport type { EmblaViewportRefType } from \"embla-carousel-react\";\n\nimport type { HeroCarouselSlideInput } from \"./hero-carousel-types.js\";\n\nexport interface SlideTrackProps {\n readonly viewportRef: EmblaViewportRefType;\n readonly slides: readonly HeroCarouselSlideInput[];\n readonly selectedIndex: number;\n}\n\nexport function SlideTrack({\n viewportRef,\n slides,\n selectedIndex,\n}: SlideTrackProps): ReactElement {\n return (\n <div className=\"rt-tpl-hero-carousel-viewport\" ref={viewportRef}>\n <div className=\"rt-tpl-hero-carousel-track\">\n {slides.map((slide, index) => (\n <div\n key={`${slide.image}-${index}`}\n className=\"rt-tpl-hero-carousel-slide\"\n aria-roledescription=\"slide\"\n aria-label={`Slide ${index + 1} of ${slides.length}`}\n aria-hidden={index === selectedIndex ? undefined : true}\n >\n <img\n className=\"rt-tpl-hero-carousel-img\"\n src={slide.image}\n alt={slide.alt}\n data-active={index === selectedIndex ? \"true\" : \"false\"}\n loading={index === 0 ? \"eager\" : \"lazy\"}\n />\n </div>\n ))}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","useCallback","useState","jsx","jsx","jsxs","useRef","useState","useCallback","useEffect"]}
@@ -50,11 +50,55 @@ function collectCollectionCategories(posts, collections, key) {
50
50
  return collectCategoriesWithCounts(owned).filter((c) => allowed.has(c.slug));
51
51
  }
52
52
 
53
+ // src/routes-shared.ts
54
+ import { fetchPosts } from "@roottale/cms-client/server";
55
+ import { resolvePostCollection as resolvePostCollection2, resolvePostUrl as resolvePostUrl2 } from "@roottale/cms-core";
56
+ async function resolveCollectionsResult(input) {
57
+ if (typeof input === "function") {
58
+ try {
59
+ return { collections: await input(), failed: false };
60
+ } catch (err) {
61
+ console.error(
62
+ "[@roottale/cms-renderer-next/routes] collections resolver \uC2E4\uD328:",
63
+ err
64
+ );
65
+ return { collections: void 0, failed: true };
66
+ }
67
+ }
68
+ return { collections: input, failed: false };
69
+ }
70
+ async function resolveCollections(input) {
71
+ return (await resolveCollectionsResult(input)).collections;
72
+ }
73
+ function trimSlash(u) {
74
+ return u.replace(/\/+$/, "");
75
+ }
76
+ async function safeFetchPosts(config, limit) {
77
+ try {
78
+ const page = await fetchPosts({
79
+ apiKey: config.apiKey,
80
+ baseUrl: config.apiBase,
81
+ type: "post",
82
+ limit
83
+ });
84
+ return page.items;
85
+ } catch (err) {
86
+ console.error("[@roottale/cms-renderer-next/routes] fetchPosts \uC2E4\uD328:", err);
87
+ return [];
88
+ }
89
+ }
90
+
53
91
  export {
54
92
  collectCategories,
55
93
  collectCategoriesWithCounts,
56
94
  filterPostsByCategory,
57
95
  defaultCategoryHref,
58
- collectCollectionCategories
96
+ collectCollectionCategories,
97
+ resolveCollectionsResult,
98
+ resolveCollections,
99
+ trimSlash,
100
+ safeFetchPosts,
101
+ resolvePostCollection2 as resolvePostCollection,
102
+ resolvePostUrl2 as resolvePostUrl
59
103
  };
60
- //# sourceMappingURL=chunk-FUA5H6JE.js.map
104
+ //# sourceMappingURL=chunk-U2IM6OCQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blog-filter.ts","../src/routes-shared.ts"],"sourcesContent":["/**\n * `RootTaleBlogList` 카테고리 필터 — 순수 헬퍼(서버 필터, client JS 불요).\n *\n * SSR-only 정책 유지: 필터는 `?category=<slug>` URL 파라미터로 동작하고(칩 =\n * 서버 렌더 `<a>` 링크), consumer 가 searchParams 의 category 를 `activeCategory`\n * 로 주입한다. hydration 없이 SEO 친화 + 공유 가능한 URL.\n */\n\nimport type { CmsPostContent, CmsTermRef } from \"@roottale/cms-client/server\";\n\nexport interface BlogCategory {\n name: string;\n slug: string;\n}\n\nexport interface BlogCategoryWithCount extends BlogCategory {\n count: number;\n}\n\nfunction categoryTerms(post: CmsPostContent): CmsTermRef[] {\n return (post.terms ?? []).filter((t) => t.taxonomy === \"category\");\n}\n\n/**\n * 글 목록에 등장하는 카테고리를 slug 기준 unique 로 수집. 서버가 보내는 정렬\n * (category → name asc)을 보존하기 위해 처음 등장 순서를 유지한다.\n */\nexport function collectCategories(posts: CmsPostContent[]): BlogCategory[] {\n return collectCategoriesWithCounts(posts).map(({ name, slug }) => ({\n name,\n slug,\n }));\n}\n\nexport function collectCategoriesWithCounts(\n posts: CmsPostContent[],\n): BlogCategoryWithCount[] {\n const bySlug = new Map<string, BlogCategoryWithCount>();\n const out: BlogCategoryWithCount[] = [];\n for (const post of posts) {\n for (const t of categoryTerms(post)) {\n const current = bySlug.get(t.slug);\n if (current) {\n current.count += 1;\n } else {\n const category = { name: t.name, slug: t.slug, count: 1 };\n bySlug.set(t.slug, category);\n out.push(category);\n }\n }\n }\n return out;\n}\n\n/** activeSlug 가 비면 전체 반환. 아니면 그 카테고리를 가진 글만. */\nexport function filterPostsByCategory(\n posts: CmsPostContent[],\n activeSlug: string | null | undefined,\n): CmsPostContent[] {\n if (!activeSlug) return posts;\n return posts.filter((p) => categoryTerms(p).some((t) => t.slug === activeSlug));\n}\n\n/** 기본 카테고리 링크: 전체 → `/blog`, 특정 → `/blog/categories/<slug>`. */\nexport function defaultCategoryHref(slug: string | null): string {\n return slug ? `/blog/categories/${encodeURIComponent(slug)}` : \"/blog\";\n}\n\n// ── Collections (ADR-0060) — cms-core 라우팅 프리미티브 재export + 렌더 헬퍼 ──────\n//\n// 라우팅 코어(RouteCollection/resolvePostCollection/resolvePostUrl/postsForCollection/\n// validateCollections)는 `@roottale/cms-core` 에 있어 next/astro 가 공유한다\n// (ADR-0029 parity — codex consult `019ef577` 반영). 여기서는 next 소비자 편의를\n// 위해 재export + CmsPostContent 기반 스트림 스코프 카테고리 집계만 추가한다.\nimport {\n type RouteCollection,\n postsForCollection,\n} from \"@roottale/cms-core\";\n\nexport {\n type RouteCollection,\n type RoutablePost,\n isInCollectionMode,\n resolvePostCollection,\n resolvePostUrl,\n postsForCollection,\n postsInAnyCollection,\n validateCollections,\n} from \"@roottale/cms-core\";\n\n/**\n * 특정 collection 안에서 등장하는 카테고리 집계 — 그 스트림의 카테고리 칩/아카이브용.\n * **전체 정렬 리스트로 first-wins 해석**해 그 스트림에 최종 귀속된 글만 대상(한 글이\n * 여러 스트림 리스트에 중복되는 것을 방지 — codex 지적).\n */\nexport function collectCollectionCategories(\n posts: CmsPostContent[],\n collections: readonly RouteCollection[],\n key: string,\n): BlogCategoryWithCount[] {\n const owned = postsForCollection(posts, collections, key);\n const collection = collections.find((c) => c.key === key);\n const allowed = new Set(collection?.categories ?? []);\n return collectCategoriesWithCounts(owned).filter((c) => allowed.has(c.slug));\n}\n","/**\n * `routes` 공통 — config 타입, collections resolve, fetch 헬퍼. feed/sitemap/llms/\n * revalidate/og 모듈이 공유한다(순환 import 회피). 공개 표면은 `routes.ts` 배럴.\n */\n\nimport { fetchPosts, type CmsPostContent } from \"@roottale/cms-client/server\";\nimport type { RouteCollection } from \"@roottale/cms-core\";\n\nexport type { RouteCollection };\n// 소비자 가드용 재export (`/blog/[slug]` 에서 스트림 누출 차단 등) — ADR-0060.\nexport { resolvePostCollection, resolvePostUrl } from \"@roottale/cms-core\";\n\nexport interface RootTaleSiteConfig {\n /** `rtlk_cust_*` — 블로그 조회와 같은 키. 서버에서만(절대 브라우저 금지). */\n apiKey: string;\n /** API base 오버라이드. 기본 `https://api.roottale.com`. */\n apiBase?: string;\n /** 사이트 절대 URL (뒤 슬래시 없이). 예: `https://kjmtax.roottale.app`. */\n siteUrl: string;\n /** 피드/사이트 제목. */\n title: string;\n description?: string;\n /** 기본 \"ko-KR\". */\n language?: string;\n /** 블로그 base path. 기본 \"/blog\". 레거시 단일 스트림 — `collections` 미사용 시. */\n blogBasePath?: string;\n /**\n * 다중 스트림 라우팅 (ADR-0060). **길이 > 0 이면 collection 모드** — feed/sitemap/\n * llms.txt 가 글을 소속 스트림 basePath 로 라우팅하고, 어느 스트림에도 안 속한 글은\n * 제외한다. 비우거나 미전달 = 레거시 단일 `blogBasePath`(/blog) 동작(하위호환).\n * 선언 순서 = 우선순위(first-wins).\n *\n * 배열 또는 **async resolver**(`() => Promise<RouteCollection[]>`)를 받는다 — 후자는\n * 사이트가 `@roottale/cms-client` `fetchCollections()` 로 admin 설정을 가져올 때 쓴다.\n * resolver 가 throw 하면 fail-soft(레거시 단일 /blog).\n */\n collections?: CollectionsInput;\n}\n\n/** collections 입력 — 정적 배열 또는 async resolver(admin 설정 fetch 용). */\nexport type CollectionsInput =\n | RouteCollection[]\n | (() => RouteCollection[] | Promise<RouteCollection[]>);\n\n/** resolve 결과 — `failed` 로 \"resolver throw\" 를 \"collections 미설정\" 과 구분한다. */\nexport async function resolveCollectionsResult(\n input: CollectionsInput | undefined,\n): Promise<{ collections: RouteCollection[] | undefined; failed: boolean }> {\n if (typeof input === \"function\") {\n try {\n return { collections: await input(), failed: false };\n } catch (err) {\n console.error(\n \"[@roottale/cms-renderer-next/routes] collections resolver 실패:\",\n err,\n );\n return { collections: undefined, failed: true };\n }\n }\n return { collections: input, failed: false };\n}\n\n/**\n * resolver 면 호출(실패 시 undefined → 레거시), 배열이면 그대로. GET 팩토리(feed/\n * sitemap/llms)는 always-200 철학상 실패를 레거시 /blog 로 흡수 — revalidate 만\n * `resolveCollectionsResult` 로 실패를 구분해 under-invalidation 을 막는다.\n */\nexport async function resolveCollections(\n input: CollectionsInput | undefined,\n): Promise<RouteCollection[] | undefined> {\n return (await resolveCollectionsResult(input)).collections;\n}\n\nexport function trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nexport async function safeFetchPosts(\n config: RootTaleSiteConfig,\n limit: number,\n): Promise<CmsPostContent[]> {\n try {\n const page = await fetchPosts({\n apiKey: config.apiKey,\n baseUrl: config.apiBase,\n type: \"post\",\n limit,\n });\n return page.items;\n } catch (err) {\n // 빌드/런타임 fetch 실패 시 빈 목록 — 라우트는 항상 정상 응답(404 방지).\n console.error(\"[@roottale/cms-renderer-next/routes] fetchPosts 실패:\", err);\n return [];\n }\n}\n"],"mappings":";AA0EA;AAAA,EAEE;AAAA,OACK;AAEP;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA,sBAAAA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AArEP,SAAS,cAAc,MAAoC;AACzD,UAAQ,KAAK,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,UAAU;AACnE;AAMO,SAAS,kBAAkB,OAAyC;AACzE,SAAO,4BAA4B,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO;AAAA,IACjE;AAAA,IACA;AAAA,EACF,EAAE;AACJ;AAEO,SAAS,4BACd,OACyB;AACzB,QAAM,SAAS,oBAAI,IAAmC;AACtD,QAAM,MAA+B,CAAC;AACtC,aAAW,QAAQ,OAAO;AACxB,eAAW,KAAK,cAAc,IAAI,GAAG;AACnC,YAAM,UAAU,OAAO,IAAI,EAAE,IAAI;AACjC,UAAI,SAAS;AACX,gBAAQ,SAAS;AAAA,MACnB,OAAO;AACL,cAAM,WAAW,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE;AACxD,eAAO,IAAI,EAAE,MAAM,QAAQ;AAC3B,YAAI,KAAK,QAAQ;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,sBACd,OACA,YACkB;AAClB,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,MAAM,OAAO,CAAC,MAAM,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU,CAAC;AAChF;AAGO,SAAS,oBAAoB,MAA6B;AAC/D,SAAO,OAAO,oBAAoB,mBAAmB,IAAI,CAAC,KAAK;AACjE;AA6BO,SAAS,4BACd,OACA,aACA,KACyB;AACzB,QAAM,QAAQ,mBAAmB,OAAO,aAAa,GAAG;AACxD,QAAM,aAAa,YAAY,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AACxD,QAAM,UAAU,IAAI,IAAI,YAAY,cAAc,CAAC,CAAC;AACpD,SAAO,4BAA4B,KAAK,EAAE,OAAO,CAAC,MAAM,QAAQ,IAAI,EAAE,IAAI,CAAC;AAC7E;;;ACnGA,SAAS,kBAAuC;AAKhD,SAAS,yBAAAC,wBAAuB,kBAAAC,uBAAsB;AAmCtD,eAAsB,yBACpB,OAC0E;AAC1E,MAAI,OAAO,UAAU,YAAY;AAC/B,QAAI;AACF,aAAO,EAAE,aAAa,MAAM,MAAM,GAAG,QAAQ,MAAM;AAAA,IACrD,SAAS,KAAK;AACZ,cAAQ;AAAA,QACN;AAAA,QACA;AAAA,MACF;AACA,aAAO,EAAE,aAAa,QAAW,QAAQ,KAAK;AAAA,IAChD;AAAA,EACF;AACA,SAAO,EAAE,aAAa,OAAO,QAAQ,MAAM;AAC7C;AAOA,eAAsB,mBACpB,OACwC;AACxC,UAAQ,MAAM,yBAAyB,KAAK,GAAG;AACjD;AAEO,SAAS,UAAU,GAAmB;AAC3C,SAAO,EAAE,QAAQ,QAAQ,EAAE;AAC7B;AAEA,eAAsB,eACpB,QACA,OAC2B;AAC3B,MAAI;AACF,UAAM,OAAO,MAAM,WAAW;AAAA,MAC5B,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AACD,WAAO,KAAK;AAAA,EACd,SAAS,KAAK;AAEZ,YAAQ,MAAM,iEAAuD,GAAG;AACxE,WAAO,CAAC;AAAA,EACV;AACF;","names":["postsForCollection","resolvePostCollection","resolvePostUrl"]}