@roottale/cms-renderer-next 0.51.1 → 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.
- package/CHANGELOG.md +96 -0
- package/README.md +54 -4
- package/dist/{chunk-U2IM6OCQ.js → chunk-HM5VP7SR.js} +1 -1
- package/dist/{chunk-U2IM6OCQ.js.map → chunk-HM5VP7SR.js.map} +1 -1
- package/dist/chunk-KSPPYX22.js +14 -0
- package/dist/chunk-KSPPYX22.js.map +1 -0
- package/dist/cms-public.css +46 -0
- package/dist/routes.d.ts +25 -1
- package/dist/routes.js +72 -14
- package/dist/routes.js.map +1 -1
- package/dist/section-runtime.d.ts +46 -0
- package/dist/section-runtime.js +9 -0
- package/dist/server.d.ts +5 -3
- package/dist/server.js +1679 -34
- package/dist/server.js.map +1 -1
- package/package.json +7 -23
- package/dist/before-after-client.d.ts +0 -15
- package/dist/before-after-client.js +0 -8
- package/dist/chunk-27RT6B3O.js +0 -93
- package/dist/chunk-27RT6B3O.js.map +0 -1
- package/dist/chunk-2QU4JU46.js +0 -3927
- package/dist/chunk-2QU4JU46.js.map +0 -1
- package/dist/chunk-4RQ3SEDL.js +0 -106
- package/dist/chunk-4RQ3SEDL.js.map +0 -1
- package/dist/chunk-6GQCFFHU.js +0 -540
- package/dist/chunk-6GQCFFHU.js.map +0 -1
- package/dist/chunk-H46JUIHZ.js +0 -79
- package/dist/chunk-H46JUIHZ.js.map +0 -1
- package/dist/chunk-K62RR4UQ.js +0 -53
- package/dist/chunk-K62RR4UQ.js.map +0 -1
- package/dist/chunk-LEHHSI6G.js +0 -64
- package/dist/chunk-LEHHSI6G.js.map +0 -1
- package/dist/chunk-QYNEOFL7.js +0 -418
- package/dist/chunk-QYNEOFL7.js.map +0 -1
- package/dist/chunk-U5AKACPN.js +0 -113
- package/dist/chunk-U5AKACPN.js.map +0 -1
- package/dist/chunk-UM37DOIK.js +0 -961
- package/dist/chunk-UM37DOIK.js.map +0 -1
- package/dist/chunk-YLSQAT6B.js +0 -59
- package/dist/chunk-YLSQAT6B.js.map +0 -1
- package/dist/chunk-ZL2NA3QT.js +0 -313
- package/dist/chunk-ZL2NA3QT.js.map +0 -1
- package/dist/concerns-client.d.ts +0 -12
- package/dist/concerns-client.js +0 -8
- package/dist/concerns-client.js.map +0 -1
- package/dist/contact-map-client.d.ts +0 -27
- package/dist/contact-map-client.js +0 -8
- package/dist/contact-map-client.js.map +0 -1
- package/dist/expertise-client.d.ts +0 -16
- package/dist/expertise-client.js +0 -9
- package/dist/expertise-client.js.map +0 -1
- package/dist/hero-carousel-client.d.ts +0 -52
- package/dist/hero-carousel-client.js +0 -9
- package/dist/hero-carousel-client.js.map +0 -1
- package/dist/seo-checks.d.ts +0 -87
- package/dist/seo-checks.js +0 -27
- package/dist/seo-checks.js.map +0 -1
- package/dist/site-config-BvUyDx_f.d.ts +0 -3961
- package/dist/spaces-client.d.ts +0 -10
- package/dist/spaces-client.js +0 -8
- package/dist/spaces-client.js.map +0 -1
- package/dist/team-expanding-stage-client.d.ts +0 -35
- package/dist/team-expanding-stage-client.js +0 -8
- package/dist/team-expanding-stage-client.js.map +0 -1
- package/dist/templates.css +0 -4145
- package/dist/templates.d.ts +0 -171
- package/dist/templates.js +0 -291
- package/dist/templates.js.map +0 -1
- package/dist/theme-css-vars-CD3jcEkp.d.ts +0 -7
- package/dist/ui-react-motion-client.d.ts +0 -76
- package/dist/ui-react-motion-client.js +0 -16
- package/dist/ui-react-motion-client.js.map +0 -1
- package/dist/validate.d.ts +0 -104
- package/dist/validate.js +0 -1204
- package/dist/validate.js.map +0 -1
- /package/dist/{before-after-client.js.map → section-runtime.js.map} +0 -0
package/dist/chunk-4RQ3SEDL.js
DELETED
|
@@ -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":[]}
|
package/dist/chunk-6GQCFFHU.js
DELETED
|
@@ -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
|