@roottale/cms-renderer-next 0.41.2 → 0.42.1
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 +54 -0
- package/dist/before-after-client.d.ts +15 -0
- package/dist/before-after-client.js +8 -0
- package/dist/before-after-client.js.map +1 -0
- package/dist/chunk-4LQPZDXZ.js +75 -0
- package/dist/chunk-4LQPZDXZ.js.map +1 -0
- package/dist/chunk-6GQCFFHU.js +540 -0
- package/dist/chunk-6GQCFFHU.js.map +1 -0
- package/dist/{chunk-YUT4BRJY.js → chunk-F3G6RVIR.js} +68 -15
- package/dist/chunk-F3G6RVIR.js.map +1 -0
- package/dist/chunk-H46JUIHZ.js +79 -0
- package/dist/chunk-H46JUIHZ.js.map +1 -0
- package/dist/chunk-K62RR4UQ.js +53 -0
- package/dist/chunk-K62RR4UQ.js.map +1 -0
- package/dist/chunk-NUUTTSGT.js +916 -0
- package/dist/chunk-NUUTTSGT.js.map +1 -0
- package/dist/chunk-RSNUZ4BF.js +3378 -0
- package/dist/chunk-RSNUZ4BF.js.map +1 -0
- package/dist/chunk-U5AKACPN.js +113 -0
- package/dist/chunk-U5AKACPN.js.map +1 -0
- package/dist/chunk-WWS43XCT.js +206 -0
- package/dist/chunk-WWS43XCT.js.map +1 -0
- package/dist/cms-public.css +175 -55
- package/dist/concerns-client.d.ts +12 -0
- package/dist/concerns-client.js +8 -0
- package/dist/concerns-client.js.map +1 -0
- package/dist/expertise-client.d.ts +10 -0
- package/dist/expertise-client.js +8 -0
- package/dist/expertise-client.js.map +1 -0
- package/dist/hero-carousel-client.d.ts +22 -1
- package/dist/hero-carousel-client.js +1 -1
- package/dist/routes.d.ts +51 -5
- package/dist/routes.js +109 -62
- package/dist/routes.js.map +1 -1
- package/dist/seo-checks.d.ts +87 -0
- package/dist/seo-checks.js +27 -0
- package/dist/seo-checks.js.map +1 -0
- package/dist/server.d.ts +51 -2
- package/dist/server.js +8 -3
- package/dist/site-config-DgYGGwu1.d.ts +3910 -0
- package/dist/spaces-client.d.ts +10 -0
- package/dist/spaces-client.js +8 -0
- package/dist/spaces-client.js.map +1 -0
- package/dist/templates.css +3200 -344
- package/dist/templates.d.ts +19 -1127
- package/dist/templates.js +70 -3
- package/dist/templates.js.map +1 -1
- package/dist/{theme-css-vars-BiyHBsJG.d.ts → theme-css-vars-CD3jcEkp.d.ts} +2 -2
- package/dist/ui-react-motion-client.d.ts +73 -0
- package/dist/ui-react-motion-client.js +14 -0
- package/dist/ui-react-motion-client.js.map +1 -0
- package/dist/validate.d.ts +15 -1
- package/dist/validate.js +399 -15
- package/dist/validate.js.map +1 -1
- package/package.json +15 -5
- package/dist/chunk-5R6APJZ2.js +0 -1688
- package/dist/chunk-5R6APJZ2.js.map +0 -1
- package/dist/chunk-QKZ4HRBB.js +0 -300
- package/dist/chunk-QKZ4HRBB.js.map +0 -1
- package/dist/chunk-YUT4BRJY.js.map +0 -1
package/dist/validate.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SectionSchema
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-NUUTTSGT.js";
|
|
4
|
+
import {
|
|
5
|
+
runReadabilityChecks,
|
|
6
|
+
runSeoChecks
|
|
7
|
+
} from "./chunk-6GQCFFHU.js";
|
|
4
8
|
|
|
5
9
|
// src/templates/section-locks.ts
|
|
6
10
|
var CONTENT_PROP_PATHS = {
|
|
@@ -54,6 +58,46 @@ var CONTENT_PROP_PATHS = {
|
|
|
54
58
|
["hours", Number.NaN, "close"],
|
|
55
59
|
["hours", Number.NaN, "note"],
|
|
56
60
|
["mapEmbedUrl"]
|
|
61
|
+
],
|
|
62
|
+
expertise: [
|
|
63
|
+
["heading"],
|
|
64
|
+
["description"],
|
|
65
|
+
["items", Number.NaN, "label"],
|
|
66
|
+
["items", Number.NaN, "kicker"],
|
|
67
|
+
["items", Number.NaN, "title"],
|
|
68
|
+
["items", Number.NaN, "body"],
|
|
69
|
+
["items", Number.NaN, "imageSrc"],
|
|
70
|
+
["items", Number.NaN, "imageAlt"],
|
|
71
|
+
["items", Number.NaN, "tags", Number.NaN],
|
|
72
|
+
["items", Number.NaN, "href"],
|
|
73
|
+
["items", Number.NaN, "ctaLabel"]
|
|
74
|
+
],
|
|
75
|
+
spaces: [
|
|
76
|
+
["heading"],
|
|
77
|
+
["description"],
|
|
78
|
+
["spaces", Number.NaN, "label"],
|
|
79
|
+
["spaces", Number.NaN, "kicker"],
|
|
80
|
+
["spaces", Number.NaN, "title"],
|
|
81
|
+
["spaces", Number.NaN, "description"],
|
|
82
|
+
["spaces", Number.NaN, "imageSrc"],
|
|
83
|
+
["spaces", Number.NaN, "imageAlt"],
|
|
84
|
+
["spaces", Number.NaN, "tags", Number.NaN],
|
|
85
|
+
["disclaimer"]
|
|
86
|
+
],
|
|
87
|
+
concerns: [
|
|
88
|
+
["heading"],
|
|
89
|
+
["description"],
|
|
90
|
+
["figureImage"],
|
|
91
|
+
["figureAlt"],
|
|
92
|
+
["regions", Number.NaN, "label"],
|
|
93
|
+
["regions", Number.NaN, "x"],
|
|
94
|
+
["regions", Number.NaN, "y"],
|
|
95
|
+
["regions", Number.NaN, "title"],
|
|
96
|
+
["regions", Number.NaN, "description"],
|
|
97
|
+
["regions", Number.NaN, "items", Number.NaN],
|
|
98
|
+
["regions", Number.NaN, "href"],
|
|
99
|
+
["regions", Number.NaN, "ctaLabel"],
|
|
100
|
+
["disclaimer"]
|
|
57
101
|
]
|
|
58
102
|
};
|
|
59
103
|
function readSectionLock(value) {
|
|
@@ -301,12 +345,25 @@ var PRICE_DISCOUNT_PROMOTION_TERMS = [
|
|
|
301
345
|
var TESTIMONIAL_TERMS = ["\uD658\uC790 \uD6C4\uAE30", "\uD658\uC790 \uC0AC\uB840", "\uCE58\uB8CC \uC0AC\uB840", "\uC804\uD6C4 \uC0AC\uC9C4"];
|
|
302
346
|
var UNAUTHORIZED_TITLE_TERMS = ["\uC804\uBB38\uBCD1\uC6D0", "\uC9C0\uC815\uBCD1\uC6D0", "\uCD5C\uACE0\uC758 \uC758\uC0AC"];
|
|
303
347
|
var FOREIGN_PROMOTION_TERMS = ["\uC758\uB8CC\uAD00\uAD11 \uC804\uBB38", "\uC678\uAD6D\uC778 \uD658\uC790 1\uC704"];
|
|
348
|
+
function isRecord2(value) {
|
|
349
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
350
|
+
}
|
|
351
|
+
function hasBeforeAfterSection(value) {
|
|
352
|
+
if (Array.isArray(value)) return value.some(hasBeforeAfterSection);
|
|
353
|
+
if (!isRecord2(value)) return false;
|
|
354
|
+
if (value.type === "before-after") return true;
|
|
355
|
+
if (value.type === "section") {
|
|
356
|
+
const attrs = value.attrs;
|
|
357
|
+
if (isRecord2(attrs) && attrs.sectionType === "before-after") return true;
|
|
358
|
+
}
|
|
359
|
+
return Object.values(value).some(hasBeforeAfterSection);
|
|
360
|
+
}
|
|
304
361
|
var MEDICAL_RULES = [
|
|
305
362
|
// MED-001~005: 절대표현 / 보장
|
|
306
363
|
ruleFromTerms(
|
|
307
364
|
"MED-001",
|
|
308
365
|
"critical",
|
|
309
|
-
"\uC808\uB300 \uC6B0\uC704 \uD45C\uD604 \uAE08\uC9C0 (\uC758\uB8CC\uBC95 \xA756 \
|
|
366
|
+
"\uC808\uB300 \uC6B0\uC704 \uD45C\uD604 \uAE08\uC9C0 (\uC758\uB8CC\uBC95 \xA756 \u24618 \uACFC\uC7A5)",
|
|
310
367
|
ABSOLUTE_SUPERLATIVES,
|
|
311
368
|
(term) => `"${term}" \uAC19\uC740 \uC808\uB300 \uC6B0\uC704 \uD45C\uD604\uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
312
369
|
),
|
|
@@ -564,9 +621,47 @@ var MEDICAL_RULES = [
|
|
|
564
621
|
"\uC6D0\uACA9\uC9C4\uB8CC\xB7\uBE44\uB300\uBA74 \uC9C4\uB8CC \uD45C\uD604",
|
|
565
622
|
["\uC6D0\uACA9\uC9C4\uB8CC", "\uBE44\uB300\uBA74 \uC9C4\uB8CC", "\uC628\uB77C\uC778 \uC9C4\uB8CC"],
|
|
566
623
|
(term) => `"${term}" \u2014 \uD604\uD589 \uC758\uB8CC\uBC95 \uD55C\uC2DC\uC801 \uD5C8\uC6A9 \uBC94\uC704 \uD655\uC778 + \uCD9C\uCC98 \uBA85\uC2DC \uAD8C\uC7A5.`
|
|
567
|
-
)
|
|
624
|
+
),
|
|
625
|
+
{
|
|
626
|
+
id: "MED-026",
|
|
627
|
+
severity: "critical",
|
|
628
|
+
description: "\uC758\uB8CC \uC0AC\uC774\uD2B8 \uCE58\uB8CC \uC804\uD6C4 \uC0AC\uC9C4 \uC139\uC158 \uC0AC\uC6A9 \uAE08\uC9C0",
|
|
629
|
+
check(_text, contentJson) {
|
|
630
|
+
if (!hasBeforeAfterSection(contentJson)) return [];
|
|
631
|
+
return [
|
|
632
|
+
{
|
|
633
|
+
ruleId: "MED-026",
|
|
634
|
+
severity: "critical",
|
|
635
|
+
message: "\uC758\uB8CC \uC0AC\uC774\uD2B8\uC5D0\uB294 before-after \uC139\uC158\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uCE58\uB8CC \uC804\uD6C4 \uC0AC\uC9C4 \uAD11\uACE0\uB294 \uC758\uB8CC\uAD11\uACE0\uBC95\uC0C1 \uC81C\uD55C\uB429\uB2C8\uB2E4."
|
|
636
|
+
}
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
id: "MED-027",
|
|
642
|
+
severity: "critical",
|
|
643
|
+
description: "\uD658\uC790 \uC720\uC778 \u2014 \uC9C4\uB8CC\xB7\uC2DC\uC220 \uBB34\uB8CC \uC81C\uACF5 \uD45C\uD604 \uAE08\uC9C0 (\uC758\uB8CC\uBC95 \xA727 \u2462)",
|
|
644
|
+
check(text) {
|
|
645
|
+
const findings = [];
|
|
646
|
+
const re = /(무료|공짜)\s*(시술|수술|검진|진료|치료)/g;
|
|
647
|
+
let match;
|
|
648
|
+
while ((match = re.exec(text)) !== null) {
|
|
649
|
+
const before = text.slice(Math.max(0, match.index - 14), match.index);
|
|
650
|
+
if (/(국가|국민건강보험|건강보험공단|공단|보건소)[^\n.]{0,8}$/.test(before)) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
findings.push({
|
|
654
|
+
ruleId: "MED-027",
|
|
655
|
+
severity: "critical",
|
|
656
|
+
message: `"${match[0]}" \u2014 \uBCF8\uC778\uBD80\uB2F4\uAE08 \uBA74\uC81C\xB7\uBB34\uB8CC \uC9C4\uB8CC \uC81C\uACF5 \uD45C\uD604\uC740 \uD658\uC790 \uC720\uC778 \uD589\uC704\uB85C \uAE08\uC9C0\uB429\uB2C8\uB2E4 (\uC758\uB8CC\uBC95 \xA727 \u2462).`,
|
|
657
|
+
snippet: snippetFor(text, match.index, match[0])
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return findings;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
568
663
|
];
|
|
569
|
-
var MEDICAL_LINT_RULESET = "medical-2026-
|
|
664
|
+
var MEDICAL_LINT_RULESET = "medical-2026-07-11";
|
|
570
665
|
function extractText(doc) {
|
|
571
666
|
const out = [];
|
|
572
667
|
const walk = (node) => {
|
|
@@ -586,6 +681,220 @@ function lintMedicalPage(input) {
|
|
|
586
681
|
].join("\n");
|
|
587
682
|
const findings = [];
|
|
588
683
|
for (const rule of MEDICAL_RULES) {
|
|
684
|
+
findings.push(...rule.check(text, input.contentJson));
|
|
685
|
+
}
|
|
686
|
+
const counts = {
|
|
687
|
+
critical: 0,
|
|
688
|
+
warning: 0,
|
|
689
|
+
info: 0
|
|
690
|
+
};
|
|
691
|
+
for (const f of findings) counts[f.severity] += 1;
|
|
692
|
+
return { findings, counts };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ../audit-tax/src/index.ts
|
|
696
|
+
function snippetFor2(text, idx, term) {
|
|
697
|
+
const start = Math.max(0, idx - 20);
|
|
698
|
+
const end = Math.min(text.length, idx + term.length + 20);
|
|
699
|
+
return `\u2026${text.slice(start, end)}\u2026`;
|
|
700
|
+
}
|
|
701
|
+
function findAll2(text, terms) {
|
|
702
|
+
const out = [];
|
|
703
|
+
for (const term of terms) {
|
|
704
|
+
let from = 0;
|
|
705
|
+
while (from <= text.length) {
|
|
706
|
+
const idx = text.indexOf(term, from);
|
|
707
|
+
if (idx === -1) break;
|
|
708
|
+
out.push({ term, idx });
|
|
709
|
+
from = idx + term.length;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
function ruleFromTerms2(id, severity, description, terms, message) {
|
|
715
|
+
return {
|
|
716
|
+
id,
|
|
717
|
+
severity,
|
|
718
|
+
description,
|
|
719
|
+
check(text) {
|
|
720
|
+
return findAll2(text, terms).map(({ term, idx }) => ({
|
|
721
|
+
ruleId: id,
|
|
722
|
+
severity,
|
|
723
|
+
message: message(term),
|
|
724
|
+
snippet: snippetFor2(text, idx, term)
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
var SUPERLATIVES = ["\uCD5C\uACE0\uC758 \uC138\uBB34\uC0AC", "\uCD5C\uB300 \uD658\uAE09", "\uC720\uC77C\uD55C", "1\uC704 \uC138\uBB34\uC0AC", "\uB118\uBC84\uC6D0"];
|
|
730
|
+
var GUARANTEE_TERMS2 = [
|
|
731
|
+
"\uD658\uAE09 \uBCF4\uC7A5",
|
|
732
|
+
"100% \uD658\uAE09",
|
|
733
|
+
"\uC808\uC138 \uBCF4\uC7A5",
|
|
734
|
+
"\uC808\uB300 \uD658\uAE09",
|
|
735
|
+
"\uC808\uC138 100%"
|
|
736
|
+
];
|
|
737
|
+
var COMPARISON_TERMS2 = ["\uB2E4\uB978 \uC138\uBB34\uC0AC\uBCF4\uB2E4", "\uD0C0 \uC138\uBB34\uBC95\uC778\uBCF4\uB2E4"];
|
|
738
|
+
var REFUND_PROMISE_TERMS = ["\uD658\uAE09\uB960 1\uC704", "\uCD5C\uB300 \uD658\uAE09\uC561 \uBCF4\uC7A5", "\uC218\uC2ED\uC5B5 \uD658\uAE09"];
|
|
739
|
+
var PROMOTION_TERMS = [
|
|
740
|
+
"\uBB34\uB8CC \uC0C1\uB2F4 + \uD560\uC778",
|
|
741
|
+
"\uC218\uC218\uB8CC 50% \uD560\uC778",
|
|
742
|
+
"\uC218\uC784\uB8CC \uBA74\uC81C",
|
|
743
|
+
// 시행령 6호 — 객관적 근거 없는 무료·최저가 표시 (2026-07-11)
|
|
744
|
+
"\uC218\uC784\uB8CC \uBB34\uB8CC",
|
|
745
|
+
"\uCD5C\uC800 \uC218\uC218\uB8CC",
|
|
746
|
+
"\uCD5C\uC800\uAC00"
|
|
747
|
+
];
|
|
748
|
+
var TAX_RULES = [
|
|
749
|
+
ruleFromTerms2(
|
|
750
|
+
"TAX-001",
|
|
751
|
+
"critical",
|
|
752
|
+
"\uC808\uB300 \uC6B0\uC704 \uD45C\uD604 \uAE08\uC9C0 (\uC138\uBB34\uC0AC\uBC95 \uC81C12\uC870\uC7587 \u24613 \uACFC\uC7A5\xB7\uC624\uB3C4)",
|
|
753
|
+
SUPERLATIVES,
|
|
754
|
+
(term) => `"${term}" \uAC19\uC740 \uC808\uB300 \uC6B0\uC704 \uD45C\uD604\uC740 \uAD11\uACE0\uC5D0 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
755
|
+
),
|
|
756
|
+
ruleFromTerms2(
|
|
757
|
+
"TAX-002",
|
|
758
|
+
"critical",
|
|
759
|
+
"\uACB0\uACFC \uBCF4\uC7A5 \uD45C\uD604 \uAE08\uC9C0 (\uC138\uBB34\uC0AC\uBC95 \uC81C12\uC870\uC7587 \u24614 \uBD80\uB2F9\uD55C \uAE30\uB300)",
|
|
760
|
+
GUARANTEE_TERMS2,
|
|
761
|
+
(term) => `"${term}" \u2014 \uD658\uAE09\xB7\uC808\uC138 \uACB0\uACFC \uBCF4\uC7A5 \uAD11\uACE0\uB294 \uAE08\uC9C0\uB429\uB2C8\uB2E4.`
|
|
762
|
+
),
|
|
763
|
+
ruleFromTerms2(
|
|
764
|
+
"TAX-003",
|
|
765
|
+
"critical",
|
|
766
|
+
"\uB2E4\uB978 \uC138\uBB34\uC0AC \uBE44\uAD50 \uAD11\uACE0 \uAE08\uC9C0 (\uC138\uBB34\uC0AC\uBC95 \uC81C12\uC870\uC7587 \u24615 \uBE44\uBC29\xB7\uBE44\uAD50)",
|
|
767
|
+
COMPARISON_TERMS2,
|
|
768
|
+
(term) => `\uB2E4\uB978 \uC138\uBB34\uC0AC\xB7\uC138\uBB34\uBC95\uC778 \uBE44\uAD50 \uD45C\uD604 "${term}" \uC740 \uAE08\uC9C0\uB429\uB2C8\uB2E4.`
|
|
769
|
+
),
|
|
770
|
+
ruleFromTerms2(
|
|
771
|
+
"TAX-004",
|
|
772
|
+
"warning",
|
|
773
|
+
"\uD658\uAE09\uC561 \uAC15\uC870 (\uC2DC\uD589\uB839 \xA733\u24612 \u2014 \uC0B0\uC815\uAE30\uC900\xB7\uC801\uC6A9\uB300\uC0C1\xB7\uC870\uAC74 \uD45C\uC2DC \uC758\uBB34)",
|
|
774
|
+
REFUND_PROMISE_TERMS,
|
|
775
|
+
(term) => `"${term}" \u2014 \uD658\uAE09\uC561 \uAC15\uC870\uB294 \uD3C9\uADE0\xB7\uC911\uAC04\uAC12 + \uCD9C\uCC98 \uBA85\uC2DC \uD544\uC694 (\uC724\uB9AC\uADDC\uC815).`
|
|
776
|
+
),
|
|
777
|
+
ruleFromTerms2(
|
|
778
|
+
"TAX-005",
|
|
779
|
+
"warning",
|
|
780
|
+
"\uD560\uC778\xB7\uBB34\uB8CC\xB7\uCD5C\uC800\uAC00 \uAD11\uACE0 (\uC2DC\uD589\uB839 \xA733\u24616 \u2014 \uADFC\uAC70 \uC5C6\uB294 \uBB34\uB8CC\xB7\uCD5C\uC800\uAC00)",
|
|
781
|
+
PROMOTION_TERMS,
|
|
782
|
+
(term) => `"${term}" \u2014 \uC218\uC784\uB8CC \uBA74\uC81C\xB7\uACFC\uB3C4\uD55C \uD560\uC778\uC740 \uC720\uC778\uC131 \uAD11\uACE0\uB85C \uBD84\uB958 \uAC00\uB2A5.`
|
|
783
|
+
),
|
|
784
|
+
{
|
|
785
|
+
id: "TAX-006",
|
|
786
|
+
severity: "info",
|
|
787
|
+
description: "\uC138\uBB34\uC0AC \uC790\uACA9 \uBA85\uC2DC \uAD8C\uC7A5",
|
|
788
|
+
check(text) {
|
|
789
|
+
if (text.length > 100 && !/세무사|세무법인|회계사|CPA/.test(text)) {
|
|
790
|
+
return [
|
|
791
|
+
{
|
|
792
|
+
ruleId: "TAX-006",
|
|
793
|
+
severity: "info",
|
|
794
|
+
message: "\uC138\uBB34\uC0AC \uC790\uACA9 (\uC138\uBB34\uC0AC/\uC138\uBB34\uBC95\uC778/\uD68C\uACC4\uC0AC \uB4F1) \uBA85\uC2DC \uAD8C\uC7A5."
|
|
795
|
+
}
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
id: "TAX-007",
|
|
803
|
+
severity: "warning",
|
|
804
|
+
description: "\uC0AC\uBB34\uC18C\uBA85\xB7\uB4F1\uB85D \uC131\uBA85 \uD45C\uC2DC \uC758\uBB34 (\uC2DC\uD589\uB839 \xA733\u2460)",
|
|
805
|
+
check(text) {
|
|
806
|
+
if (text.length > 200 && !/사무소|법인|등록번호/.test(text)) {
|
|
807
|
+
return [
|
|
808
|
+
{
|
|
809
|
+
ruleId: "TAX-007",
|
|
810
|
+
severity: "warning",
|
|
811
|
+
message: "\uAD11\uACE0\uC5D0\uB294 \uC0AC\uBB34\uC18C\uBA85\xB7\uB4F1\uB85D\uB41C \uC131\uBA85 \uD45C\uC2DC\uAC00 \uC758\uBB34\uC785\uB2C8\uB2E4 (\uC138\uBB34\uC0AC\uBC95 \uC2DC\uD589\uB839, 2026-06-24 \uC2DC\uD589)."
|
|
812
|
+
}
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
ruleFromTerms2(
|
|
819
|
+
"TAX-008",
|
|
820
|
+
"warning",
|
|
821
|
+
"\uD6C4\uAE30\xB7\uC0AC\uB840 \uAD11\uACE0 \u2014 \uD3C9\uADE0/\uD55C\uACC4 \uBA85\uC2DC \uD544\uC694",
|
|
822
|
+
["\uD658\uAE09 \uC0AC\uB840", "\uACE0\uAC1D \uD6C4\uAE30", "\uC131\uACF5 \uC0AC\uB840"],
|
|
823
|
+
(term) => `"${term}" \u2014 \uAC1C\uBCC4 \uC0AC\uB840\uB294 \uD3C9\uADE0\xB7\uD55C\uACC4 (\uAC1C\uC778\uCC28) \uBA85\uC2DC \uD544\uC694 (\uC724\uB9AC\uADDC\uC815).`
|
|
824
|
+
),
|
|
825
|
+
ruleFromTerms2(
|
|
826
|
+
"TAX-009",
|
|
827
|
+
"info",
|
|
828
|
+
"\uC678\uAD6D\uC5B4 \uC790\uACA9 \uD45C\uC2DC \u2014 \uAD6D\uB0B4 \uC790\uACA9 \uBCD1\uAE30",
|
|
829
|
+
["US CPA", "AICPA", "Big4"],
|
|
830
|
+
(term) => `"${term}" \u2014 \uC678\uAD6D \uC790\uACA9 \uD45C\uC2DC \uC2DC \uAD6D\uB0B4 \uC138\uBB34\uC0AC \uC790\uACA9 \uBCF4\uC720 \uC5EC\uBD80 \uBA85\uC2DC.`
|
|
831
|
+
),
|
|
832
|
+
{
|
|
833
|
+
id: "TAX-010",
|
|
834
|
+
severity: "critical",
|
|
835
|
+
description: "\uC808\uB300 \uC6B0\uC704 \uD45C\uD604 \u2014 \uD6C4\uCE58\uD615 \uC5B4\uC21C ('\uC138\uBB34 \uC0C1\uB2F4 1\uC704', TAX-001 \uBCF4\uC644)",
|
|
836
|
+
check(text) {
|
|
837
|
+
const re = /(세무사?|세무법인|기장|상담)[^\n.]{0,10}?(1위|넘버원)/g;
|
|
838
|
+
const findings = [];
|
|
839
|
+
let match;
|
|
840
|
+
while ((match = re.exec(text)) !== null) {
|
|
841
|
+
findings.push({
|
|
842
|
+
ruleId: "TAX-010",
|
|
843
|
+
severity: "critical",
|
|
844
|
+
message: `"${match[0]}" \u2014 \uD6C4\uCE58\uD615 \uC808\uB300 \uC6B0\uC704 \uD45C\uD604\uB3C4 \uAD11\uACE0\uC5D0 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC138\uBB34\uC0AC\uBC95 \uC81C12\uC870\uC7587 \u24613).`,
|
|
845
|
+
snippet: snippetFor2(text, match.index, match[0])
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
return findings;
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
id: "TAX-011",
|
|
853
|
+
severity: "critical",
|
|
854
|
+
description: "\uC138\uBB34\uACF5\uBB34\uC6D0 \uC5F0\uACE0 \uC120\uC804 \uAE08\uC9C0 (\uC138\uBB34\uC0AC\uBC95 \xA714\uC7582 + \uC2DC\uD589\uB839 \xA733\u24611)",
|
|
855
|
+
check(text) {
|
|
856
|
+
const re = /(국세청|세무서|세무공무원|국세공무원)[^\n.]{0,10}?(인맥|연줄|연고)/g;
|
|
857
|
+
const findings = [];
|
|
858
|
+
let match;
|
|
859
|
+
while ((match = re.exec(text)) !== null) {
|
|
860
|
+
findings.push({
|
|
861
|
+
ruleId: "TAX-011",
|
|
862
|
+
severity: "critical",
|
|
863
|
+
message: `"${match[0]}" \u2014 \uC138\uBB34\uACF5\uBB34\uC6D0\uACFC\uC758 \uC5F0\uACE0 \uB4F1 \uC0AC\uC801 \uAD00\uACC4\uB85C \uC601\uD5A5\uB825\uC744 \uBBF8\uCE60 \uC218 \uC788\uB294 \uAC83\uCC98\uB7FC \uC120\uC804\uD558\uB294 \uAD11\uACE0\uB294 \uAE08\uC9C0\uB429\uB2C8\uB2E4 (\uC138\uBB34\uC0AC\uBC95 \xA714\uC7582).`,
|
|
864
|
+
snippet: snippetFor2(text, match.index, match[0])
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return findings;
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
ruleFromTerms2(
|
|
871
|
+
"TAX-012",
|
|
872
|
+
"critical",
|
|
873
|
+
"\uD589\uC815\uCC98\uBD84\xB7\uD310\uACB0 \uB4F1 \uACB0\uACFC \uC608\uCE21 \uAD11\uACE0 \uAE08\uC9C0 (\uC2DC\uD589\uB839 \xA733\u24615)",
|
|
874
|
+
["\uC2B9\uC18C \uBCF4\uC7A5", "\uC778\uC6A9 \uBCF4\uC7A5", "\uCDE8\uC18C \uBCF4\uC7A5", "\uC138\uBB34\uC870\uC0AC \uBB34\uB9C8", "\uD658\uAE09 \uD655\uC815"],
|
|
875
|
+
(term) => `"${term}" \u2014 \uD589\uC815\uAE30\uAD00 \uCC98\uBD84\xB7\uBC95\uC6D0 \uD310\uACB0 \uB4F1\uC758 \uACB0\uACFC\uB97C \uC608\uCE21\xB7\uD655\uC815\uD558\uB294 \uAD11\uACE0\uB294 \uAE08\uC9C0\uB429\uB2C8\uB2E4 (\uC138\uBB34\uC0AC\uBC95 \uC2DC\uD589\uB839, \uC0BC\uCA5C\uC0BC \uACFC\uC9D5\uAE08 \uC120\uB840).`
|
|
876
|
+
)
|
|
877
|
+
];
|
|
878
|
+
var TAX_LINT_RULESET = "tax-2026-07-11b";
|
|
879
|
+
function extractText2(doc) {
|
|
880
|
+
const out = [];
|
|
881
|
+
const walk = (node) => {
|
|
882
|
+
if (!node || typeof node !== "object") return;
|
|
883
|
+
const n = node;
|
|
884
|
+
if (typeof n.text === "string") out.push(n.text);
|
|
885
|
+
if (Array.isArray(n.content)) n.content.forEach(walk);
|
|
886
|
+
};
|
|
887
|
+
walk(doc);
|
|
888
|
+
return out.join("\n");
|
|
889
|
+
}
|
|
890
|
+
function lintTaxPage(input) {
|
|
891
|
+
const text = [
|
|
892
|
+
input.title,
|
|
893
|
+
input.excerpt ?? "",
|
|
894
|
+
extractText2(input.contentJson)
|
|
895
|
+
].join("\n");
|
|
896
|
+
const findings = [];
|
|
897
|
+
for (const rule of TAX_RULES) {
|
|
589
898
|
findings.push(...rule.check(text));
|
|
590
899
|
}
|
|
591
900
|
const counts = {
|
|
@@ -599,11 +908,11 @@ function lintMedicalPage(input) {
|
|
|
599
908
|
|
|
600
909
|
// src/templates/publish-gate.ts
|
|
601
910
|
var PUBLISH_GATE_RULESET = "publish-gate-2026-07-02";
|
|
602
|
-
function
|
|
911
|
+
function isRecord3(value) {
|
|
603
912
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
604
913
|
}
|
|
605
914
|
function recordOrEmpty(value) {
|
|
606
|
-
return
|
|
915
|
+
return isRecord3(value) ? value : {};
|
|
607
916
|
}
|
|
608
917
|
function stringOrEmpty(value) {
|
|
609
918
|
return typeof value === "string" ? value.trim() : "";
|
|
@@ -618,7 +927,7 @@ function collectPropsCopy(value, out) {
|
|
|
618
927
|
for (const item of value) collectPropsCopy(item, out);
|
|
619
928
|
return;
|
|
620
929
|
}
|
|
621
|
-
if (!
|
|
930
|
+
if (!isRecord3(value)) return;
|
|
622
931
|
for (const [key, child] of Object.entries(value)) {
|
|
623
932
|
if (NON_COPY_PROP_KEYS.has(key)) continue;
|
|
624
933
|
collectPropsCopy(child, out);
|
|
@@ -629,7 +938,7 @@ function collectText(value, out) {
|
|
|
629
938
|
for (const item of value) collectText(item, out);
|
|
630
939
|
return;
|
|
631
940
|
}
|
|
632
|
-
if (!
|
|
941
|
+
if (!isRecord3(value)) return;
|
|
633
942
|
if (typeof value.text === "string") out.push(value.text);
|
|
634
943
|
if (value.type === "section") {
|
|
635
944
|
const attrs = recordOrEmpty(value.attrs);
|
|
@@ -647,10 +956,25 @@ function seoDescription(doc) {
|
|
|
647
956
|
const seo = recordOrEmpty(meta.seo);
|
|
648
957
|
return stringOrEmpty(seo.description) || stringOrEmpty(doc.excerpt);
|
|
649
958
|
}
|
|
959
|
+
function toSeoCheckInput(doc) {
|
|
960
|
+
const meta = recordOrEmpty(doc.metaJson);
|
|
961
|
+
const seo = recordOrEmpty(meta.seo);
|
|
962
|
+
return {
|
|
963
|
+
title: stringOrEmpty(doc.title),
|
|
964
|
+
seoTitle: stringOrEmpty(seo.title),
|
|
965
|
+
seoDescription: stringOrEmpty(seo.description),
|
|
966
|
+
excerpt: stringOrEmpty(doc.excerpt),
|
|
967
|
+
slug: doc.slug?.trim() ?? "",
|
|
968
|
+
bodyJson: doc.bodyJson,
|
|
969
|
+
hasFeaturedImage: doc.hasFeaturedImage === true,
|
|
970
|
+
canonical: typeof seo.canonical === "string" ? seo.canonical : void 0,
|
|
971
|
+
noindex: seo.noindex === true
|
|
972
|
+
};
|
|
973
|
+
}
|
|
650
974
|
function collectH1Labels(doc) {
|
|
651
975
|
const labels = [];
|
|
652
976
|
const walk = (node) => {
|
|
653
|
-
if (!
|
|
977
|
+
if (!isRecord3(node)) return;
|
|
654
978
|
const type = stringOrEmpty(node.type);
|
|
655
979
|
if (type === "heading") {
|
|
656
980
|
const attrs = recordOrEmpty(node.attrs);
|
|
@@ -672,8 +996,19 @@ function collectH1Labels(doc) {
|
|
|
672
996
|
walk(doc);
|
|
673
997
|
return labels;
|
|
674
998
|
}
|
|
999
|
+
function hasBeforeAfterSection2(value) {
|
|
1000
|
+
if (Array.isArray(value)) return value.some(hasBeforeAfterSection2);
|
|
1001
|
+
if (!isRecord3(value)) return false;
|
|
1002
|
+
if (value.type === "before-after") return true;
|
|
1003
|
+
if (value.type === "section") {
|
|
1004
|
+
const attrs = recordOrEmpty(value.attrs);
|
|
1005
|
+
return attrs.sectionType === "before-after";
|
|
1006
|
+
}
|
|
1007
|
+
return Object.values(value).some(hasBeforeAfterSection2);
|
|
1008
|
+
}
|
|
675
1009
|
function assertPublishable(doc, options) {
|
|
676
1010
|
const errors = [];
|
|
1011
|
+
const warnings = [];
|
|
677
1012
|
const ruleset = { publish: PUBLISH_GATE_RULESET };
|
|
678
1013
|
const h1Labels = collectH1Labels(doc.bodyJson);
|
|
679
1014
|
if (options.documentType === "post") {
|
|
@@ -703,28 +1038,76 @@ function assertPublishable(doc, options) {
|
|
|
703
1038
|
message: "\uBC1C\uD589\uD558\uB824\uBA74 SEO meta description \uB610\uB294 excerpt\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
|
|
704
1039
|
});
|
|
705
1040
|
}
|
|
706
|
-
|
|
1041
|
+
const vertical = stringOrEmpty(options.vertical);
|
|
1042
|
+
const includesBeforeAfter = hasBeforeAfterSection2(doc.bodyJson);
|
|
1043
|
+
if (!vertical && includesBeforeAfter) {
|
|
1044
|
+
errors.push({
|
|
1045
|
+
code: "VERTICAL_UNKNOWN",
|
|
1046
|
+
severity: "warning",
|
|
1047
|
+
message: "before-after \uC139\uC158\uC774 \uC788\uC9C0\uB9CC \uC0AC\uC774\uD2B8 vertical \uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC5B4 \uBC1C\uD589\uC744 \uCC28\uB2E8\uD569\uB2C8\uB2E4."
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (vertical === "medical" || options.complianceFlags?.includes("medical-review")) {
|
|
707
1051
|
ruleset.medical = MEDICAL_LINT_RULESET;
|
|
1052
|
+
const sectionText = textFrom(doc.bodyJson);
|
|
708
1053
|
const lint = lintMedicalPage({
|
|
709
1054
|
title: doc.title ?? "",
|
|
710
1055
|
excerpt: doc.excerpt,
|
|
711
1056
|
contentJson: {
|
|
712
1057
|
type: "doc",
|
|
713
|
-
content: [{ type: "text", text:
|
|
1058
|
+
content: [doc.bodyJson, { type: "text", text: sectionText }]
|
|
714
1059
|
}
|
|
715
1060
|
});
|
|
716
1061
|
for (const finding of lint.findings) {
|
|
717
|
-
|
|
718
|
-
errors.push({
|
|
1062
|
+
const entry = {
|
|
719
1063
|
code: "MEDICAL_COMPLIANCE",
|
|
720
1064
|
ruleId: finding.ruleId,
|
|
721
1065
|
severity: finding.severity,
|
|
722
1066
|
message: finding.message,
|
|
723
1067
|
snippet: finding.snippet
|
|
724
|
-
}
|
|
1068
|
+
};
|
|
1069
|
+
if (finding.severity === "critical") errors.push(entry);
|
|
1070
|
+
else warnings.push(entry);
|
|
725
1071
|
}
|
|
726
1072
|
}
|
|
727
|
-
|
|
1073
|
+
if (vertical === "tax" || options.complianceFlags?.includes("tax-review")) {
|
|
1074
|
+
ruleset.tax = TAX_LINT_RULESET;
|
|
1075
|
+
const sectionText = textFrom(doc.bodyJson);
|
|
1076
|
+
const lint = lintTaxPage({
|
|
1077
|
+
title: doc.title ?? "",
|
|
1078
|
+
excerpt: doc.excerpt,
|
|
1079
|
+
contentJson: {
|
|
1080
|
+
type: "doc",
|
|
1081
|
+
content: [doc.bodyJson, { type: "text", text: sectionText }]
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
for (const finding of lint.findings) {
|
|
1085
|
+
const entry = {
|
|
1086
|
+
code: "TAX_COMPLIANCE",
|
|
1087
|
+
ruleId: finding.ruleId,
|
|
1088
|
+
severity: finding.severity,
|
|
1089
|
+
message: finding.message,
|
|
1090
|
+
snippet: finding.snippet
|
|
1091
|
+
};
|
|
1092
|
+
if (finding.severity === "critical") errors.push(entry);
|
|
1093
|
+
else warnings.push(entry);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const seoCheckInput = toSeoCheckInput(doc);
|
|
1097
|
+
const seoFindings = [
|
|
1098
|
+
...runSeoChecks(seoCheckInput),
|
|
1099
|
+
...runReadabilityChecks(seoCheckInput.bodyJson)
|
|
1100
|
+
];
|
|
1101
|
+
for (const finding of seoFindings) {
|
|
1102
|
+
if (finding.level !== "bad") continue;
|
|
1103
|
+
warnings.push({
|
|
1104
|
+
code: "SEO_CHECK",
|
|
1105
|
+
ruleId: finding.id,
|
|
1106
|
+
severity: "warning",
|
|
1107
|
+
message: finding.hint
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return { errors, warnings, ruleset };
|
|
728
1111
|
}
|
|
729
1112
|
function writePublishGateMeta(metaJson, result) {
|
|
730
1113
|
const meta = { ...recordOrEmpty(metaJson) };
|
|
@@ -734,6 +1117,7 @@ function writePublishGateMeta(metaJson, result) {
|
|
|
734
1117
|
blocked: result.errors.length > 0,
|
|
735
1118
|
ruleset: result.ruleset,
|
|
736
1119
|
errors: result.errors,
|
|
1120
|
+
warnings: result.warnings,
|
|
737
1121
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
738
1122
|
...result.errors.length === 0 ? { override: null } : {}
|
|
739
1123
|
};
|