@roottale/cms-renderer-next 0.52.0 → 0.53.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 (76) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +23 -2
  3. package/dist/{chunk-U2IM6OCQ.js → chunk-HM5VP7SR.js} +1 -1
  4. package/dist/{chunk-U2IM6OCQ.js.map → chunk-HM5VP7SR.js.map} +1 -1
  5. package/dist/chunk-KSPPYX22.js +14 -0
  6. package/dist/chunk-KSPPYX22.js.map +1 -0
  7. package/dist/cms-public.css +38 -0
  8. package/dist/routes.d.ts +9 -0
  9. package/dist/routes.js +18 -5
  10. package/dist/routes.js.map +1 -1
  11. package/dist/section-runtime.d.ts +46 -0
  12. package/dist/section-runtime.js +9 -0
  13. package/dist/server.d.ts +5 -3
  14. package/dist/server.js +1679 -34
  15. package/dist/server.js.map +1 -1
  16. package/package.json +7 -23
  17. package/dist/before-after-client.d.ts +0 -15
  18. package/dist/before-after-client.js +0 -8
  19. package/dist/chunk-27RT6B3O.js +0 -93
  20. package/dist/chunk-27RT6B3O.js.map +0 -1
  21. package/dist/chunk-2QU4JU46.js +0 -3927
  22. package/dist/chunk-2QU4JU46.js.map +0 -1
  23. package/dist/chunk-4RQ3SEDL.js +0 -106
  24. package/dist/chunk-4RQ3SEDL.js.map +0 -1
  25. package/dist/chunk-6GQCFFHU.js +0 -540
  26. package/dist/chunk-6GQCFFHU.js.map +0 -1
  27. package/dist/chunk-H46JUIHZ.js +0 -79
  28. package/dist/chunk-H46JUIHZ.js.map +0 -1
  29. package/dist/chunk-K62RR4UQ.js +0 -53
  30. package/dist/chunk-K62RR4UQ.js.map +0 -1
  31. package/dist/chunk-LEHHSI6G.js +0 -64
  32. package/dist/chunk-LEHHSI6G.js.map +0 -1
  33. package/dist/chunk-QYNEOFL7.js +0 -418
  34. package/dist/chunk-QYNEOFL7.js.map +0 -1
  35. package/dist/chunk-U5AKACPN.js +0 -113
  36. package/dist/chunk-U5AKACPN.js.map +0 -1
  37. package/dist/chunk-UM37DOIK.js +0 -961
  38. package/dist/chunk-UM37DOIK.js.map +0 -1
  39. package/dist/chunk-YLSQAT6B.js +0 -59
  40. package/dist/chunk-YLSQAT6B.js.map +0 -1
  41. package/dist/chunk-ZL2NA3QT.js +0 -313
  42. package/dist/chunk-ZL2NA3QT.js.map +0 -1
  43. package/dist/concerns-client.d.ts +0 -12
  44. package/dist/concerns-client.js +0 -8
  45. package/dist/concerns-client.js.map +0 -1
  46. package/dist/contact-map-client.d.ts +0 -27
  47. package/dist/contact-map-client.js +0 -8
  48. package/dist/contact-map-client.js.map +0 -1
  49. package/dist/expertise-client.d.ts +0 -16
  50. package/dist/expertise-client.js +0 -9
  51. package/dist/expertise-client.js.map +0 -1
  52. package/dist/hero-carousel-client.d.ts +0 -52
  53. package/dist/hero-carousel-client.js +0 -9
  54. package/dist/hero-carousel-client.js.map +0 -1
  55. package/dist/seo-checks.d.ts +0 -87
  56. package/dist/seo-checks.js +0 -27
  57. package/dist/seo-checks.js.map +0 -1
  58. package/dist/site-config-DNpvlqA5.d.ts +0 -3961
  59. package/dist/spaces-client.d.ts +0 -10
  60. package/dist/spaces-client.js +0 -8
  61. package/dist/spaces-client.js.map +0 -1
  62. package/dist/team-expanding-stage-client.d.ts +0 -35
  63. package/dist/team-expanding-stage-client.js +0 -8
  64. package/dist/team-expanding-stage-client.js.map +0 -1
  65. package/dist/templates.css +0 -4145
  66. package/dist/templates.d.ts +0 -171
  67. package/dist/templates.js +0 -291
  68. package/dist/templates.js.map +0 -1
  69. package/dist/theme-css-vars-CD3jcEkp.d.ts +0 -7
  70. package/dist/ui-react-motion-client.d.ts +0 -76
  71. package/dist/ui-react-motion-client.js +0 -16
  72. package/dist/ui-react-motion-client.js.map +0 -1
  73. package/dist/validate.d.ts +0 -104
  74. package/dist/validate.js +0 -1204
  75. package/dist/validate.js.map +0 -1
  76. /package/dist/{before-after-client.js.map → section-runtime.js.map} +0 -0
@@ -1,106 +0,0 @@
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
@@ -1 +0,0 @@
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":[]}
@@ -1,540 +0,0 @@
1
- // src/templates/seo-checks.ts
2
- var BLOCK_TYPES = /* @__PURE__ */ new Set([
3
- "paragraph",
4
- "heading",
5
- "listItem",
6
- "blockquote",
7
- "codeBlock"
8
- ]);
9
- function walkNodes(node, visit) {
10
- if (!node || typeof node !== "object") return;
11
- visit(node);
12
- if (Array.isArray(node.content)) {
13
- for (const child of node.content) walkNodes(child, visit);
14
- }
15
- }
16
- function collectNodes(node, predicate) {
17
- const out = [];
18
- walkNodes(node, (n) => {
19
- if (predicate(n)) out.push(n);
20
- });
21
- return out;
22
- }
23
- function extractText(node) {
24
- if (!node || typeof node !== "object") return "";
25
- if (node.type === "text") return typeof node.text === "string" ? node.text : "";
26
- const inner = Array.isArray(node.content) ? node.content.map((c) => extractText(c)).join("") : "";
27
- return BLOCK_TYPES.has(node.type ?? "") ? `${inner}
28
- ` : inner;
29
- }
30
- function countBodyChars(node) {
31
- return extractText(node).replace(/\s+/g, "").length;
32
- }
33
- var TITLE_MIN = 8;
34
- var DESC_MIN = 50;
35
- var BODY_GOOD = 800;
36
- var SLUG_MAX = 60;
37
- var DEFAULT_SEO_THRESHOLDS = {
38
- titleMaxLen: 30,
39
- descMaxLen: 90,
40
- bodyMinLen: 300
41
- };
42
- function checkTitle(input, thresholds) {
43
- const display = input.seoTitle.trim() || input.title.trim();
44
- const len = display.length;
45
- const max = thresholds.titleMaxLen;
46
- const label = "\uC81C\uBAA9 \uAE38\uC774";
47
- if (len === 0) {
48
- return {
49
- id: "title-length",
50
- level: "bad",
51
- label,
52
- hint: "\uC81C\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uAC80\uC0C9\uACB0\uACFC\uC758 \uCCAB\uC778\uC0C1\uC774\uBBC0\uB85C \uAF2D \uC791\uC131\uD558\uC138\uC694."
53
- };
54
- }
55
- if (len >= TITLE_MIN && len <= max) {
56
- return {
57
- id: "title-length",
58
- level: "good",
59
- label,
60
- hint: `${len}\uC790 \u2014 \uD55C\uAD6D\uC5B4 \uAC80\uC0C9\uACB0\uACFC\uC5D0 \uC798\uB9AC\uC9C0 \uC54A\uB294 \uAE38\uC774\uC608\uC694 (${max}\uC790 \uAE30\uC900). \uBA54\uC778 \uD0A4\uC6CC\uB4DC\uB294 \uC67C\uCABD\uC5D0.`
61
- };
62
- }
63
- return {
64
- id: "title-length",
65
- level: "warn",
66
- label,
67
- hint: len < TITLE_MIN ? `${len}\uC790 \u2014 \uB108\uBB34 \uC9E7\uC73C\uBA74 \uB0B4\uC6A9\uC744 \uC804\uB2EC\uD558\uAE30 \uC5B4\uB824\uC6CC\uC694. ${TITLE_MIN}~${max}\uC790\uB97C \uAD8C\uC7A5\uD574\uC694.` : `${len}\uC790 \u2014 \uD55C\uAD6D\uC5B4 \uAC80\uC0C9\uACB0\uACFC\uB294 \uC57D ${max}\uC790(\uACF5\uBC31 \uD3EC\uD568 ${max + 5}\uC790)\uAE4C\uC9C0 \uD45C\uC2DC\uB3FC\uC694. \uBA54\uC778 \uD0A4\uC6CC\uB4DC\uB97C \uC67C\uCABD\uC5D0 \uB450\uACE0 \uC904\uC5EC\uBCF4\uC138\uC694.`
68
- };
69
- }
70
- function checkDescription(input, thresholds) {
71
- const desc = input.seoDescription.trim() || input.excerpt.trim();
72
- const len = desc.length;
73
- const max = thresholds.descMaxLen;
74
- const label = "\uBA54\uD0C0 \uC124\uBA85";
75
- if (len === 0) {
76
- return {
77
- id: "meta-description",
78
- level: "warn",
79
- label,
80
- hint: "\uBA54\uD0C0 \uC124\uBA85\xB7\uC694\uC57D\uC774 \uBE44\uC5B4 \uC788\uC5B4\uC694. \uC5C6\uC73C\uBA74 \uAC80\uC0C9\uC5D4\uC9C4\uC774 \uBCF8\uBB38\uC5D0\uC11C \uBC1C\uCDCC\uD569\uB2C8\uB2E4."
81
- };
82
- }
83
- if (len >= DESC_MIN && len <= max) {
84
- return {
85
- id: "meta-description",
86
- level: "good",
87
- label,
88
- hint: `${len}\uC790 \u2014 \uD55C\uAD6D\uC5B4 \uAC80\uC0C9\uACB0\uACFC \uC124\uBA85\uC73C\uB85C \uC801\uB2F9\uD55C \uAE38\uC774\uC608\uC694 (${max}\uC790 \uAE30\uC900). \uD0A4\uC6CC\uB4DC\uB294 2\uD68C\uAE4C\uC9C0, \uD615\uC6A9\uC0AC\uB85C \uD074\uB9AD \uC720\uB3C4.`
89
- };
90
- }
91
- return {
92
- id: "meta-description",
93
- level: "warn",
94
- label,
95
- hint: len < DESC_MIN ? `${len}\uC790 \u2014 \uC870\uAE08 \uB354 \uCC44\uC6B0\uBA74 \uD074\uB9AD\uC744 \uC720\uB3C4\uD558\uAE30 \uC88B\uC544\uC694 (${DESC_MIN}~${max}\uC790 \uAD8C\uC7A5).` : `${len}\uC790 \u2014 \uD55C\uAD6D\uC5B4 \uAC80\uC0C9\uACB0\uACFC\uB294 \uC57D ${max}\uC790(\uACF5\uBC31 \uD3EC\uD568 ${max + 10}\uC790)\uAE4C\uC9C0 \uD45C\uC2DC\uB3FC\uC694. ${max}\uC790 \uC774\uB0B4 \uAD8C\uC7A5.`
96
- };
97
- }
98
- function checkBodyLength(chars, thresholds) {
99
- const min = thresholds.bodyMinLen;
100
- const label = "\uBCF8\uBB38 \uBD84\uB7C9";
101
- if (chars >= BODY_GOOD) {
102
- return {
103
- id: "body-length",
104
- level: "good",
105
- label,
106
- hint: `\uC57D ${chars}\uC790 \u2014 \uC8FC\uC81C\uB97C \uCDA9\uBD84\uD788 \uB2E4\uB8F0 \uC218 \uC788\uB294 \uBD84\uB7C9\uC774\uC5D0\uC694.`
107
- };
108
- }
109
- if (chars < min) {
110
- return {
111
- id: "body-length",
112
- level: "warn",
113
- label,
114
- hint: `\uC57D ${chars}\uC790 \u2014 \uB108\uBB34 \uC9E7\uC73C\uBA74 \uAC80\uC0C9\uC5D4\uC9C4\uC774 \uC8FC\uC81C\uB97C \uD30C\uC545\uD558\uAE30 \uC5B4\uB824\uC6CC\uC694 (${min}\uC790 \uC774\uC0C1 \uAD8C\uC7A5).`
115
- };
116
- }
117
- return {
118
- id: "body-length",
119
- level: "warn",
120
- label,
121
- hint: `\uC57D ${chars}\uC790 \u2014 ${BODY_GOOD}\uC790 \uC774\uC0C1\uC73C\uB85C \uAE4A\uC774\uB97C \uB354\uD558\uBA74 \uB354 \uC88B\uC544\uC694.`
122
- };
123
- }
124
- function checkHeadings(body, chars) {
125
- const label = "\uC18C\uC81C\uBAA9 \uAD6C\uC870";
126
- const headings = collectNodes(body, (n) => n.type === "heading");
127
- const levels = headings.map((h) => {
128
- const lv = Number(h.attrs?.level ?? 2);
129
- return Number.isFinite(lv) ? lv : 2;
130
- });
131
- const hasH2orH3 = levels.some((lv) => lv === 2 || lv === 3);
132
- if (chars >= BODY_GOOD && !hasH2orH3) {
133
- return {
134
- id: "heading-structure",
135
- level: "warn",
136
- label,
137
- hint: "\uAE34 \uAE00\uC778\uB370 \uC18C\uC81C\uBAA9(h2/h3)\uC774 \uC5C6\uC5B4\uC694. \uAD6C\uD68D\uC744 \uB098\uB204\uBA74 \uB3C5\uC790\xB7\uAC80\uC0C9\uC5D4\uC9C4 \uBAA8\uB450 \uAD6C\uC870\uB97C \uD30C\uC545\uD558\uAE30 \uC26C\uC6CC\uC694."
138
- };
139
- }
140
- for (let i = 1; i < levels.length; i += 1) {
141
- const prevLevel = levels[i - 1];
142
- const currLevel = levels[i];
143
- if (prevLevel === void 0 || currLevel === void 0) continue;
144
- if (currLevel - prevLevel > 1) {
145
- return {
146
- id: "heading-structure",
147
- level: "warn",
148
- label,
149
- hint: `\uC18C\uC81C\uBAA9 \uB808\uBCA8\uC744 \uAC74\uB108\uB6F0\uC5C8\uC5B4\uC694 (h${prevLevel}\u2192h${currLevel}). \uB2E8\uACC4\uC801\uC73C\uB85C \uB0B4\uB824\uAC00\uBA74 \uBB38\uC11C \uAD6C\uC870\uAC00 \uBA85\uD655\uD574\uC838\uC694.`
150
- };
151
- }
152
- }
153
- return {
154
- id: "heading-structure",
155
- level: "good",
156
- label,
157
- hint: headings.length > 0 ? `\uC18C\uC81C\uBAA9 ${headings.length}\uAC1C \u2014 \uAD6C\uC870\uAC00 \uB2E8\uACC4\uC801\uC73C\uB85C \uC798 \uC7A1\uD600 \uC788\uC5B4\uC694.` : "\uC544\uC9C1 \uC9E7\uC740 \uAE00\uC774\uB77C \uC18C\uC81C\uBAA9 \uC5C6\uC774\uB3C4 \uAD1C\uCC2E\uC544\uC694."
158
- };
159
- }
160
- function checkImageAlt(body) {
161
- const label = "\uC774\uBBF8\uC9C0 \uB300\uCCB4\uD14D\uC2A4\uD2B8";
162
- const images = collectNodes(body, (n) => n.type === "image");
163
- const missing = images.filter((img) => {
164
- const alt = img.attrs?.alt;
165
- return typeof alt !== "string" || alt.trim() === "";
166
- });
167
- if (missing.length > 0) {
168
- return {
169
- id: "image-alt",
170
- level: "warn",
171
- label,
172
- hint: `\uC774\uBBF8\uC9C0 ${missing.length}\uAC1C\uC5D0 \uB300\uCCB4\uD14D\uC2A4\uD2B8(alt)\uAC00 \uC5C6\uC5B4\uC694. \uC774\uBBF8\uC9C0 \uAC80\uC0C9\xB7\uC2A4\uD06C\uB9B0\uB9AC\uB354 \uC811\uADFC\uC131\uC5D0 \uD544\uC694\uD574\uC694.`
173
- };
174
- }
175
- return {
176
- id: "image-alt",
177
- level: "good",
178
- label,
179
- hint: images.length > 0 ? `\uC774\uBBF8\uC9C0 ${images.length}\uAC1C \uBAA8\uB450 \uB300\uCCB4\uD14D\uC2A4\uD2B8\uAC00 \uC788\uC5B4\uC694.` : "\uBCF8\uBB38\uC5D0 \uC774\uBBF8\uC9C0\uAC00 \uC5C6\uC5B4\uC694 \u2014 \uCD94\uAC00\uD55C\uB2E4\uBA74 \uB300\uCCB4\uD14D\uC2A4\uD2B8\uB97C \uD568\uAED8 \uB123\uC5B4 \uC8FC\uC138\uC694."
180
- };
181
- }
182
- function checkFeaturedImage(input) {
183
- const label = "\uB300\uD45C \uC774\uBBF8\uC9C0";
184
- if (!input.hasFeaturedImage) {
185
- return {
186
- id: "featured-image",
187
- level: "warn",
188
- label,
189
- hint: "\uB300\uD45C \uC774\uBBF8\uC9C0\uAC00 \uC5C6\uC5B4\uC694. SNS \uACF5\uC720 \uCE74\uB4DC\uC640 \uAC80\uC0C9 \uC378\uB124\uC77C\uC5D0 \uC4F0\uC5EC\uC694."
190
- };
191
- }
192
- return {
193
- id: "featured-image",
194
- level: "good",
195
- label,
196
- hint: "\uB300\uD45C \uC774\uBBF8\uC9C0\uAC00 \uC124\uC815\uB418\uC5B4 \uC788\uC5B4\uC694."
197
- };
198
- }
199
- function checkLinks(body) {
200
- const label = "\uB9C1\uD06C";
201
- const linked = collectNodes(
202
- body,
203
- (n) => Array.isArray(n.marks) ? n.marks.some((m) => m.type === "link") : false
204
- );
205
- if (linked.length === 0) {
206
- return {
207
- id: "links",
208
- level: "warn",
209
- label,
210
- // W4-5 PR 3 — 내부링크 제안 패널(에디터 사이드바)로 안내 (§3③).
211
- hint: "\uBCF8\uBB38\uC5D0 \uB9C1\uD06C\uAC00 \uC5C6\uC5B4\uC694. \uC544\uB798 '\uB9C1\uD06C \uC81C\uC548' \uD328\uB110\uC5D0\uC11C \uCD94\uCC9C \uAE00\uC744 \uD655\uC778\uD574\uBCF4\uC138\uC694."
212
- };
213
- }
214
- return {
215
- id: "links",
216
- level: "good",
217
- label,
218
- hint: `\uBCF8\uBB38\uC5D0 \uB9C1\uD06C ${linked.length}\uAC1C \u2014 \uAD00\uB828 \uAE00\xB7\uCD9C\uCC98 \uC5F0\uACB0\uC774 \uB418\uC5B4 \uC788\uC5B4\uC694.`
219
- };
220
- }
221
- function checkSlug(input) {
222
- const label = "Slug \uAE38\uC774";
223
- const len = input.slug.length;
224
- if (len > SLUG_MAX) {
225
- return {
226
- id: "slug-length",
227
- level: "warn",
228
- label,
229
- hint: `${len}\uC790 \u2014 URL \uC774 \uAE38\uBA74 \uACF5\uC720\xB7\uAC00\uB3C5\uC131\uC774 \uB5A8\uC5B4\uC838\uC694 (${SLUG_MAX}\uC790 \uC774\uB0B4 \uAD8C\uC7A5).`
230
- };
231
- }
232
- return {
233
- id: "slug-length",
234
- level: "good",
235
- label,
236
- hint: "URL \uB85C \uC4F0\uAE30 \uC801\uB2F9\uD55C \uAE38\uC774\uC608\uC694."
237
- };
238
- }
239
- function checkIntro(body) {
240
- const label = "\uB3C4\uC785\uBD80";
241
- const first = Array.isArray(body?.content) ? body.content[0] : void 0;
242
- if (!first) {
243
- return {
244
- id: "intro",
245
- level: "warn",
246
- label,
247
- hint: "\uBCF8\uBB38\uC774 \uBE44\uC5B4 \uC788\uC5B4\uC694. \uCCAB \uBB38\uB2E8\uC5D0 \uAE00\uC758 \uD575\uC2EC \uC694\uC57D\uC744 \uB2F4\uC544 \uC8FC\uC138\uC694."
248
- };
249
- }
250
- if (first.type === "image" || first.type === "embed") {
251
- return {
252
- id: "intro",
253
- level: "warn",
254
- label,
255
- hint: "\uBCF8\uBB38\uC774 \uC774\uBBF8\uC9C0\uB85C \uC2DC\uC791\uD574\uC694. \uCCAB \uBB38\uB2E8\uC5D0 \uD575\uC2EC \uC694\uC57D\uC744 \uBA3C\uC800 \uC4F0\uBA74 \uAC80\uC0C9 \uBC1C\uCDCC\uC5D0 \uC720\uB9AC\uD574\uC694."
256
- };
257
- }
258
- if (extractText(first).trim() === "") {
259
- return {
260
- id: "intro",
261
- level: "warn",
262
- label,
263
- hint: "\uCCAB \uBB38\uB2E8\uC774 \uBE44\uC5B4 \uC788\uC5B4\uC694. \uAE00\uC758 \uD575\uC2EC \uC694\uC57D\uC73C\uB85C \uC2DC\uC791\uD574 \uC8FC\uC138\uC694."
264
- };
265
- }
266
- return {
267
- id: "intro",
268
- level: "good",
269
- label,
270
- hint: "\uCCAB \uBB38\uB2E8\uC774 \uD14D\uC2A4\uD2B8\uB85C \uC2DC\uC791\uD574\uC694 \u2014 \uAC80\uC0C9 \uBC1C\uCDCC\xB7\uBBF8\uB9AC\uBCF4\uAE30\uC5D0 \uC88B\uC544\uC694."
271
- };
272
- }
273
- function checkNoindexCanonicalConflict(input) {
274
- if (!input.noindex || !input.canonical?.trim()) return null;
275
- return {
276
- id: "noindex-canonical",
277
- level: "bad",
278
- label: "noindex + canonical \uCDA9\uB3CC",
279
- hint: "noindex \uC640 canonical \uC744 \uD568\uAED8 \uC4F0\uBA74 \uAC80\uC0C9\uC5D4\uC9C4\uC5D0 \uBAA8\uC21C \uC2E0\uD638\uAC00 \uAC11\uB2C8\uB2E4. \uC0C9\uC778 \uC81C\uC678\uAC00 \uBAA9\uC801\uC774\uBA74 canonical \uC744 \uBE44\uC6B0\uACE0, \uB300\uD45C URL \uC9C0\uC815\uC774 \uBAA9\uC801\uC774\uBA74 noindex \uB97C \uB044\uC138\uC694."
280
- };
281
- }
282
- var GENERIC_ANCHOR_RE = /^(여기|여기를 클릭|클릭|클릭하세요|더 보기|더보기|자세히|자세히 보기|바로가기|링크|click here|here|more|read more)$/i;
283
- function checkAnchorText(body) {
284
- const linked = collectNodes(
285
- body,
286
- (n) => Array.isArray(n.marks) ? n.marks.some((m) => m.type === "link") : false
287
- );
288
- if (linked.length === 0) return null;
289
- const generic = linked.filter(
290
- (n) => GENERIC_ANCHOR_RE.test((n.text ?? "").trim())
291
- );
292
- if (generic.length === 0) {
293
- return {
294
- id: "anchor-text",
295
- level: "good",
296
- label: "\uC575\uCEE4 \uD14D\uC2A4\uD2B8",
297
- hint: "\uB9C1\uD06C \uAE00\uC790\uAC00 \uAD6C\uCCB4\uC801\uC774\uC5D0\uC694 \u2014 \uAC80\uC0C9\uC5D4\uC9C4\uC774 \uB9C1\uD06C \uB300\uC0C1\uC758 \uC8FC\uC81C\uB97C \uC774\uD574\uD560 \uC218 \uC788\uC5B4\uC694."
298
- };
299
- }
300
- return {
301
- id: "anchor-text",
302
- level: "warn",
303
- label: "\uC575\uCEE4 \uD14D\uC2A4\uD2B8",
304
- hint: `"\uC5EC\uAE30\uB97C \uD074\uB9AD" \uAC19\uC740 \uC77C\uBC18 \uC575\uCEE4 ${generic.length}\uAC1C \u2014 \uB9C1\uD06C \uB300\uC0C1\uC758 \uD0A4\uC6CC\uB4DC\uAC00 \uB4E4\uC5B4\uAC04 \uAD6C\uCCB4\uC801\uC778 \uBB38\uAD6C\uB85C \uBC14\uAFD4\uBCF4\uC138\uC694.`
305
- };
306
- }
307
- function checkBusinessProfile(profile) {
308
- if (!profile) return null;
309
- const label = "\uBE44\uC988\uB2C8\uC2A4 \uD504\uB85C\uD544";
310
- const missing = [];
311
- if (!profile.hasAddress) missing.push("\uC8FC\uC18C");
312
- if (!profile.hasTelephone) missing.push("\uC804\uD654\uBC88\uD638");
313
- if (!profile.hasOpeningHours) missing.push("\uC601\uC5C5\uC2DC\uAC04");
314
- if (!profile.hasGeo) missing.push("\uC88C\uD45C");
315
- if (missing.length === 0) {
316
- return {
317
- id: "business-profile",
318
- level: "good",
319
- label,
320
- hint: "\uC0AC\uC5C5\uC7A5 \uC815\uBCF4\uAC00 \uCC44\uC6CC\uC838 \uC788\uC5B4\uC694 \u2014 \uB85C\uCEEC SEO \uAD6C\uC870\uD654 \uB370\uC774\uD130\uAC00 \uC628\uC804\uD574\uC694."
321
- };
322
- }
323
- return {
324
- id: "business-profile",
325
- level: "warn",
326
- label,
327
- hint: `${missing.join("\xB7")} \uC815\uBCF4\uAC00 \uBE44\uC5B4 \uC788\uC5B4\uC694 \u2014 \uC124\uC815 > \uBE44\uC988\uB2C8\uC2A4 \uD504\uB85C\uD544\uC5D0\uC11C \uCC44\uC6B0\uBA74 \uC9C0\uC5ED \uAC80\uC0C9 \uB178\uCD9C\uC5D0 \uB3C4\uC6C0\uB3FC\uC694.`
328
- };
329
- }
330
- function runSeoChecks(input, thresholds = DEFAULT_SEO_THRESHOLDS) {
331
- const chars = countBodyChars(input.bodyJson);
332
- const results = [
333
- checkTitle(input, thresholds),
334
- checkDescription(input, thresholds),
335
- checkBodyLength(chars, thresholds),
336
- checkHeadings(input.bodyJson, chars),
337
- checkImageAlt(input.bodyJson),
338
- checkFeaturedImage(input),
339
- checkLinks(input.bodyJson),
340
- checkAnchorText(input.bodyJson),
341
- checkSlug(input),
342
- checkIntro(input.bodyJson),
343
- checkNoindexCanonicalConflict(input)
344
- ];
345
- return results.filter((r) => r !== null);
346
- }
347
- var SEO_SCORE_WEIGHTS = {
348
- "title-length": 20,
349
- "meta-description": 15,
350
- "body-length": 20,
351
- "heading-structure": 10,
352
- "image-alt": 8,
353
- "featured-image": 5,
354
- links: 7,
355
- "anchor-text": 5,
356
- "slug-length": 5,
357
- intro: 5
358
- };
359
- var LEVEL_SCORE = { good: 1, warn: 0.5, bad: 0 };
360
- function computeSeoScore(results, weights = SEO_SCORE_WEIGHTS) {
361
- let earned = 0;
362
- let total = 0;
363
- for (const r of results) {
364
- const w = weights[r.id];
365
- if (w === void 0) continue;
366
- total += w;
367
- earned += w * LEVEL_SCORE[r.level];
368
- }
369
- if (total <= 0) return 0;
370
- return Math.round(earned / total * 100);
371
- }
372
- var DEFAULT_READABILITY_THRESHOLDS = {
373
- sentenceAvgMax: 60,
374
- sentenceLongMax: 100,
375
- paragraphAvgMax: 300,
376
- headingDensityMax: 1200,
377
- listRecommendBodyMin: 800
378
- };
379
- function splitSentences(text) {
380
- return text.split(/(?<=[.!?…])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
381
- }
382
- function checkSentenceLength(body, thresholds) {
383
- const label = "\uBB38\uC7A5 \uAE38\uC774";
384
- const sentences = splitSentences(extractText(body));
385
- if (sentences.length === 0) {
386
- return {
387
- id: "readability-sentence-length",
388
- level: "good",
389
- label,
390
- hint: "\uBCF8\uBB38\uC774 \uBE44\uC5B4 \uC788\uC5B4 \uC544\uC9C1 \uD3C9\uAC00\uD560 \uBB38\uC7A5\uC774 \uC5C6\uC5B4\uC694."
391
- };
392
- }
393
- const lens = sentences.map((s) => s.replace(/\s+/g, "").length);
394
- const avg = lens.reduce((a, b) => a + b, 0) / lens.length;
395
- const max = Math.max(...lens);
396
- if (avg <= thresholds.sentenceAvgMax && max <= thresholds.sentenceLongMax) {
397
- return {
398
- id: "readability-sentence-length",
399
- level: "good",
400
- label,
401
- hint: `\uD3C9\uADE0 ${Math.round(avg)}\uC790\xB7\uCD5C\uC7A5 ${max}\uC790 \u2014 \uC77D\uAE30 \uD3B8\uD55C \uBB38\uC7A5 \uAE38\uC774\uC608\uC694.`
402
- };
403
- }
404
- return {
405
- id: "readability-sentence-length",
406
- level: "warn",
407
- label,
408
- hint: `\uD3C9\uADE0 ${Math.round(avg)}\uC790\xB7\uCD5C\uC7A5 ${max}\uC790 \u2014 \uBB38\uC7A5\uC774 \uAE38\uC5B4\uC694. \uD55C \uBB38\uC7A5\uC5D0 \uD558\uB098\uC758 \uC815\uBCF4\uB9CC \uB2F4\uC544 \uC9E7\uAC8C \uB04A\uC5B4\uBCF4\uC138\uC694 (\uD3C9\uADE0 ${thresholds.sentenceAvgMax}\uC790 \uC774\uD558 \uAD8C\uC7A5).`
409
- };
410
- }
411
- function checkParagraphLength(body, thresholds) {
412
- const label = "\uBB38\uB2E8 \uAE38\uC774";
413
- const paragraphLens = collectNodes(body, (n) => n.type === "paragraph").map((p) => extractText(p).replace(/\s+/g, "").length).filter((n) => n > 0);
414
- if (paragraphLens.length === 0) {
415
- return {
416
- id: "readability-paragraph-length",
417
- level: "good",
418
- label,
419
- hint: "\uC544\uC9C1 \uD3C9\uAC00\uD560 \uBB38\uB2E8\uC774 \uC5C6\uC5B4\uC694."
420
- };
421
- }
422
- const avg = paragraphLens.reduce((a, b) => a + b, 0) / paragraphLens.length;
423
- const max = Math.max(...paragraphLens);
424
- if (avg <= thresholds.paragraphAvgMax) {
425
- return {
426
- id: "readability-paragraph-length",
427
- level: "good",
428
- label,
429
- hint: `\uD3C9\uADE0 ${Math.round(avg)}\uC790 \u2014 \uBB38\uB2E8\uC774 \uC801\uB2F9\uD788 \uB098\uB258\uC5B4 \uC788\uC5B4\uC694.`
430
- };
431
- }
432
- return {
433
- id: "readability-paragraph-length",
434
- level: "warn",
435
- label,
436
- hint: `\uD3C9\uADE0 ${Math.round(avg)}\uC790(\uCD5C\uC7A5 ${max}\uC790) \u2014 \uBB38\uB2E8\uC774 \uAE38\uC5B4\uC694. ${thresholds.paragraphAvgMax}\uC790 \uC548\uD30E\uC5D0\uC11C \uC904\uBC14\uAFC8\uD558\uBA74 \uC77D\uAE30 \uD3B8\uD574\uC838\uC694.`
437
- };
438
- }
439
- function checkHeadingDensity(body, thresholds) {
440
- const label = "\uC18C\uC81C\uBAA9 \uAC04 \uBC00\uB3C4";
441
- const topNodes = Array.isArray(body?.content) ? body.content : [];
442
- const segments = [];
443
- let current = 0;
444
- let sawHeading = false;
445
- for (const node of topNodes) {
446
- if (node.type === "heading") {
447
- sawHeading = true;
448
- segments.push(current);
449
- current = 0;
450
- continue;
451
- }
452
- current += extractText(node).replace(/\s+/g, "").length;
453
- }
454
- segments.push(current);
455
- if (!sawHeading) {
456
- const total = countBodyChars(body);
457
- if (total <= thresholds.headingDensityMax) {
458
- return {
459
- id: "readability-heading-density",
460
- level: "good",
461
- label,
462
- hint: "\uBCF8\uBB38 \uAE38\uC774\uAC00 \uC9E7\uC544 \uC18C\uC81C\uBAA9 \uC5C6\uC774\uB3C4 \uBC00\uB3C4\uAC00 \uC801\uB2F9\uD574\uC694."
463
- };
464
- }
465
- return {
466
- id: "readability-heading-density",
467
- level: "warn",
468
- label,
469
- hint: `\uBCF8\uBB38 ${total}\uC790 \u2014 \uC18C\uC81C\uBAA9 \uC5C6\uC774 \uC774\uC5B4\uC838 \uC788\uC5B4\uC694. \uC139\uC158\uC744 \uB098\uB204\uBA74 \uC77D\uAE30 \uD3B8\uD574\uC838\uC694.`
470
- };
471
- }
472
- const max = Math.max(...segments);
473
- if (max <= thresholds.headingDensityMax) {
474
- return {
475
- id: "readability-heading-density",
476
- level: "good",
477
- label,
478
- hint: `\uC18C\uC81C\uBAA9 \uC0AC\uC774 \uCD5C\uB300 ${max}\uC790 \u2014 \uBC00\uB3C4\uAC00 \uC801\uB2F9\uD574\uC694.`
479
- };
480
- }
481
- return {
482
- id: "readability-heading-density",
483
- level: "warn",
484
- label,
485
- hint: `\uC18C\uC81C\uBAA9 \uC0AC\uC774 \uCD5C\uB300 ${max}\uC790 \u2014 \uD55C \uAD6C\uAC04\uC774 \uB108\uBB34 \uAE38\uC5B4\uC694. \uC18C\uC81C\uBAA9\uC744 \uB354 \uB098\uB220\uBCF4\uC138\uC694 (${thresholds.headingDensityMax}\uC790 \uC774\uD558 \uAD8C\uC7A5).`
486
- };
487
- }
488
- function checkListUsage(body, thresholds) {
489
- const label = "\uBAA9\uB85D \uD65C\uC6A9";
490
- const lists = collectNodes(
491
- body,
492
- (n) => n.type === "bulletList" || n.type === "orderedList"
493
- );
494
- if (lists.length > 0) {
495
- return {
496
- id: "readability-list-usage",
497
- level: "good",
498
- label,
499
- hint: `\uBAA9\uB85D ${lists.length}\uAC1C \u2014 \uC815\uBCF4\uB97C \uAD6C\uC870\uC801\uC73C\uB85C \uC815\uB9AC\uD588\uC5B4\uC694.`
500
- };
501
- }
502
- const chars = countBodyChars(body);
503
- if (chars < thresholds.listRecommendBodyMin) {
504
- return {
505
- id: "readability-list-usage",
506
- level: "good",
507
- label,
508
- hint: "\uC9E7\uC740 \uAE00\uC774\uB77C \uBAA9\uB85D \uC5C6\uC774\uB3C4 \uAD1C\uCC2E\uC544\uC694."
509
- };
510
- }
511
- return {
512
- id: "readability-list-usage",
513
- level: "warn",
514
- label,
515
- hint: "\uAE34 \uAE00\uC778\uB370 \uBAA9\uB85D(\uB9AC\uC2A4\uD2B8)\uC774 \uC5C6\uC5B4\uC694. \uB098\uC5F4\uD560 \uB0B4\uC6A9\uC740 \uBAA9\uB85D\uC73C\uB85C \uC815\uB9AC\uD558\uBA74 \uC2A4\uCE94\uD558\uAE30 \uC26C\uC6CC\uC838\uC694."
516
- };
517
- }
518
- function runReadabilityChecks(bodyJson, thresholds = DEFAULT_READABILITY_THRESHOLDS) {
519
- return [
520
- checkSentenceLength(bodyJson, thresholds),
521
- checkParagraphLength(bodyJson, thresholds),
522
- checkHeadingDensity(bodyJson, thresholds),
523
- checkListUsage(bodyJson, thresholds)
524
- ];
525
- }
526
-
527
- export {
528
- walkNodes,
529
- collectNodes,
530
- extractText,
531
- countBodyChars,
532
- DEFAULT_SEO_THRESHOLDS,
533
- checkBusinessProfile,
534
- runSeoChecks,
535
- SEO_SCORE_WEIGHTS,
536
- computeSeoScore,
537
- DEFAULT_READABILITY_THRESHOLDS,
538
- runReadabilityChecks
539
- };
540
- //# sourceMappingURL=chunk-6GQCFFHU.js.map