legal-doc-editor 0.6.0 → 0.7.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  - **조·항·호 자동 번호**: 조를 넣거나 옮기면 제1조, ①, 1. 번호가 알아서 다시 매겨져요
8
8
  - **`{{변수}}` 채우기**: 본문에 `{{갑}}`을 입력하면 입력칸이 생기고, 값을 넣으면 문서에 반영돼요
9
- - **한글(.hwp·.hwpx)·Word(.docx·.doc) 열기·저장**: 일반 문단으로 쓴 계약서도 "제N조", "①", "1." 패턴을 인식해서 조·항·호 구조로 바꿔요
9
+ - **한글(.hwp·.hwpx)·Word(.docx·.doc) 열기·저장**: 일반 문단으로 쓴 계약서도 "제N조", "①", "1." 패턴을 인식해서 조·항·호 구조로 바꿔요. "제1조(목적) 이 계약은…"처럼 한 줄에 붙은 조는 제목과 본문으로 나눠요
10
10
  - 저장은 `.docx`, `.hwpx`, `.hwp`(HWP 5.0)예요. `.hwpx`는 한글 2014 이상에서 열려요
11
11
  - 옛 Word `.doc`(97–2003)는 열기만 돼요. 글자와 표만 옮기고 굵게 같은 글자 서식과 그림은 옮기지 않아요
12
12
  - 파일 변환 라이브러리는 필요할 때만 불러와서 에디터 본체 번들은 가벼워요
package/dist/index.js CHANGED
@@ -440,6 +440,30 @@ function be(e) {
440
440
  }
441
441
  return null;
442
442
  }
443
+ var xe = "(([【〔", Se = "))]】〕";
444
+ function Ce(e) {
445
+ let t = e.length - e.trimStart().length, n = e.trimStart();
446
+ if (xe.includes(n[0] ?? "_")) {
447
+ let e = 0;
448
+ for (let r = 0; r < Math.min(n.length, 60); r++) if (xe.includes(n[r])) e++;
449
+ else if (Se.includes(n[r]) && --e === 0) return t + r + 1;
450
+ return 0;
451
+ }
452
+ return n.trim().length <= 20 && !/[.다]$/.test(n.trim()) ? e.length : 0;
453
+ }
454
+ function we(e, t) {
455
+ let n = e.ownerDocument.createRange();
456
+ n.setStart(e, 0), n.setEnd(e, e.childNodes.length);
457
+ let r = e.ownerDocument.createTreeWalker(e, NodeFilter.SHOW_TEXT), i = t;
458
+ for (let e = r.nextNode(); e; e = r.nextNode()) {
459
+ if (i <= e.data.length) {
460
+ n.setEnd(e, i);
461
+ break;
462
+ }
463
+ i -= e.data.length;
464
+ }
465
+ return n.extractContents();
466
+ }
443
467
  function Z(e, t) {
444
468
  let n = typeof t == "number" ? t : e.textContent.match(t)[0].length, r = e.ownerDocument.createTreeWalker(e, NodeFilter.SHOW_TEXT);
445
469
  for (let e = r.nextNode(); e && n > 0; e = r.nextNode()) {
@@ -479,8 +503,9 @@ function Q(e) {
479
503
  let n = t.createElement("h3");
480
504
  n.textContent = r, e.replaceWith(n), a = null, o.fill(0), s.fill(0);
481
505
  } else if (e.tagName === "P" && Y.test(n)) {
506
+ Z(e, Y);
482
507
  let n = t.createElement("h2");
483
- n.innerHTML = e.innerHTML, Z(n, Y), e.replaceWith(n), a = null, o.fill(0), s.fill(0);
508
+ n.append(we(e, Ce(e.textContent ?? ""))), Z(e, /^\s*/), e.replaceWith(n), e.textContent.trim() && n.after(e), a = null, o.fill(0), s.fill(0);
484
509
  } else if (e.tagName === "P" && he.test(n)) {
485
510
  let r = n.trim().charCodeAt(0) - 9311;
486
511
  (!a || a.children.length + Number(a.getAttribute("start") ?? 1) !== r) && (e.before(a = t.createElement("ol")), r > 1 && a.setAttribute("start", String(r))), Z(e, he);
@@ -500,50 +525,50 @@ function Q(e) {
500
525
  }
501
526
  return t.body.innerHTML;
502
527
  }
503
- async function $(e) {
528
+ async function Te(e) {
504
529
  let { default: t } = await import("mammoth"), { value: n } = await t.convertToHtml({ arrayBuffer: await e.arrayBuffer() });
505
530
  return Q(n);
506
531
  }
507
- async function xe(e) {
532
+ async function Ee(e) {
508
533
  let { detectFormat: t, hwpToHwpx: n, HwpxReader: r } = await import("hwp-convert"), i = new Uint8Array(await e.arrayBuffer());
509
534
  t(i) === "hwp" && (i = new Uint8Array(await n(i)));
510
535
  let a = new r();
511
536
  return await a.loadFromArrayBuffer(i.buffer), Q(await a.extractHtml());
512
537
  }
513
- async function Se(e) {
538
+ async function De(e) {
514
539
  let { parseMsDoc: t } = await import("@file-viewer/doc");
515
- return Q(we(t(new Uint8Array(await e.arrayBuffer())).blocks));
540
+ return Q(ke(t(new Uint8Array(await e.arrayBuffer())).blocks));
516
541
  }
517
- var Ce = (e) => e.replace(/[\x00-\x08\x0c\x0e-\x1f]/g, "");
518
- function we(e) {
519
- let t = (e) => Ce(e).split("\r").filter((e) => e.trim()).map((e) => `<p>${K(e).replace(/\v/g, "<br>")}</p>`).join("");
542
+ var Oe = (e) => e.replace(/[\x00-\x08\x0c\x0e-\x1f]/g, "");
543
+ function ke(e) {
544
+ let t = (e) => Oe(e).split("\r").filter((e) => e.trim()).map((e) => `<p>${K(e).replace(/\v/g, "<br>")}</p>`).join("");
520
545
  return e.map((e) => {
521
546
  if (e.type === "paragraph") return t(e.text);
522
547
  if (e.type !== "table") return "";
523
- let n = (e) => `<td${(e.colspan ?? 1) > 1 ? ` colspan="${e.colspan}"` : ""}${(e.rowspan ?? 1) > 1 ? ` rowspan="${e.rowspan}"` : ""}>${e.paragraphs.map((e) => K(Ce(e.text).replace(/[\r\v]/g, ""))).filter(Boolean).join("<br>")}</td>`;
548
+ let n = (e) => `<td${(e.colspan ?? 1) > 1 ? ` colspan="${e.colspan}"` : ""}${(e.rowspan ?? 1) > 1 ? ` rowspan="${e.rowspan}"` : ""}>${e.paragraphs.map((e) => K(Oe(e.text).replace(/[\r\v]/g, ""))).filter(Boolean).join("<br>")}</td>`;
524
549
  return `<table>${e.rows.map((e) => `<tr>${e.cells.filter((e) => !e.hidden).map(n).join("")}</tr>`).join("")}</table>`;
525
550
  }).join("");
526
551
  }
527
- function Te(e) {
528
- return /\.docx$/i.test(e.name) ? $(e) : /\.doc$/i.test(e.name) ? Se(e) : /\.hwpx?$/i.test(e.name) ? xe(e) : Promise.reject(/* @__PURE__ */ Error(".docx, .doc, .hwp, .hwpx 파일만 열 수 있어요"));
552
+ function Ae(e) {
553
+ return /\.docx$/i.test(e.name) ? Te(e) : /\.doc$/i.test(e.name) ? De(e) : /\.hwpx?$/i.test(e.name) ? Ee(e) : Promise.reject(/* @__PURE__ */ Error(".docx, .doc, .hwp, .hwpx 파일만 열 수 있어요"));
529
554
  }
530
555
  //#endregion
531
556
  //#region src/LegalEditor.tsx
532
- function Ee(e) {
557
+ function $(e) {
533
558
  let t = /* @__PURE__ */ new Set();
534
559
  return e.state.doc.descendants((e) => {
535
560
  e.type.name === "variable" && t.add(e.attrs.name);
536
561
  }), [...t];
537
562
  }
538
- function De() {
563
+ function je() {
539
564
  let e = document.documentElement;
540
565
  e.classList.add("le-printing"), window.addEventListener("afterprint", () => e.classList.remove("le-printing"), { once: !0 }), window.print();
541
566
  }
542
- function Oe(e, t) {
567
+ function Me(e, t) {
543
568
  let n = document.createElement("a");
544
569
  n.href = URL.createObjectURL(e), n.download = t, n.click(), setTimeout(() => URL.revokeObjectURL(n.href), 1e3);
545
570
  }
546
- function ke({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, editable: u = !0, autoFees: d, searchCases: f, clauses: p, hwpWasmUrl: ee }) {
571
+ function Ne({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, editable: u = !0, autoFees: d, searchCases: f, clauses: p, hwpWasmUrl: ee }) {
547
572
  let [v, b] = t(s), [x, S] = t([]), [C, te] = t(""), [w, T] = t(null), [E, D] = t(""), [O, A] = t(""), j = d ? re(x, v, d === !0 ? {} : d) : v, N = r({
548
573
  extensions: [
549
574
  i.configure({ heading: { levels: [
@@ -560,9 +585,9 @@ function ke({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
560
585
  editable: u,
561
586
  immediatelyRender: !1,
562
587
  editorProps: { attributes: { class: "legal-doc" } },
563
- onCreate: ({ editor: e }) => S(Ee(e)),
588
+ onCreate: ({ editor: e }) => S($(e)),
564
589
  onUpdate: ({ editor: e }) => {
565
- S(Ee(e)), c?.(e.getHTML());
590
+ S($(e)), c?.(e.getHTML());
566
591
  }
567
592
  });
568
593
  e(() => {
@@ -583,14 +608,14 @@ function ke({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
583
608
  }, I = async (e) => {
584
609
  let t = e.target.files?.[0];
585
610
  if (e.target.value = "", t && N) try {
586
- N.commands.setContent(_(await Te(t)), { emitUpdate: !0 });
611
+ N.commands.setContent(_(await Ae(t)), { emitUpdate: !0 });
587
612
  } catch (e) {
588
613
  window.alert(`파일을 열 수 없어요: ${e.message}`);
589
614
  }
590
615
  }, L = async (e, t) => {
591
616
  let n = N.state.doc.firstChild?.textContent.trim() || "문서";
592
617
  try {
593
- Oe(await e(N.getJSON(), j), `${n}.${t}`);
618
+ Me(await e(N.getJSON(), j), `${n}.${t}`);
594
619
  } catch (e) {
595
620
  window.alert(`저장하지 못했어요: ${e.message}`);
596
621
  }
@@ -718,7 +743,7 @@ function ke({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
718
743
  }),
719
744
  /* @__PURE__ */ m("button", {
720
745
  type: "button",
721
- onClick: De,
746
+ onClick: je,
722
747
  children: "인쇄 · PDF"
723
748
  })
724
749
  ]
@@ -815,7 +840,7 @@ function ke({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
815
840
  }
816
841
  //#endregion
817
842
  //#region src/templates.ts
818
- var Ae = [
843
+ var Pe = [
819
844
  {
820
845
  id: "complaint-loan",
821
846
  title: "소장 (대여금)",
@@ -840,7 +865,7 @@ var Ae = [
840
865
  description: "재판 없이 법원의 지급명령을 받는 독촉절차 신청서. 인지대는 소장의 10분의 1, 송달료는 당사자 수 × 6회분",
841
866
  html: "\n<h1>지 급 명 령 신 청 서</h1>\n<p>채 권 자 {{채권자 이름}}</p>\n<p>주소 {{채권자 주소}}</p>\n<p>연락처 {{채권자 연락처}}</p>\n<p>채 무 자 {{채무자 이름}}</p>\n<p>주소 {{채무자 주소}}</p>\n<p>대여금 청구 독촉사건</p>\n<p>청구금액 {{청구금액}}</p>\n<h3>신 청 취 지</h3>\n<p>채무자는 채권자에게 아래 청구금액 및 독촉절차비용을 지급하라는 명령을 구합니다.</p>\n<p data-num=\"1\">{{청구금액}}</p>\n<p data-num=\"1\">위 금액에 대하여 {{변제기 다음 날}}부터 이 사건 지급명령정본을 송달받은 날까지는 연 5%의, 그 다음 날부터 다 갚는 날까지는 연 12%의 각 비율로 계산한 돈</p>\n<p data-num=\"1\">독촉절차비용 {{독촉절차비용}} (인지대 {{독촉절차 인지대}}, 송달료 {{독촉절차 송달료}})</p>\n<h3>신 청 이 유</h3>\n<p data-num=\"1\">채권자는 {{대여일}} 채무자에게 {{청구금액}}을 변제기 {{변제기}}로 정하여 빌려주었습니다.</p>\n<p data-num=\"1\">그러나 채무자는 변제기가 지났는데도 위 돈을 갚지 않고 있습니다.</p>\n<p data-num=\"1\">따라서 채권자는 채무자에게 위 청구금액과 지연손해금 및 독촉절차비용의 지급을 구하기 위하여 이 신청에 이르렀습니다.</p>\n<h3>첨 부 서 류</h3>\n<p data-num=\"1\">차용증 사본 1통</p>\n<p data-num=\"1\">송달료 납부서 1통</p>\n<p>{{작성일}}</p>\n<p>채권자 {{채권자 이름}} (서명 또는 날인)</p>\n<p>{{관할 법원}} 귀중</p>\n"
842
867
  }
843
- ], je = [
868
+ ], Fe = [
844
869
  {
845
870
  id: "purpose",
846
871
  title: "목적",
@@ -909,4 +934,4 @@ var Ae = [
909
934
  }
910
935
  ];
911
936
  //#endregion
912
- export { M as CitationLink, ke as LegalEditor, y as Numbering, I as SERVICE_ROUNDS, L as SERVICE_UNIT_FEE, z as SMALL_CLAIM_LIMIT, g as Variable, F as calcPaymentOrderStampFee, R as calcServiceFee, N as calcStampFee, je as clauses, ee as fillTemplate, O as findCitations, H as formatAmount, k as formatCaseCitation, Se as fromDoc, $ as fromDocx, Te as fromFile, xe as fromHwp, Q as normalizeLegalHtml, se as parseAmount, Ae as templates, _ as toChips, pe as toDocx, me as toHwp, q as toHwpx, V as toKoreanAmount, de as toPlainHtml, re as withCourtFees };
937
+ export { M as CitationLink, Ne as LegalEditor, y as Numbering, I as SERVICE_ROUNDS, L as SERVICE_UNIT_FEE, z as SMALL_CLAIM_LIMIT, g as Variable, F as calcPaymentOrderStampFee, R as calcServiceFee, N as calcStampFee, Fe as clauses, ee as fillTemplate, O as findCitations, H as formatAmount, k as formatCaseCitation, De as fromDoc, Te as fromDocx, Ae as fromFile, Ee as fromHwp, Q as normalizeLegalHtml, se as parseAmount, Pe as templates, _ as toChips, pe as toDocx, me as toHwp, q as toHwpx, V as toKoreanAmount, de as toPlainHtml, re as withCourtFees };
package/dist/io.d.ts CHANGED
@@ -15,6 +15,13 @@ export declare function toHwpx(doc: JSONContent, values: Values): Promise<Blob>;
15
15
  export declare function toHwp(doc: JSONContent, values: Values, opts?: {
16
16
  wasm?: string | URL | BufferSource;
17
17
  }): Promise<Blob>;
18
+ /**
19
+ * "제N조"를 뗀 나머지에서 조 제목이 차지하는 글자 수.
20
+ * - 괄호 제목 "(목적) 본문" → 괄호까지 (괄호 안 괄호도 짝을 셈)
21
+ * - 괄호 없는 짧은 한 줄 "목적" → 전체
22
+ * - 괄호 없이 문장 "갑은 …한다." → 0 (제목 없음, 전부 본문)
23
+ */
24
+ export declare function articleTitleLength(text: string): number;
18
25
  /**
19
26
  * 문단으로만 된 법률문서 HTML(한글·워드에서 온 것)을 에디터 구조로 정리.
20
27
  * - 빈 줄 문단 제거, 짧은 첫 줄 → 제목(h1), "청 구 취 지" 같은 소제목 → h3, [빈칸] → {{변수}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "legal-doc-editor",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "법률문서(계약서·소장·고소장) 생성용 React 에디터 — 조항·번호 자동 매김, {{변수}} 채우기, 한글(hwp·hwpx)·Word(docx) 열기·저장",
5
5
  "license": "MIT",
6
6
  "repository": {