@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/before-after-client.d.ts +15 -0
  3. package/dist/before-after-client.js +8 -0
  4. package/dist/before-after-client.js.map +1 -0
  5. package/dist/chunk-4LQPZDXZ.js +75 -0
  6. package/dist/chunk-4LQPZDXZ.js.map +1 -0
  7. package/dist/chunk-6GQCFFHU.js +540 -0
  8. package/dist/chunk-6GQCFFHU.js.map +1 -0
  9. package/dist/{chunk-YUT4BRJY.js → chunk-F3G6RVIR.js} +68 -15
  10. package/dist/chunk-F3G6RVIR.js.map +1 -0
  11. package/dist/chunk-H46JUIHZ.js +79 -0
  12. package/dist/chunk-H46JUIHZ.js.map +1 -0
  13. package/dist/chunk-K62RR4UQ.js +53 -0
  14. package/dist/chunk-K62RR4UQ.js.map +1 -0
  15. package/dist/chunk-NUUTTSGT.js +916 -0
  16. package/dist/chunk-NUUTTSGT.js.map +1 -0
  17. package/dist/chunk-RSNUZ4BF.js +3378 -0
  18. package/dist/chunk-RSNUZ4BF.js.map +1 -0
  19. package/dist/chunk-U5AKACPN.js +113 -0
  20. package/dist/chunk-U5AKACPN.js.map +1 -0
  21. package/dist/chunk-WWS43XCT.js +206 -0
  22. package/dist/chunk-WWS43XCT.js.map +1 -0
  23. package/dist/cms-public.css +175 -55
  24. package/dist/concerns-client.d.ts +12 -0
  25. package/dist/concerns-client.js +8 -0
  26. package/dist/concerns-client.js.map +1 -0
  27. package/dist/expertise-client.d.ts +10 -0
  28. package/dist/expertise-client.js +8 -0
  29. package/dist/expertise-client.js.map +1 -0
  30. package/dist/hero-carousel-client.d.ts +22 -1
  31. package/dist/hero-carousel-client.js +1 -1
  32. package/dist/routes.d.ts +51 -5
  33. package/dist/routes.js +109 -62
  34. package/dist/routes.js.map +1 -1
  35. package/dist/seo-checks.d.ts +87 -0
  36. package/dist/seo-checks.js +27 -0
  37. package/dist/seo-checks.js.map +1 -0
  38. package/dist/server.d.ts +51 -2
  39. package/dist/server.js +8 -3
  40. package/dist/site-config-DgYGGwu1.d.ts +3910 -0
  41. package/dist/spaces-client.d.ts +10 -0
  42. package/dist/spaces-client.js +8 -0
  43. package/dist/spaces-client.js.map +1 -0
  44. package/dist/templates.css +3200 -344
  45. package/dist/templates.d.ts +19 -1127
  46. package/dist/templates.js +70 -3
  47. package/dist/templates.js.map +1 -1
  48. package/dist/{theme-css-vars-BiyHBsJG.d.ts → theme-css-vars-CD3jcEkp.d.ts} +2 -2
  49. package/dist/ui-react-motion-client.d.ts +73 -0
  50. package/dist/ui-react-motion-client.js +14 -0
  51. package/dist/ui-react-motion-client.js.map +1 -0
  52. package/dist/validate.d.ts +15 -1
  53. package/dist/validate.js +399 -15
  54. package/dist/validate.js.map +1 -1
  55. package/package.json +15 -5
  56. package/dist/chunk-5R6APJZ2.js +0 -1688
  57. package/dist/chunk-5R6APJZ2.js.map +0 -1
  58. package/dist/chunk-QKZ4HRBB.js +0 -300
  59. package/dist/chunk-QKZ4HRBB.js.map +0 -1
  60. 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-QKZ4HRBB.js";
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 \u24613)",
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-06-13";
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 isRecord2(value) {
911
+ function isRecord3(value) {
603
912
  return value !== null && typeof value === "object" && !Array.isArray(value);
604
913
  }
605
914
  function recordOrEmpty(value) {
606
- return isRecord2(value) ? value : {};
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 (!isRecord2(value)) return;
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 (!isRecord2(value)) return;
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 (!isRecord2(node)) return;
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
- if (options.vertical === "medical" || options.complianceFlags?.includes("medical-review")) {
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: textFrom(doc.bodyJson) }]
1058
+ content: [doc.bodyJson, { type: "text", text: sectionText }]
714
1059
  }
715
1060
  });
716
1061
  for (const finding of lint.findings) {
717
- if (finding.severity !== "critical") continue;
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
- return { errors, ruleset };
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
  };