legal-doc-editor 0.3.0 → 0.5.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
@@ -7,9 +7,10 @@
7
7
  - **조·항·호 자동 번호**: 조를 넣거나 옮기면 제1조, ①, 1. 번호가 알아서 다시 매겨져요
8
8
  - **`{{변수}}` 채우기**: 본문에 `{{갑}}`을 입력하면 입력칸이 생기고, 값을 넣으면 문서에 반영돼요
9
9
  - **한글(.hwp·.hwpx)·Word(.docx) 열기·저장**: 일반 문단으로 쓴 계약서도 "제N조", "①", "1." 패턴을 인식해서 조·항·호 구조로 바꿔요
10
- - 저장은 `.docx`와 `.hwpx`예요. `.hwpx`는 한글 2014 이상에서 열리고, 한글에서 `.hwp`로 다시 저장할 수 있어요
10
+ - 저장은 `.docx`, `.hwpx`, `.hwp`(HWP 5.0)예요. `.hwpx`는 한글 2014 이상에서 열려요
11
11
  - 파일 변환 라이브러리는 필요할 때만 불러와서 에디터 본체 번들은 가벼워요
12
12
  - **판례·법령 인용 링크**: 본문의 `민법 제750조`, `대법원 2016. 4. 28. 선고 2015다12345 판결`, `2024가단157033` 같은 인용을 알아보고 [국가법령정보센터](https://www.law.go.kr) 링크를 붙여요 (API 키가 필요 없어요)
13
+ - **조항 라이브러리**: 비밀유지·계약해지·관할법원 같은 자주 쓰는 조항을 골라 커서 위치에 넣어요. 조 번호는 알아서 다시 매겨져요
13
14
  - **문서 템플릿**: 소장(대여금)·고소장(사기)·내용증명·지급명령 신청서를 `{{변수}}` 빈칸이 들어간 상태로 바로 시작해요
14
15
  - **금액 표기**: 선택한 숫자를 `금 37,200,000원`이나 `금 삼천칠백이십만 원정(₩37,200,000)`으로 바꿔요
15
16
  - **인쇄·PDF**: 비어 있는 변수는 손으로 쓰는 밑줄 칸으로 인쇄돼요
@@ -67,6 +68,35 @@ export default function Page() {
67
68
 
68
69
  날짜(`2026. 9. 2.`), 전화번호, 계좌번호는 인용으로 보지 않아요. 다만 인용 인식은 글자 모양만 보는 것이라 놓치거나 잘못 잡을 수 있고, 링크가 가리키는 조문이 실제로 맞는지는 직접 확인해 주세요.
69
70
 
71
+ ### .hwp 저장
72
+
73
+ 툴바의 **한글(.hwp) 저장**은 `.hwpx`를 만든 뒤 [@rhwp/core](https://github.com/edwardkim/rhwp)(Rust→WASM, MIT)로 HWP 5.0 파일로 바꿔요.
74
+
75
+ - WASM이 약 10MB라 **처음 `.hwp`로 저장할 때만** 불러와요. 에디터 본체 번들에는 들어가지 않아요.
76
+ - Vite를 쓰면 사전 번들링이 WASM 경로를 잃지 않도록 제외해 주세요.
77
+
78
+ ```ts
79
+ // vite.config.ts
80
+ export default defineConfig({ optimizeDeps: { exclude: ['@rhwp/core'] } })
81
+ ```
82
+
83
+ - 그래도 WASM을 못 찾으면 `node_modules/@rhwp/core/rhwp_bg.wasm`을 정적 폴더에 복사하고 주소를 넘겨요: `<LegalEditor hwpWasmUrl="/rhwp_bg.wasm" />`
84
+ - 만든 파일은 다시 읽었을 때 제목·조항·번호·표·글자가 그대로인지 검사하지만, **한글 프로그램에서 열리는지는 직접 확인**해 주세요.
85
+
86
+ ### 조항 라이브러리
87
+
88
+ `clauses`를 넘기면 오른쪽 패널에 조항 목록이 생겨요. 검색칸에서 제목이나 분류로 찾고, 누르면 커서 위치에 조항이 들어가요. 조항 안의 `{{변수}}`는 입력칸에 바로 나타나고 뒤쪽 조 번호는 알아서 밀려요.
89
+
90
+ ```tsx
91
+ import { LegalEditor, clauses } from 'legal-doc-editor'
92
+
93
+ <LegalEditor clauses={clauses} />
94
+ // 내 조항 더하기
95
+ <LegalEditor clauses={[...clauses, { id: 'my-sla', title: '서비스 수준', category: '의무', html: '<h2>(서비스 수준)</h2><p>…</p>' }]} />
96
+ ```
97
+
98
+ 기본 조항: 목적, 계약기간(자동 연장), 비밀유지, 손해배상, 계약의 해제·해지, 불가항력, 권리·의무의 양도 금지, 통지, 분쟁 해결(관할법원), 완전합의 및 계약의 변경, 계약의 해석. 일반적인 경우를 위한 예시 문장이니 계약에 맞게 고쳐 쓰세요.
99
+
70
100
  ### 판례 검색
71
101
 
72
102
  `searchCases`에 검색 함수를 넘기면 오른쪽 패널에 **판례 검색** 칸이 생겨요. 결과를 누르면 커서 위치에 `대법원 2016. 4. 28. 선고 2015다12345 판결` 같은 인용 문구가 들어가고, 인용 링크도 자동으로 붙어요.
@@ -108,7 +138,8 @@ export default function Page() {
108
138
 
109
139
  | 이름 | 설명 |
110
140
  | --- | --- |
111
- | `<LegalEditor content values onChange onValuesChange editable autoFees searchCases />` | 에디터 컴포넌트 |
141
+ | `<LegalEditor content values onChange onValuesChange editable autoFees searchCases clauses hwpWasmUrl />` | 에디터 컴포넌트 |
142
+ | `clauses` | 기본 조항 목록 `{ id, title, category, html }[]` |
112
143
  | `formatCaseCitation(result)` | 판례 검색 결과 → `대법원 2016. 4. 28. 선고 2015다12345 판결` |
113
144
  | `calcStampFee(소가, { electronic })` | 소장 인지액(원) |
114
145
  | `calcPaymentOrderStampFee(청구금액)` | 지급명령 신청서 인지대(원) — 종이 신청 기준 |
@@ -121,6 +152,7 @@ export default function Page() {
121
152
  | `fillTemplate(html, values)` | 변수를 채운 완성본 HTML. `.legal-doc` 안에서 렌더하면 번호가 붙어요 |
122
153
  | `toDocx(editor.getJSON(), values)` | `.docx` Blob 생성 |
123
154
  | `toHwpx(editor.getJSON(), values)` | `.hwpx` Blob 생성 |
155
+ | `toHwp(editor.getJSON(), values, { wasm })` | `.hwp`(HWP 5.0) Blob 생성. `wasm`은 WASM 주소나 바이트(선택) |
124
156
  | `toPlainHtml(editor.getJSON(), values)` | 번호가 텍스트로 들어간 독립 HTML |
125
157
  | `fromFile(file)` | `.docx` / `.hwp` / `.hwpx`를 에디터용 HTML로 변환 (`fromDocx`, `fromHwp`도 있음) |
126
158
  | `normalizeLegalHtml(html)` | "제N조 / ① / 1." 문단을 조·항·호 구조로 변환 |
@@ -132,7 +164,7 @@ export default function Page() {
132
164
  ## 로드맵
133
165
 
134
166
  - [x] HWP / HWPX 열기, HWPX 저장
135
- - [ ] `.hwp` 바이너리로 바로 저장 ([rhwp](https://github.com/edwardkim/rhwp) 검토)
167
+ - [x] `.hwp` 바이너리로 바로 저장 ([rhwp](https://github.com/edwardkim/rhwp))
136
168
  - [ ] 옛 Word `.doc` 열기
137
169
  - [ ] 표
138
170
  - [ ] 조항 라이브러리(자주 쓰는 조항 끼워 넣기)
@@ -1,5 +1,6 @@
1
1
  import { type Values } from './variable';
2
2
  import { type CaseResult } from './citation';
3
+ import type { Clause } from './clauses';
3
4
  import { type CourtFeeOptions } from './fees';
4
5
  import './legal.css';
5
6
  export interface LegalEditorProps {
@@ -14,5 +15,9 @@ export interface LegalEditorProps {
14
15
  autoFees?: boolean | CourtFeeOptions;
15
16
  /** 판례 검색 함수. 넘기면 오른쪽에 판례 검색 칸이 생기고, 결과를 누르면 인용 문구가 커서 위치에 들어감 */
16
17
  searchCases?: (query: string) => Promise<CaseResult[]>;
18
+ /** 조항 목록. 넘기면 오른쪽에 조항 라이브러리가 생기고, 누르면 커서 위치에 조항이 들어감 (기본 제공 목록: `clauses`) */
19
+ clauses?: Clause[];
20
+ /** .hwp 저장에 쓰는 @rhwp/core WASM 주소. 번들러가 WASM 경로를 못 찾을 때만 지정 (예: '/rhwp_bg.wasm') */
21
+ hwpWasmUrl?: string;
17
22
  }
18
- export declare function LegalEditor({ content, values: initial, onChange, onValuesChange, editable, autoFees, searchCases }: LegalEditorProps): import("react").JSX.Element;
23
+ export declare function LegalEditor({ content, values: initial, onChange, onValuesChange, editable, autoFees, searchCases, clauses, hwpWasmUrl }: LegalEditorProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ export interface Clause {
2
+ id: string;
3
+ title: string;
4
+ category: string;
5
+ /** 조 하나: h2(조 제목) + 본문(p) 또는 항(ol) · 호(중첩 ol). 조 번호는 자동으로 매겨지므로 적지 않는다 */
6
+ html: string;
7
+ }
8
+ export declare const clauses: Clause[];
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export { calcStampFee, calcPaymentOrderStampFee, calcServiceFee, withCourtFees,
8
8
  export type { CourtFeeOptions, Procedure } from './fees';
9
9
  export { toKoreanAmount, formatAmount, parseAmount } from './amount';
10
10
  export { templates } from './templates';
11
+ export { clauses } from './clauses';
12
+ export type { Clause } from './clauses';
11
13
  export type { Template } from './templates';
12
14
  export type { Values } from './variable';
13
- export { toDocx, toHwpx, toPlainHtml, fromDocx, fromHwp, fromFile, normalizeLegalHtml } from './io';
15
+ export { toDocx, toHwpx, toHwp, toPlainHtml, fromDocx, fromHwp, fromFile, normalizeLegalHtml } from './io';
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { Plugin as u, PluginKey as d } from "@tiptap/pm/state";
7
7
  import { Decoration as f, DecorationSet as p } from "@tiptap/pm/view";
8
8
  import { jsx as m, jsxs as h } from "react/jsx-runtime";
9
9
  //#region src/variable.ts
10
- var ee = s.create({
10
+ var g = s.create({
11
11
  name: "variable",
12
12
  group: "inline",
13
13
  inline: !0,
@@ -61,9 +61,9 @@ var ee = s.create({
61
61
  getAttributes: (e) => ({ name: e[0].slice(2, -2).trim() })
62
62
  })];
63
63
  }
64
- }), g = (e) => e.replace(/\{\{([^{}<>"]+)\}\}/g, (e, t) => `<span data-var="${t.trim().replace(/&/g, "&amp;")}"></span>`);
65
- function _(e, t) {
66
- let n = new DOMParser().parseFromString(g(e), "text/html");
64
+ }), _ = (e) => e.replace(/\{\{([^{}<>"]+)\}\}/g, (e, t) => `<span data-var="${t.trim().replace(/&/g, "&amp;")}"></span>`);
65
+ function ee(e, t) {
66
+ let n = new DOMParser().parseFromString(_(e), "text/html");
67
67
  return n.querySelectorAll("span[data-var]").forEach((e) => {
68
68
  let n = e.getAttribute("data-var");
69
69
  e.replaceWith(t[n] || `[${n}]`);
@@ -71,7 +71,7 @@ function _(e, t) {
71
71
  }
72
72
  //#endregion
73
73
  //#region src/numbering.ts
74
- var v = 4, y = o.create({
74
+ var v = 4, te = o.create({
75
75
  name: "numbering",
76
76
  priority: 1e3,
77
77
  addGlobalAttributes() {
@@ -105,31 +105,31 @@ var v = 4, y = o.create({
105
105
  Enter: () => !!e() && this.editor.state.selection.$from.parent.content.size === 0 && this.editor.commands.setNumbering(null)
106
106
  };
107
107
  }
108
- }), b = String.raw`(?<!\d)\d{4}(?![만억천년월일원])[가-힣]{1,3}\d{1,6}(?!\d)`, x = String.raw`(?:[가-힣]{0,6}법원(?:\s*[가-힣]{2,6}지원)?|헌법재판소)`, S = String.raw`\d{4}\.\s*\d{1,2}\.\s*\d{1,2}\.`, C = String.raw`${x}\s*${S}\s*(?:선고|자)\s*(?<caseNo>${b})(?:\s*[,·]\s*${b})*\s*(?:전원합의체\s*)?(?:판결|결정|명령)`, w = String.raw`(?:[가-힣]+(?<![은는이가을를와과로며고여하한된])\s+){0,4}[가-힣]+에\s*관한\s*[가-힣]*법률?|[가-힣]+법률?`, T = String.raw`(?<lawName>${w})\s*제\s*(?<jo>\d+)\s*조(?:\s*의\s*(?<ui>\d+))?(?:\s*제\s*\d+\s*(?:항|호))*`, E = new RegExp(String.raw`${C}|${T}|(?<bare>${b})`, "g"), D = /^[방불합위적탈편문어용수요기비사입공]법$/, O = (...e) => `https://www.law.go.kr/${e.map(encodeURIComponent).join("/")}`;
109
- function k(e) {
108
+ }), y = String.raw`(?<!\d)\d{4}(?![만억천년월일원])[가-힣]{1,3}\d{1,6}(?!\d)`, b = String.raw`(?:[가-힣]{0,6}법원(?:\s*[가-힣]{2,6}지원)?|헌법재판소)`, x = String.raw`\d{4}\.\s*\d{1,2}\.\s*\d{1,2}\.`, S = String.raw`${b}\s*${x}\s*(?:선고|자)\s*(?<caseNo>${y})(?:\s*[,·]\s*${y})*\s*(?:전원합의체\s*)?(?:판결|결정|명령)`, ne = String.raw`(?:[가-힣]+(?<![은는이가을를와과로며고여하한된])\s+){0,4}[가-힣]+에\s*관한\s*[가-힣]*법률?|[가-힣]+법률?`, C = String.raw`(?<lawName>${ne})\s*제\s*(?<jo>\d+)\s*조(?:\s*의\s*(?<ui>\d+))?(?:\s*제\s*\d+\s*(?:항|호))*`, w = new RegExp(String.raw`${S}|${C}|(?<bare>${y})`, "g"), T = /^[방불합위적탈편문어용수요기비사입공]법$/, E = (...e) => `https://www.law.go.kr/${e.map(encodeURIComponent).join("/")}`;
109
+ function D(e) {
110
110
  let t = [];
111
- for (let n of e.matchAll(E)) {
111
+ for (let n of e.matchAll(w)) {
112
112
  let e = n.groups, r = e.caseNo ?? e.bare;
113
- if (!r && D.test(e.lawName)) continue;
113
+ if (!r && T.test(e.lawName)) continue;
114
114
  let i = n.index ?? 0;
115
115
  t.push({
116
116
  type: r ? "case" : "law",
117
117
  text: n[0],
118
118
  from: i,
119
119
  to: i + n[0].length,
120
- url: r ? `https://www.law.go.kr/LSW/precSc.do?query=${encodeURIComponent(r)}` : O("법령", e.lawName.replace(/\s+/g, " "), `제${e.jo}조${e.ui ? `의${e.ui}` : ""}`)
120
+ url: r ? `https://www.law.go.kr/LSW/precSc.do?query=${encodeURIComponent(r)}` : E("법령", e.lawName.replace(/\s+/g, " "), `제${e.jo}조${e.ui ? `의${e.ui}` : ""}`)
121
121
  });
122
122
  }
123
123
  return t;
124
124
  }
125
- function A({ court: e, date: t, caseNo: n, kind: r = "" }) {
125
+ function O({ court: e, date: t, caseNo: n, kind: r = "" }) {
126
126
  let i = t.match(/(\d{4})\D*(\d{1,2})\D*(\d{1,2})/), a = i ? `${i[1]}. ${+i[2]}. ${+i[3]}.` : t, o = /결정/.test(r) ? "결정" : /명령/.test(r) ? "명령" : "판결";
127
127
  return o === "판결" ? `${e} ${a} 선고 ${n} 판결` : `${e} ${a}자 ${n} ${o}`;
128
128
  }
129
- var j = new d("citation"), M = (e) => {
129
+ var k = new d("citation"), A = (e) => {
130
130
  let t = [];
131
131
  return e.descendants((e, n) => {
132
- if (e.isText) for (let r of k(e.text)) t.push(f.inline(n + r.from, n + r.to, {
132
+ if (e.isText) for (let r of D(e.text)) t.push(f.inline(n + r.from, n + r.to, {
133
133
  nodeName: "a",
134
134
  class: "le-cite",
135
135
  href: r.url,
@@ -138,17 +138,17 @@ var j = new d("citation"), M = (e) => {
138
138
  rel: "noopener noreferrer"
139
139
  }));
140
140
  }), p.create(e, t);
141
- }, te = o.create({
141
+ }, j = o.create({
142
142
  name: "citationLink",
143
143
  addProseMirrorPlugins() {
144
144
  return [new u({
145
- key: j,
145
+ key: k,
146
146
  state: {
147
- init: (e, { doc: t }) => M(t),
148
- apply: (e, t) => e.docChanged ? M(e.doc) : t
147
+ init: (e, { doc: t }) => A(t),
148
+ apply: (e, t) => e.docChanged ? A(e.doc) : t
149
149
  },
150
150
  props: {
151
- decorations: (e) => j.getState(e),
151
+ decorations: (e) => k.getState(e),
152
152
  handleClick: (e, t, n) => {
153
153
  let r = n.target.closest?.("a.le-cite");
154
154
  return !r || e.editable && !n.ctrlKey && !n.metaKey ? !1 : (window.open(r.getAttribute("href"), "_blank", "noopener,noreferrer"), !0);
@@ -159,21 +159,21 @@ var j = new d("citation"), M = (e) => {
159
159
  });
160
160
  //#endregion
161
161
  //#region src/fees.ts
162
- function N(e, t = {}) {
162
+ function M(e, t = {}) {
163
163
  if (!(e > 0)) return 0;
164
- let n = P(e);
164
+ let n = N(e);
165
165
  return t.electronic ? n < 1e7 ? 900 : Math.floor(n * 9 / 1e7) * 100 : n < 1e7 ? 1e3 : Math.floor(n / 1e6) * 100;
166
166
  }
167
- function P(e) {
167
+ function N(e) {
168
168
  let [t, n] = e < 1e7 ? [50, 0] : e < 1e8 ? [45, 5e3] : e < 1e9 ? [40, 55e3] : [35, 555e3];
169
169
  return e * t + n * 1e4;
170
170
  }
171
- function F(e) {
171
+ function P(e) {
172
172
  if (!(e > 0)) return 0;
173
- let t = P(e);
173
+ let t = N(e);
174
174
  return t < 1e8 ? 1e3 : Math.floor(t / 1e7) * 100;
175
175
  }
176
- var I = {
176
+ var F = {
177
177
  소액: 10,
178
178
  단독: 15,
179
179
  합의: 15,
@@ -181,23 +181,23 @@ var I = {
181
181
  상고: 8,
182
182
  조정: 5,
183
183
  독촉: 6
184
- }, L = 5500;
185
- function R({ parties: e, procedure: t, unitFee: n = L }) {
186
- return e * I[t] * n;
184
+ }, I = 5500;
185
+ function L({ parties: e, procedure: t, unitFee: n = I }) {
186
+ return e * F[t] * n;
187
187
  }
188
- var ne = 3e7, re = (e) => `${e.toLocaleString("ko-KR")}원`, ie = (e = "") => Number(e.replace(/[^\d]/g, "")) || 0;
189
- function ae(e, t, n = {}) {
190
- let r = (n) => ie(t[e.find((e) => n.test(e)) ?? ""]), i = { ...t }, a = (n, r) => {
188
+ var R = 3e7, re = (e) => `${e.toLocaleString("ko-KR")}원`, z = (e = "") => Number(e.replace(/[^\d]/g, "")) || 0;
189
+ function ie(e, t, n = {}) {
190
+ let r = (n) => z(t[e.find((e) => n.test(e)) ?? ""]), i = { ...t }, a = (n, r) => {
191
191
  for (let a of e) n.test(a) && !t[a]?.trim() && (i[a] = re(r));
192
192
  }, o = n.parties ?? 2, s = r(/^(소가|소송목적의\s*값)/);
193
- s && (a(/^인지(액|대)/, N(s, n)), a(/^송달료/, R({
193
+ s && (a(/^인지(액|대)/, M(s, n)), a(/^송달료/, L({
194
194
  parties: o,
195
195
  procedure: s <= 3e7 ? "소액" : "단독",
196
196
  unitFee: n.unitFee
197
197
  })));
198
198
  let c = r(/^청구금액/);
199
199
  if (c && e.some((e) => /^독촉절차/.test(e))) {
200
- let e = /^독촉절차\s*인지(액|대)/, t = /^독촉절차\s*송달료/, i = F(c), s = R({
200
+ let e = /^독촉절차\s*인지(액|대)/, t = /^독촉절차\s*송달료/, i = P(c), s = L({
201
201
  parties: o,
202
202
  procedure: "독촉",
203
203
  unitFee: n.unitFee
@@ -208,50 +208,50 @@ function ae(e, t, n = {}) {
208
208
  }
209
209
  //#endregion
210
210
  //#region src/amount.ts
211
- var oe = "영일이삼사오육칠팔구", se = [
211
+ var ae = "영일이삼사오육칠팔구", oe = [
212
212
  "천",
213
213
  "백",
214
214
  "십",
215
215
  ""
216
- ], ce = [
216
+ ], se = [
217
217
  "",
218
218
  "만",
219
219
  "억",
220
220
  "조",
221
221
  "경"
222
222
  ];
223
- function z(e) {
223
+ function B(e) {
224
224
  if (!Number.isSafeInteger(e)) throw RangeError(`금액은 정수여야 해요: ${e}`);
225
225
  if (e === 0) return "영";
226
- if (e < 0) return `마이너스 ${z(-e)}`;
226
+ if (e < 0) return `마이너스 ${B(-e)}`;
227
227
  let t = "", n = String(e).padStart(Math.ceil(String(e).length / 4) * 4, "0");
228
228
  for (let e = 0; e < n.length / 4; e++) {
229
229
  let r = n.slice(e * 4, e * 4 + 4);
230
230
  if (r !== "0000") {
231
- for (let e = 0; e < 4; e++) r[e] !== "0" && (t += oe[+r[e]] + se[e]);
232
- t += ce[n.length / 4 - 1 - e];
231
+ for (let e = 0; e < 4; e++) r[e] !== "0" && (t += ae[+r[e]] + oe[e]);
232
+ t += se[n.length / 4 - 1 - e];
233
233
  }
234
234
  }
235
235
  return t;
236
236
  }
237
- var B = (e) => Number(e.replace(/[^\d]/g, "")) || 0;
238
- function V(e, t = "소장") {
237
+ var V = (e) => Number(e.replace(/[^\d]/g, "")) || 0;
238
+ function H(e, t = "소장") {
239
239
  let n = e.toLocaleString("ko-KR");
240
- return t === "소장" ? `금 ${n}원` : `금 ${z(e)} 원정(₩${n})`;
240
+ return t === "소장" ? `금 ${n}원` : `금 ${B(e)} 원정(₩${n})`;
241
241
  }
242
242
  //#endregion
243
243
  //#region src/io.ts
244
- var le = (e) => e < 20 ? String.fromCharCode(9312 + e) : `${e + 1}.`, H = "가나다라마바사아자차카타파하", U = (e) => {
244
+ var ce = (e) => e < 20 ? String.fromCharCode(9312 + e) : `${e + 1}.`, U = "가나다라마바사아자차카타파하", W = (e) => {
245
245
  let t = "";
246
- for (; e > 0; e = Math.floor(e / 14)) t = H[--e % 14] + t;
246
+ for (; e > 0; e = Math.floor(e / 14)) t = U[--e % 14] + t;
247
247
  return t;
248
- }, ue = (e, t) => [
248
+ }, le = (e, t) => [
249
249
  `${t}.`,
250
- `${U(t)}.`,
250
+ `${W(t)}.`,
251
251
  `(${t})`,
252
- `(${U(t)})`
252
+ `(${W(t)})`
253
253
  ][e - 1];
254
- function W(e, t) {
254
+ function G(e, t) {
255
255
  let n = [], r = 0, i = (e = []) => e.map((e) => {
256
256
  if (e.type === "hardBreak") return {
257
257
  text: "",
@@ -271,7 +271,7 @@ function W(e, t) {
271
271
  bold: !0
272
272
  })), o = (e, t) => e.content?.forEach((r, a) => r.content?.forEach((r, s) => {
273
273
  if (r.type === "orderedList" || r.type === "bulletList") return o(r, t + 1);
274
- let c = a + (e.attrs?.start ?? 1), l = s > 0 ? "" : e.type === "bulletList" ? "•" : t === 0 ? le(c - 1) : `${c}.`;
274
+ let c = a + (e.attrs?.start ?? 1), l = s > 0 ? "" : e.type === "bulletList" ? "•" : t === 0 ? ce(c - 1) : `${c}.`;
275
275
  n.push({
276
276
  kind: "para",
277
277
  label: l,
@@ -304,7 +304,7 @@ function W(e, t) {
304
304
  runs: a(i(t.content))
305
305
  }) : t.type === "orderedList" || t.type === "bulletList" ? o(t, 0) : l ? (s[l]++, s.fill(0, l + 1), n.push({
306
306
  kind: "para",
307
- label: ue(l, s[l]),
307
+ label: le(l, s[l]),
308
308
  depth: l,
309
309
  runs: i(t.content)
310
310
  })) : t.type === "paragraph" ? n.push({
@@ -327,29 +327,29 @@ function W(e, t) {
327
327
  };
328
328
  return c(e.content), n;
329
329
  }
330
- var de = (e) => e.replace(/[&<>"]/g, (e) => `&#${e.charCodeAt(0)};`), G = (e) => e.map((e) => {
330
+ var ue = (e) => e.replace(/[&<>"]/g, (e) => `&#${e.charCodeAt(0)};`), K = (e) => e.map((e) => {
331
331
  if (e.br) return "<br>";
332
- let t = de(e.text);
332
+ let t = ue(e.text);
333
333
  return e.bold && (t = `<b>${t}</b>`), e.italics && (t = `<i>${t}</i>`), e.underline && (t = `<u>${t}</u>`), e.strike && (t = `<s>${t}</s>`), t;
334
334
  }).join("");
335
- function K(e, t) {
336
- return W(e, t).map((e) => {
335
+ function de(e, t) {
336
+ return G(e, t).map((e) => {
337
337
  if (e.kind === "table") {
338
- let t = (e) => `<td${e.colspan > 1 ? ` colspan="${e.colspan}"` : ""}${e.rowspan > 1 ? ` rowspan="${e.rowspan}"` : ""}>${G(e.runs)}</td>`;
338
+ let t = (e) => `<td${e.colspan > 1 ? ` colspan="${e.colspan}"` : ""}${e.rowspan > 1 ? ` rowspan="${e.rowspan}"` : ""}>${K(e.runs)}</td>`;
339
339
  return `<table>${e.rows.map((e) => `<tr>${e.map(t).join("")}</tr>`).join("")}</table>`;
340
340
  }
341
- let t = (e.label ? e.kind === "article" ? `<b>${e.label}</b> ` : `${e.label} ` : "") + G(e.runs);
341
+ let t = (e.label ? e.kind === "article" ? `<b>${e.label}</b> ` : `${e.label} ` : "") + K(e.runs);
342
342
  if (e.kind === "title") return `<h1>${t}</h1>`;
343
343
  if (e.kind === "section") return `<h3>${t}</h3>`;
344
344
  let n = e.label ? e.depth - 1 : e.depth;
345
345
  return n > 0 ? `<p style="margin-left:${n * 2}em">${t}</p>` : `<p>${t}</p>`;
346
346
  }).join("\n");
347
347
  }
348
- function q(e, t) {
349
- for (let n of W(e, t)) if (n.kind === "title") return n.runs.map((e) => e.text).join("").trim() || "문서";
348
+ function fe(e, t) {
349
+ for (let n of G(e, t)) if (n.kind === "title") return n.runs.map((e) => e.text).join("").trim() || "문서";
350
350
  return "문서";
351
351
  }
352
- async function fe(e, t) {
352
+ async function pe(e, t) {
353
353
  let { AlignmentType: n, Document: r, HeadingLevel: i, Packer: a, Paragraph: o, Table: s, TableCell: c, TableRow: l, TextRun: u, WidthType: d } = await import("docx"), f = (e) => new u({
354
354
  text: e.text,
355
355
  break: e.br ? 1 : void 0,
@@ -357,7 +357,7 @@ async function fe(e, t) {
357
357
  italics: e.italics,
358
358
  strike: e.strike,
359
359
  underline: e.underline ? {} : void 0
360
- }), p = W(e, t).map((e) => e.kind === "table" ? new s({
360
+ }), p = G(e, t).map((e) => e.kind === "table" ? new s({
361
361
  width: {
362
362
  size: 100,
363
363
  type: d.PERCENTAGE
@@ -381,7 +381,7 @@ async function fe(e, t) {
381
381
  })] : [], ...e.runs.map(f)]
382
382
  }));
383
383
  return a.toBlob(new r({
384
- title: q(e, t),
384
+ title: fe(e, t),
385
385
  styles: {
386
386
  default: { document: { run: {
387
387
  font: "바탕",
@@ -406,18 +406,31 @@ async function fe(e, t) {
406
406
  sections: [{ children: p }]
407
407
  }));
408
408
  }
409
- async function pe(e, t) {
410
- let { htmlToHwpx: n } = await import("hwp-convert"), r = await n(K(e, t), { title: q(e, t) });
409
+ async function q(e, t) {
410
+ let { htmlToHwpx: n } = await import("hwp-convert"), r = await n(de(e, t), { title: fe(e, t) });
411
411
  return new Blob([r], { type: "application/hwp+zip" });
412
412
  }
413
- var J = /^\s*제\s*\d+\s*조(?:의\s*\d+)?\s*/, me = /^\s*[①-⑳]\s*/, Y = /^\s*(\d+)\.\s*/, he = /^(청구취지|청구원인|신청취지|신청이유|고소취지|고소이유|고발취지|고발이유|입증방법|증명방법|증거방법|증거서류|첨부서류|당사자관계|사건개요|고소인|피고소인|범죄사실|증거자료|관련사건의수사및재판여부|기타)$/, ge = (e) => e.length <= 20 && (/^[가-힣](\s+[가-힣]){3,7}$/.test(e) || he.test(e.replace(/\s/g, ""))), _e = /\[\s*([^[\]{}<>"\d=]{1,20}?)\s*\]/g, ve = [
413
+ var J;
414
+ async function me(e, t, n = {}) {
415
+ let r = new Uint8Array(await (await q(e, t)).arrayBuffer());
416
+ J ??= import("@rhwp/core").then(async (e) => (await e.default(n.wasm === void 0 ? void 0 : { module_or_path: n.wasm }), e));
417
+ let { HwpDocument: i } = await J.catch((e) => {
418
+ throw J = void 0, e;
419
+ }), a = new i(r);
420
+ try {
421
+ return new Blob([a.exportHwp()], { type: "application/x-hwp" });
422
+ } finally {
423
+ a.free();
424
+ }
425
+ }
426
+ var Y = /^\s*제\s*\d+\s*조(?:의\s*\d+)?\s*/, he = /^\s*[①-⑳]\s*/, X = /^\s*(\d+)\.\s*/, ge = /^(청구취지|청구원인|신청취지|신청이유|고소취지|고소이유|고발취지|고발이유|입증방법|증명방법|증거방법|증거서류|첨부서류|당사자관계|사건개요|고소인|피고소인|범죄사실|증거자료|관련사건의수사및재판여부|기타)$/, _e = (e) => e.length <= 20 && (/^[가-힣](\s+[가-힣]){3,7}$/.test(e) || ge.test(e.replace(/\s/g, ""))), ve = /\[\s*([^[\]{}<>"\d=]{1,20}?)\s*\]/g, ye = [
414
427
  [/^\s*(\d{1,2})\.(?!\d)\s*/, Number],
415
- [/^\s*([가나다라마바사아자차카타파하])\.\s*/, (e) => H.indexOf(e) + 1],
428
+ [/^\s*([가나다라마바사아자차카타파하])\.\s*/, (e) => U.indexOf(e) + 1],
416
429
  [/^\s*\((\d{1,2})\)\s*/, Number],
417
- [/^\s*\(([가나다라마바사아자차카타파하])\)\s*/, (e) => H.indexOf(e) + 1]
430
+ [/^\s*\(([가나다라마바사아자차카타파하])\)\s*/, (e) => U.indexOf(e) + 1]
418
431
  ];
419
- function ye(e) {
420
- for (let [t, [n, r]] of ve.entries()) {
432
+ function be(e) {
433
+ for (let [t, [n, r]] of ye.entries()) {
421
434
  let i = e.match(n);
422
435
  if (i) return {
423
436
  level: t + 1,
@@ -427,23 +440,23 @@ function ye(e) {
427
440
  }
428
441
  return null;
429
442
  }
430
- function X(e, t) {
443
+ function Z(e, t) {
431
444
  let n = typeof t == "number" ? t : e.textContent.match(t)[0].length, r = e.ownerDocument.createTreeWalker(e, NodeFilter.SHOW_TEXT);
432
445
  for (let e = r.nextNode(); e && n > 0; e = r.nextNode()) {
433
446
  let t = Math.min(n, e.data.length);
434
447
  e.data = e.data.slice(t), n -= t;
435
448
  }
436
449
  }
437
- function Z(e) {
450
+ function Q(e) {
438
451
  let t = new DOMParser().parseFromString(e, "text/html");
439
452
  for (let e of [...t.body.children]) e.tagName === "P" && !e.textContent.trim() && !e.querySelector("img") && e.remove();
440
453
  let n = t.createTreeWalker(t.body, NodeFilter.SHOW_TEXT);
441
454
  for (let e = n.nextNode(); e; e = n.nextNode()) {
442
455
  let t = e.parentElement?.closest("p,li,td,th,h1,h2,h3")?.textContent?.trim();
443
- e.data = e.data.replace(_e, (e, n) => e.trim() === t ? e : `{{${n.trim()}}}`);
456
+ e.data = e.data.replace(ve, (e, n) => e.trim() === t ? e : `{{${n.trim()}}}`);
444
457
  }
445
458
  let r = t.body.firstElementChild, i = r?.textContent.trim() ?? "";
446
- if (!t.querySelector("h1") && r?.tagName === "P" && i.length <= 30 && !/[.다]$/.test(i) && !J.test(i)) {
459
+ if (!t.querySelector("h1") && r?.tagName === "P" && i.length <= 30 && !/[.다]$/.test(i) && !Y.test(i)) {
447
460
  let e = t.createElement("h1");
448
461
  e.textContent = i, r.replaceWith(e);
449
462
  }
@@ -462,62 +475,62 @@ function Z(e) {
462
475
  ];
463
476
  for (let e of [...t.body.children]) {
464
477
  let n = e.textContent ?? "", r = n.replace(/\s+/g, " ").trim();
465
- if (e.tagName === "P" && ge(r)) {
478
+ if (e.tagName === "P" && _e(r)) {
466
479
  let n = t.createElement("h3");
467
480
  n.textContent = r, e.replaceWith(n), a = null, o.fill(0), s.fill(0);
468
- } else if (e.tagName === "P" && J.test(n)) {
481
+ } else if (e.tagName === "P" && Y.test(n)) {
469
482
  let n = t.createElement("h2");
470
- n.innerHTML = e.innerHTML, X(n, J), e.replaceWith(n), a = null, o.fill(0), s.fill(0);
471
- } else if (e.tagName === "P" && me.test(n)) {
483
+ n.innerHTML = e.innerHTML, Z(n, Y), e.replaceWith(n), a = null, o.fill(0), s.fill(0);
484
+ } else if (e.tagName === "P" && he.test(n)) {
472
485
  let r = n.trim().charCodeAt(0) - 9311;
473
- (!a || a.children.length + Number(a.getAttribute("start") ?? 1) !== r) && (e.before(a = t.createElement("ol")), r > 1 && a.setAttribute("start", String(r))), X(e, me);
486
+ (!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);
474
487
  let i = t.createElement("li");
475
488
  i.append(e), a.append(i);
476
- } else if (e.tagName === "P" && a && Y.test(n) && Number(n.match(Y)[1]) === (a.lastElementChild.querySelector(":scope > ol")?.children.length ?? 0) + 1) {
489
+ } else if (e.tagName === "P" && a && X.test(n) && Number(n.match(X)[1]) === (a.lastElementChild.querySelector(":scope > ol")?.children.length ?? 0) + 1) {
477
490
  let n = a.lastElementChild, r = n.querySelector(":scope > ol") ?? n.appendChild(t.createElement("ol"));
478
- X(e, Y);
491
+ Z(e, X);
479
492
  let i = t.createElement("li");
480
493
  i.append(e), r.append(i);
481
494
  } else {
482
495
  a = null;
483
- let t = e.tagName === "P" ? ye(n) : null;
496
+ let t = e.tagName === "P" ? be(n) : null;
484
497
  if (!t) continue;
485
- s[t.level] && t.n === s[t.level] + 1 ? s[t.level]++ : t.n === o[t.level] + 1 ? (o[t.level]++, o.fill(0, t.level + 1), s.fill(0), X(e, t.length), e.setAttribute("data-num", String(t.level))) : s[t.level] = +(t.n === 1);
498
+ s[t.level] && t.n === s[t.level] + 1 ? s[t.level]++ : t.n === o[t.level] + 1 ? (o[t.level]++, o.fill(0, t.level + 1), s.fill(0), Z(e, t.length), e.setAttribute("data-num", String(t.level))) : s[t.level] = +(t.n === 1);
486
499
  }
487
500
  }
488
501
  return t.body.innerHTML;
489
502
  }
490
- async function be(e) {
503
+ async function $(e) {
491
504
  let { default: t } = await import("mammoth"), { value: n } = await t.convertToHtml({ arrayBuffer: await e.arrayBuffer() });
492
- return Z(n);
505
+ return Q(n);
493
506
  }
494
507
  async function xe(e) {
495
508
  let { detectFormat: t, hwpToHwpx: n, HwpxReader: r } = await import("hwp-convert"), i = new Uint8Array(await e.arrayBuffer());
496
509
  t(i) === "hwp" && (i = new Uint8Array(await n(i)));
497
510
  let a = new r();
498
- return await a.loadFromArrayBuffer(i.buffer), Z(await a.extractHtml());
511
+ return await a.loadFromArrayBuffer(i.buffer), Q(await a.extractHtml());
499
512
  }
500
- function Q(e) {
501
- return /\.docx$/i.test(e.name) ? be(e) : /\.hwpx?$/i.test(e.name) ? xe(e) : Promise.reject(/* @__PURE__ */ Error(".docx, .hwp, .hwpx 파일만 열 수 있어요"));
513
+ function Se(e) {
514
+ return /\.docx$/i.test(e.name) ? $(e) : /\.hwpx?$/i.test(e.name) ? xe(e) : Promise.reject(/* @__PURE__ */ Error(".docx, .hwp, .hwpx 파일만 열 수 있어요"));
502
515
  }
503
516
  //#endregion
504
517
  //#region src/LegalEditor.tsx
505
- function $(e) {
518
+ function Ce(e) {
506
519
  let t = /* @__PURE__ */ new Set();
507
520
  return e.state.doc.descendants((e) => {
508
521
  e.type.name === "variable" && t.add(e.attrs.name);
509
522
  }), [...t];
510
523
  }
511
- function Se() {
524
+ function we() {
512
525
  let e = document.documentElement;
513
526
  e.classList.add("le-printing"), window.addEventListener("afterprint", () => e.classList.remove("le-printing"), { once: !0 }), window.print();
514
527
  }
515
- function Ce(e, t) {
528
+ function Te(e, t) {
516
529
  let n = document.createElement("a");
517
530
  n.href = URL.createObjectURL(e), n.download = t, n.click(), setTimeout(() => URL.revokeObjectURL(n.href), 1e3);
518
531
  }
519
- function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, editable: u = !0, autoFees: d, searchCases: f }) {
520
- let [p, _] = t(s), [v, b] = t([]), [x, S] = t(""), [C, w] = t(null), [T, E] = t(""), D = d ? ae(v, p, d === !0 ? {} : d) : p, O = r({
532
+ function Ee({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, editable: u = !0, autoFees: d, searchCases: f, clauses: p, hwpWasmUrl: ee }) {
533
+ let [v, y] = t(s), [b, x] = t([]), [S, ne] = t(""), [C, w] = t(null), [T, E] = t(""), [D, k] = t(""), A = d ? ie(b, v, d === !0 ? {} : d) : v, M = r({
521
534
  extensions: [
522
535
  i.configure({ heading: { levels: [
523
536
  1,
@@ -525,46 +538,50 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
525
538
  3
526
539
  ] } }),
527
540
  a,
528
- ee,
529
- y,
530
- te
541
+ g,
542
+ te,
543
+ j
531
544
  ],
532
- content: g(o),
545
+ content: _(o),
533
546
  editable: u,
534
547
  immediatelyRender: !1,
535
548
  editorProps: { attributes: { class: "legal-doc" } },
536
- onCreate: ({ editor: e }) => b($(e)),
549
+ onCreate: ({ editor: e }) => x(Ce(e)),
537
550
  onUpdate: ({ editor: e }) => {
538
- b($(e)), c?.(e.getHTML());
551
+ x(Ce(e)), c?.(e.getHTML());
539
552
  }
540
553
  });
541
554
  e(() => {
542
- O && (O.storage.variable.values = D, O.storage.variable.views.forEach((e) => e()));
543
- }, [O, D]);
544
- let k = (e, t) => {
555
+ M && (M.storage.variable.values = A, M.storage.variable.views.forEach((e) => e()));
556
+ }, [M, A]);
557
+ let N = (e, t) => {
545
558
  let n = {
546
- ...p,
559
+ ...v,
547
560
  [e]: t
548
561
  };
549
- _(n), l?.(n);
550
- }, j = () => {
562
+ y(n), l?.(n);
563
+ }, P = () => {
551
564
  let e = window.prompt("변수 이름 (예: 갑, 계약일)")?.replace(/[{}<>"]/g, "").trim();
552
- e && O?.chain().focus().insertContent({
565
+ e && M?.chain().focus().insertContent({
553
566
  type: "variable",
554
567
  attrs: { name: e }
555
568
  }).run();
556
- }, M = async (e) => {
569
+ }, F = async (e) => {
557
570
  let t = e.target.files?.[0];
558
- if (e.target.value = "", t && O) try {
559
- O.commands.setContent(g(await Q(t)), { emitUpdate: !0 });
571
+ if (e.target.value = "", t && M) try {
572
+ M.commands.setContent(_(await Se(t)), { emitUpdate: !0 });
560
573
  } catch (e) {
561
574
  window.alert(`파일을 열 수 없어요: ${e.message}`);
562
575
  }
563
- }, N = async (e, t) => {
564
- let n = O.state.doc.firstChild?.textContent.trim() || "문서";
565
- Ce(await e(O.getJSON(), D), `${n}.${t}`);
566
- }, P = async () => {
567
- let e = x.trim();
576
+ }, I = async (e, t) => {
577
+ let n = M.state.doc.firstChild?.textContent.trim() || "문서";
578
+ try {
579
+ Te(await e(M.getJSON(), A), `${n}.${t}`);
580
+ } catch (e) {
581
+ window.alert(`저장하지 못했어요: ${e.message}`);
582
+ }
583
+ }, L = async () => {
584
+ let e = S.trim();
568
585
  if (f && e) {
569
586
  E("검색 중…");
570
587
  try {
@@ -573,33 +590,33 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
573
590
  w(null), E(`검색하지 못했어요: ${e.message}`);
574
591
  }
575
592
  }
576
- }, F = () => O.chain().focus(), I = (e) => F().setNumbering(O.getAttributes("paragraph").num === e ? null : e).run(), L = (e) => {
577
- let { from: t, to: n } = O.state.selection, r = B(O.state.doc.textBetween(t, n));
578
- r && F().insertContentAt({
593
+ }, R = () => M.chain().focus(), re = (e) => R().setNumbering(M.getAttributes("paragraph").num === e ? null : e).run(), z = (e) => {
594
+ let { from: t, to: n } = M.state.selection, r = V(M.state.doc.textBetween(t, n));
595
+ r && R().insertContentAt({
579
596
  from: t,
580
597
  to: n
581
- }, V(r, e)).run();
598
+ }, H(r, e)).run();
582
599
  };
583
600
  return /* @__PURE__ */ h("div", {
584
601
  className: "le-root",
585
602
  children: [/* @__PURE__ */ h("div", {
586
603
  className: "le-main",
587
- children: [u && O && /* @__PURE__ */ h("div", {
604
+ children: [u && M && /* @__PURE__ */ h("div", {
588
605
  className: "le-toolbar",
589
606
  children: [
590
607
  /* @__PURE__ */ m("button", {
591
608
  type: "button",
592
- onClick: () => F().toggleHeading({ level: 1 }).run(),
609
+ onClick: () => R().toggleHeading({ level: 1 }).run(),
593
610
  children: "제목"
594
611
  }),
595
612
  /* @__PURE__ */ m("button", {
596
613
  type: "button",
597
- onClick: () => F().toggleHeading({ level: 3 }).run(),
614
+ onClick: () => R().toggleHeading({ level: 3 }).run(),
598
615
  children: "소제목"
599
616
  }),
600
617
  /* @__PURE__ */ m("button", {
601
618
  type: "button",
602
- onClick: () => F().toggleHeading({ level: 2 }).run(),
619
+ onClick: () => R().toggleHeading({ level: 2 }).run(),
603
620
  children: "조"
604
621
  }),
605
622
  [
@@ -610,37 +627,37 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
610
627
  ].map((e, t) => /* @__PURE__ */ m("button", {
611
628
  type: "button",
612
629
  title: "번호 문단 — Tab·Shift+Tab 단계 변경, 맨 앞 Backspace 해제",
613
- onClick: () => I(t + 1),
630
+ onClick: () => re(t + 1),
614
631
  children: e
615
632
  }, e)),
616
633
  /* @__PURE__ */ m("button", {
617
634
  type: "button",
618
- onClick: () => F().toggleOrderedList().run(),
635
+ onClick: () => R().toggleOrderedList().run(),
619
636
  children: "항"
620
637
  }),
621
638
  /* @__PURE__ */ m("button", {
622
639
  type: "button",
623
- onClick: () => F().sinkListItem("listItem").run(),
640
+ onClick: () => R().sinkListItem("listItem").run(),
624
641
  children: "호 →"
625
642
  }),
626
643
  /* @__PURE__ */ m("button", {
627
644
  type: "button",
628
- onClick: () => F().liftListItem("listItem").run(),
645
+ onClick: () => R().liftListItem("listItem").run(),
629
646
  children: "← 내어쓰기"
630
647
  }),
631
648
  /* @__PURE__ */ m("button", {
632
649
  type: "button",
633
- onClick: () => F().toggleBold().run(),
650
+ onClick: () => R().toggleBold().run(),
634
651
  children: /* @__PURE__ */ m("b", { children: "B" })
635
652
  }),
636
653
  /* @__PURE__ */ h("button", {
637
654
  type: "button",
638
- onClick: j,
655
+ onClick: P,
639
656
  children: ["{{ }}", " 변수"]
640
657
  }),
641
658
  /* @__PURE__ */ m("button", {
642
659
  type: "button",
643
- onClick: () => F().insertTable({
660
+ onClick: () => R().insertTable({
644
661
  rows: 3,
645
662
  cols: 3,
646
663
  withHeaderRow: !1
@@ -650,13 +667,13 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
650
667
  /* @__PURE__ */ m("button", {
651
668
  type: "button",
652
669
  title: "선택한 금액 → 금 37,200,000원",
653
- onClick: () => L("소장"),
670
+ onClick: () => z("소장"),
654
671
  children: "금액"
655
672
  }),
656
673
  /* @__PURE__ */ m("button", {
657
674
  type: "button",
658
675
  title: "선택한 금액 → 금 삼천칠백이십만 원정(₩37,200,000)",
659
- onClick: () => L("계약서"),
676
+ onClick: () => z("계약서"),
660
677
  children: "금액(한글)"
661
678
  }),
662
679
  /* @__PURE__ */ m("span", { className: "le-spacer" }),
@@ -666,31 +683,37 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
666
683
  type: "file",
667
684
  accept: ".docx,.hwp,.hwpx",
668
685
  hidden: !0,
669
- onChange: M
686
+ onChange: F
670
687
  })]
671
688
  }),
672
689
  /* @__PURE__ */ m("button", {
673
690
  type: "button",
674
- onClick: () => N(fe, "docx"),
691
+ onClick: () => I(pe, "docx"),
675
692
  children: "Word 저장"
676
693
  }),
677
694
  /* @__PURE__ */ m("button", {
678
695
  type: "button",
679
- onClick: () => N(pe, "hwpx"),
680
- children: "한글 저장"
696
+ onClick: () => I(q, "hwpx"),
697
+ children: "한글(.hwpx) 저장"
698
+ }),
699
+ /* @__PURE__ */ m("button", {
700
+ type: "button",
701
+ title: "HWP 5.0 — 처음 저장할 때 변환기(약 10MB)를 불러와요",
702
+ onClick: () => I((e, t) => me(e, t, { wasm: ee }), "hwp"),
703
+ children: "한글(.hwp) 저장"
681
704
  }),
682
705
  /* @__PURE__ */ m("button", {
683
706
  type: "button",
684
- onClick: Se,
707
+ onClick: we,
685
708
  children: "인쇄 · PDF"
686
709
  })
687
710
  ]
688
- }), /* @__PURE__ */ m(n, { editor: O })]
711
+ }), /* @__PURE__ */ m(n, { editor: M })]
689
712
  }), /* @__PURE__ */ h("aside", {
690
713
  className: "le-panel",
691
714
  children: [
692
715
  /* @__PURE__ */ m("h3", { children: "입력값" }),
693
- v.length === 0 && /* @__PURE__ */ h("p", {
716
+ b.length === 0 && /* @__PURE__ */ h("p", {
694
717
  className: "le-hint",
695
718
  children: [
696
719
  "본문에 ",
@@ -698,11 +721,32 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
698
721
  "처럼 입력하면 변수가 생겨요."
699
722
  ]
700
723
  }),
701
- v.map((e) => /* @__PURE__ */ h("label", { children: [e, /* @__PURE__ */ m("input", {
702
- value: p[e] ?? "",
703
- placeholder: p[e] ? void 0 : D[e],
704
- onChange: (t) => k(e, t.target.value)
724
+ b.map((e) => /* @__PURE__ */ h("label", { children: [e, /* @__PURE__ */ m("input", {
725
+ value: v[e] ?? "",
726
+ placeholder: v[e] ? void 0 : A[e],
727
+ onChange: (t) => N(e, t.target.value)
705
728
  })] }, e)),
729
+ p && /* @__PURE__ */ h("section", {
730
+ className: "le-clauses",
731
+ children: [
732
+ /* @__PURE__ */ m("h3", { children: "조항 라이브러리" }),
733
+ /* @__PURE__ */ m("input", {
734
+ value: D,
735
+ placeholder: "조항 찾기 (예: 해지)",
736
+ onChange: (e) => k(e.target.value)
737
+ }),
738
+ /* @__PURE__ */ m("ul", {
739
+ className: "le-clause-list",
740
+ children: p.filter((e) => `${e.category} ${e.title}`.includes(D.trim())).map((e) => /* @__PURE__ */ m("li", { children: /* @__PURE__ */ h("button", {
741
+ type: "button",
742
+ disabled: !u,
743
+ title: "커서 위치에 조항 넣기",
744
+ onClick: () => M?.chain().focus().insertContent(_(e.html), { parseOptions: { preserveWhitespace: !1 } }).run(),
745
+ children: [/* @__PURE__ */ m("small", { children: e.category }), e.title]
746
+ }) }, e.id))
747
+ })
748
+ ]
749
+ }),
706
750
  f && /* @__PURE__ */ h("section", {
707
751
  className: "le-cases",
708
752
  children: [
@@ -710,15 +754,15 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
710
754
  /* @__PURE__ */ h("div", {
711
755
  className: "le-cases-bar",
712
756
  children: [/* @__PURE__ */ m("input", {
713
- value: x,
757
+ value: S,
714
758
  placeholder: "사건명·사건번호·키워드",
715
- onChange: (e) => S(e.target.value),
759
+ onChange: (e) => ne(e.target.value),
716
760
  onKeyDown: (e) => {
717
- e.key === "Enter" && !e.nativeEvent.isComposing && (e.preventDefault(), P());
761
+ e.key === "Enter" && !e.nativeEvent.isComposing && (e.preventDefault(), L());
718
762
  }
719
763
  }), /* @__PURE__ */ m("button", {
720
764
  type: "button",
721
- onClick: P,
765
+ onClick: L,
722
766
  children: "검색"
723
767
  })]
724
768
  }),
@@ -736,10 +780,10 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
736
780
  type: "button",
737
781
  disabled: !u,
738
782
  title: "커서 위치에 인용 넣기",
739
- onClick: () => O?.chain().focus().insertContent(A(e)).run(),
783
+ onClick: () => M?.chain().focus().insertContent(O(e)).run(),
740
784
  children: [
741
785
  /* @__PURE__ */ m("strong", { children: e.title }),
742
- /* @__PURE__ */ m("span", { children: A(e) }),
786
+ /* @__PURE__ */ m("span", { children: O(e) }),
743
787
  e.summary && /* @__PURE__ */ m("small", { children: e.summary })
744
788
  ]
745
789
  }), /* @__PURE__ */ m("a", {
@@ -757,7 +801,7 @@ function we({ content: o = "", values: s = {}, onChange: c, onValuesChange: l, e
757
801
  }
758
802
  //#endregion
759
803
  //#region src/templates.ts
760
- var Te = [
804
+ var De = [
761
805
  {
762
806
  id: "complaint-loan",
763
807
  title: "소장 (대여금)",
@@ -782,6 +826,73 @@ var Te = [
782
826
  description: "재판 없이 법원의 지급명령을 받는 독촉절차 신청서. 인지대는 소장의 10분의 1, 송달료는 당사자 수 × 6회분",
783
827
  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"
784
828
  }
829
+ ], Oe = [
830
+ {
831
+ id: "purpose",
832
+ title: "목적",
833
+ category: "일반",
834
+ html: "<h2>(목적)</h2>\n<p>이 계약은 {{갑}}과 {{을}} 사이의 {{계약 대상}}에 관한 권리와 의무를 정하는 것을 목적으로 한다.</p>"
835
+ },
836
+ {
837
+ id: "term",
838
+ title: "계약기간",
839
+ category: "기간",
840
+ html: "<h2>(계약기간)</h2>\n<ol>\n <li><p>이 계약의 기간은 {{시작일}}부터 {{종료일}}까지로 한다.</p></li>\n <li><p>기간 만료 {{통지 기한}} 전까지 어느 당사자도 서면으로 종료 의사를 알리지 않으면, 이 계약은 같은 조건으로 {{연장 기간}} 연장된 것으로 본다.</p></li>\n</ol>"
841
+ },
842
+ {
843
+ id: "confidentiality",
844
+ title: "비밀유지",
845
+ category: "의무",
846
+ html: "<h2>(비밀유지)</h2>\n<ol>\n <li><p>각 당사자는 이 계약과 관련하여 알게 된 상대방의 기술상·경영상 정보를 상대방의 사전 서면 동의 없이 제3자에게 공개하거나 이 계약의 목적 외에 사용하지 아니한다.</p></li>\n <li><p>제1항의 의무는 이 계약이 끝난 뒤에도 {{비밀유지 기간}} 동안 유지된다.</p></li>\n</ol>"
847
+ },
848
+ {
849
+ id: "damages",
850
+ title: "손해배상",
851
+ category: "책임",
852
+ html: "<h2>(손해배상)</h2>\n<p>어느 당사자가 이 계약을 위반하여 상대방에게 손해를 입힌 경우 그 손해를 배상하여야 한다.</p>"
853
+ },
854
+ {
855
+ id: "termination",
856
+ title: "계약의 해제·해지",
857
+ category: "종료",
858
+ html: "<h2>(계약의 해제·해지)</h2>\n<ol>\n <li><p>어느 당사자가 이 계약을 위반한 경우 상대방은 {{시정 기간}} 이상의 기간을 정하여 서면으로 시정을 요구하고, 그 기간 안에 시정되지 않으면 이 계약을 해제 또는 해지할 수 있다.</p></li>\n <li><p>다음 각 호의 어느 하나에 해당하면 상대방은 최고 없이 이 계약을 해제 또는 해지할 수 있다.</p>\n <ol>\n <li><p>어음·수표가 부도 처리되거나 파산·회생절차 개시 신청이 있는 경우</p></li>\n <li><p>강제집행, 가압류 또는 가처분을 받아 이 계약을 이행하기 어려운 경우</p></li>\n </ol>\n </li>\n <li><p>제1항 및 제2항에 따른 해제 또는 해지는 손해배상 청구에 영향을 미치지 아니한다.</p></li>\n</ol>"
859
+ },
860
+ {
861
+ id: "force-majeure",
862
+ title: "불가항력",
863
+ category: "책임",
864
+ html: "<h2>(불가항력)</h2>\n<p>천재지변, 전쟁, 감염병 확산, 정부의 조치 등 당사자가 통제할 수 없는 사유로 이 계약을 이행하지 못한 경우 그 당사자는 이행 지체나 불이행에 대한 책임을 지지 아니한다. 다만, 그 사유가 생긴 사실을 지체 없이 상대방에게 알려야 한다.</p>"
865
+ },
866
+ {
867
+ id: "no-assignment",
868
+ title: "권리·의무의 양도 금지",
869
+ category: "일반",
870
+ html: "<h2>(권리·의무의 양도 금지)</h2>\n<p>어느 당사자도 상대방의 사전 서면 동의 없이 이 계약상의 권리나 의무의 전부 또는 일부를 제3자에게 양도하거나 담보로 제공할 수 없다.</p>"
871
+ },
872
+ {
873
+ id: "notice",
874
+ title: "통지",
875
+ category: "일반",
876
+ html: "<h2>(통지)</h2>\n<ol>\n <li><p>이 계약에 따른 통지는 서면으로 하며, 각 당사자가 이 계약서에 적은 주소나 {{통지 받을 이메일}}로 보내어 상대방에게 도달한 때에 효력이 생긴다.</p></li>\n <li><p>주소나 연락처가 바뀐 당사자는 지체 없이 상대방에게 알려야 한다.</p></li>\n</ol>"
877
+ },
878
+ {
879
+ id: "dispute",
880
+ title: "분쟁 해결 (관할법원)",
881
+ category: "분쟁",
882
+ html: "<h2>(분쟁 해결)</h2>\n<ol>\n <li><p>이 계약과 관련하여 분쟁이 생기면 당사자는 먼저 성실히 협의하여 해결한다.</p></li>\n <li><p>협의로 해결되지 않은 분쟁에 관한 소송은 {{관할 법원}}을 제1심 관할법원으로 한다.</p></li>\n</ol>"
883
+ },
884
+ {
885
+ id: "entire-agreement",
886
+ title: "완전합의 및 계약의 변경",
887
+ category: "일반",
888
+ html: "<h2>(완전합의 및 계약의 변경)</h2>\n<ol>\n <li><p>이 계약은 계약 대상에 관한 당사자 사이의 완전한 합의이며, 이 계약 전에 있었던 구두 또는 서면의 합의보다 우선한다.</p></li>\n <li><p>이 계약의 변경은 당사자가 서명 또는 날인한 서면으로만 할 수 있다.</p></li>\n</ol>"
889
+ },
890
+ {
891
+ id: "interpretation",
892
+ title: "계약의 해석",
893
+ category: "일반",
894
+ html: "<h2>(계약의 해석)</h2>\n<p>이 계약에서 정하지 않은 사항이나 해석에 다툼이 있는 사항은 당사자가 서로 협의하여 정하고, 협의가 되지 않으면 관계 법령과 일반 거래 관행에 따른다.</p>"
895
+ }
785
896
  ];
786
897
  //#endregion
787
- export { te as CitationLink, we as LegalEditor, y as Numbering, I as SERVICE_ROUNDS, L as SERVICE_UNIT_FEE, ne as SMALL_CLAIM_LIMIT, ee as Variable, F as calcPaymentOrderStampFee, R as calcServiceFee, N as calcStampFee, _ as fillTemplate, k as findCitations, V as formatAmount, A as formatCaseCitation, be as fromDocx, Q as fromFile, xe as fromHwp, Z as normalizeLegalHtml, B as parseAmount, Te as templates, g as toChips, fe as toDocx, pe as toHwpx, z as toKoreanAmount, K as toPlainHtml, ae as withCourtFees };
898
+ export { j as CitationLink, Ee as LegalEditor, te as Numbering, F as SERVICE_ROUNDS, I as SERVICE_UNIT_FEE, R as SMALL_CLAIM_LIMIT, g as Variable, P as calcPaymentOrderStampFee, L as calcServiceFee, M as calcStampFee, Oe as clauses, ee as fillTemplate, D as findCitations, H as formatAmount, O as formatCaseCitation, $ as fromDocx, Se as fromFile, xe as fromHwp, Q as normalizeLegalHtml, V as parseAmount, De as templates, _ as toChips, pe as toDocx, me as toHwp, q as toHwpx, B as toKoreanAmount, de as toPlainHtml, ie as withCourtFees };
package/dist/io.d.ts CHANGED
@@ -6,6 +6,14 @@ export declare function toPlainHtml(doc: JSONContent, values: Values): string;
6
6
  export declare function toDocx(doc: JSONContent, values: Values): Promise<Blob>;
7
7
  /** 에디터 JSON + 입력값 → .hwpx Blob (한글 2014 이상에서 열림) */
8
8
  export declare function toHwpx(doc: JSONContent, values: Values): Promise<Blob>;
9
+ /**
10
+ * 에디터 JSON + 입력값 → .hwp(HWP 5.0) Blob. toHwpx 결과를 @rhwp/core(WASM, 약 10MB — 처음 저장할 때만 불러옴)로 변환
11
+ * `wasm`: WASM 파일 주소나 바이트. 번들러가 WASM 경로를 못 찾을 때 넘김 (예: public에 복사한 '/rhwp_bg.wasm')
12
+ * ponytail: 한글 프로그램에서 열리는지는 사람이 확인해야 함 — 여기선 hwp-convert로 다시 읽어 글자·구조만 검증함
13
+ */
14
+ export declare function toHwp(doc: JSONContent, values: Values, opts?: {
15
+ wasm?: string | URL | BufferSource;
16
+ }): Promise<Blob>;
9
17
  /**
10
18
  * 문단으로만 된 법률문서 HTML(한글·워드에서 온 것)을 에디터 구조로 정리.
11
19
  * - 빈 줄 문단 제거, 짧은 첫 줄 → 제목(h1), "청 구 취 지" 같은 소제목 → h3, [빈칸] → {{변수}}
package/dist/style.css CHANGED
@@ -1,2 +1,2 @@
1
- @counter-style le-circled{system:fixed;symbols:"①" "②" "③" "④" "⑤" "⑥" "⑦" "⑧" "⑨" "⑩" "⑪" "⑫" "⑬" "⑭" "⑮" "⑯" "⑰" "⑱" "⑲" "⑳";suffix:" "}.legal-doc{counter-reset:le-article le-n1 le-n2 le-n3 le-n4;color:#111;background:#fff;outline:none;max-width:760px;margin:0 auto;padding:56px 64px;font-family:Noto Serif KR,Nanum Myeongjo,Batang,serif;font-size:15px;line-height:1.85}.legal-doc h1{text-align:center;letter-spacing:.25em;margin:0 0 1.6em;font-size:1.6em}.legal-doc h2{counter-increment:le-article;margin:1.6em 0 .3em;font-size:1em}.legal-doc h2:before{content:"제" counter(le-article) "조 "}.legal-doc p{margin:0 0 .4em}.legal-doc ol{margin:0 0 .4em;padding-left:1.8em;list-style:le-circled}.legal-doc ol ol{list-style:decimal}.legal-doc li p{margin:0}.legal-doc h3{text-align:center;letter-spacing:.3em;margin:1.8em 0 .8em;font-size:1.05em}.legal-doc>h1,.legal-doc>h2,.legal-doc>h3{counter-set:le-n1 le-n2 le-n3 le-n4}.legal-doc>p[data-num]{padding-left:calc(var(--le-num-indent,0em) + 2em);text-indent:-2em}.legal-doc>p[data-num]:before{text-indent:0;min-width:2em;display:inline-block}.legal-doc>p[data-num="1"]{counter-increment:le-n1;counter-set:le-n2 le-n3 le-n4}.legal-doc>p[data-num="2"]{counter-increment:le-n2;counter-set:le-n3 le-n4;--le-num-indent:1.2em}.legal-doc>p[data-num="3"]{counter-increment:le-n3;counter-set:le-n4;--le-num-indent:2.4em}.legal-doc>p[data-num="4"]{counter-increment:le-n4;--le-num-indent:3.6em}.legal-doc>p[data-num="1"]:before{content:counter(le-n1) "."}.legal-doc>p[data-num="2"]:before{content:counter(le-n2, hangul) "."}.legal-doc>p[data-num="3"]:before{content:"(" counter(le-n3) ")"}.legal-doc>p[data-num="4"]:before{content:"(" counter(le-n4, hangul) ")"}.legal-doc .le-cite{color:inherit;text-underline-offset:3px;cursor:pointer;-webkit-text-decoration:underline dotted #3a56b4;text-decoration:underline dotted #3a56b4}.legal-doc .le-cite:hover{background:#eef2ff}.legal-doc .tableWrapper{overflow-x:auto}.legal-doc table{border-collapse:collapse;width:100%;margin:.6em 0;font-size:.93em}.legal-doc td,.legal-doc th{vertical-align:top;border:1px solid #555;padding:3px 6px}.legal-doc td p,.legal-doc th p{margin:0}.legal-doc .selectedCell{background:#e5edff}.le-root{background:#eef0f3;grid-template-columns:minmax(0,1fr) 260px;gap:16px;padding:16px;font:14px/1.5 system-ui,sans-serif;display:grid}.le-main{min-width:0}.le-toolbar{z-index:1;background:#fff;border-radius:8px;flex-wrap:wrap;gap:4px;max-width:760px;margin:0 auto 8px;padding:6px;display:flex;position:sticky;top:0;box-shadow:0 1px 2px #0001}.le-toolbar button,.le-toolbar .le-btn{cursor:pointer;font:inherit;background:#fff;border:1px solid #d0d4da;border-radius:6px;padding:4px 10px}.le-toolbar button:hover{background:#f3f5f8}.le-spacer{flex:1}.le-main .legal-doc{min-height:900px;box-shadow:0 1px 4px #0002}.le-var{color:#1d3f94;background:#e5edff;border-radius:3px;padding:0 3px}.le-var--empty{color:#8a5a00;background:#fff1c2}.le-panel{background:#fff;border-radius:8px;align-self:start;padding:14px;position:sticky;top:16px;box-shadow:0 1px 2px #0001}.le-panel h3{margin:0 0 10px;font-size:14px}.le-panel label{color:#444;gap:3px;margin-bottom:10px;font-size:13px;display:grid}.le-panel input{font:inherit;border:1px solid #d0d4da;border-radius:6px;padding:6px 8px}.le-hint{color:#777;margin:0;font-size:13px}.le-cases{border-top:1px solid #e3e6ea;margin-top:16px;padding-top:12px}.le-cases-bar{gap:4px;margin-bottom:8px;display:flex}.le-cases-bar input{flex:1;min-width:0}.le-cases-bar button,.le-case-list button{cursor:pointer;font:inherit;background:#fff;border:1px solid #d0d4da;border-radius:6px;padding:4px 8px}.le-case-list{gap:6px;margin:8px 0 0;padding:0;list-style:none;display:grid}.le-case-list li{gap:2px;display:grid}.le-case-list button{text-align:left;gap:2px;font-size:12px;display:grid}.le-case-list button:hover:not(:disabled){background:#f3f5f8}.le-case-list span,.le-case-list small{color:#555}.le-case-list a{color:#3a56b4;justify-self:end;font-size:12px}@media (width<=800px){.le-root{grid-template-columns:1fr}.legal-doc{padding:32px 24px}}@media print{.le-printing body *{visibility:hidden}.le-printing .legal-doc,.le-printing .legal-doc *{visibility:visible}.le-printing .legal-doc{width:100%;max-width:none;box-shadow:none;padding:0;position:absolute;top:0;left:0}.le-printing .le-var{color:inherit;background:0 0;padding:0}.le-printing .le-cite{background:0 0;text-decoration:none}.le-printing .le-var--empty{color:#0000;border-bottom:1px solid #000}}
1
+ @counter-style le-circled{system:fixed;symbols:"①" "②" "③" "④" "⑤" "⑥" "⑦" "⑧" "⑨" "⑩" "⑪" "⑫" "⑬" "⑭" "⑮" "⑯" "⑰" "⑱" "⑲" "⑳";suffix:" "}.legal-doc{counter-reset:le-article le-n1 le-n2 le-n3 le-n4;color:#111;background:#fff;outline:none;max-width:760px;margin:0 auto;padding:56px 64px;font-family:Noto Serif KR,Nanum Myeongjo,Batang,serif;font-size:15px;line-height:1.85}.legal-doc h1{text-align:center;letter-spacing:.25em;margin:0 0 1.6em;font-size:1.6em}.legal-doc h2{counter-increment:le-article;margin:1.6em 0 .3em;font-size:1em}.legal-doc h2:before{content:"제" counter(le-article) "조 "}.legal-doc p{margin:0 0 .4em}.legal-doc ol{margin:0 0 .4em;padding-left:1.8em;list-style:le-circled}.legal-doc ol ol{list-style:decimal}.legal-doc li p{margin:0}.legal-doc h3{text-align:center;letter-spacing:.3em;margin:1.8em 0 .8em;font-size:1.05em}.legal-doc>h1,.legal-doc>h2,.legal-doc>h3{counter-set:le-n1 le-n2 le-n3 le-n4}.legal-doc>p[data-num]{padding-left:calc(var(--le-num-indent,0em) + 2em);text-indent:-2em}.legal-doc>p[data-num]:before{text-indent:0;min-width:2em;display:inline-block}.legal-doc>p[data-num="1"]{counter-increment:le-n1;counter-set:le-n2 le-n3 le-n4}.legal-doc>p[data-num="2"]{counter-increment:le-n2;counter-set:le-n3 le-n4;--le-num-indent:1.2em}.legal-doc>p[data-num="3"]{counter-increment:le-n3;counter-set:le-n4;--le-num-indent:2.4em}.legal-doc>p[data-num="4"]{counter-increment:le-n4;--le-num-indent:3.6em}.legal-doc>p[data-num="1"]:before{content:counter(le-n1) "."}.legal-doc>p[data-num="2"]:before{content:counter(le-n2, hangul) "."}.legal-doc>p[data-num="3"]:before{content:"(" counter(le-n3) ")"}.legal-doc>p[data-num="4"]:before{content:"(" counter(le-n4, hangul) ")"}.legal-doc .le-cite{color:inherit;text-underline-offset:3px;cursor:pointer;-webkit-text-decoration:underline dotted #3a56b4;text-decoration:underline dotted #3a56b4}.legal-doc .le-cite:hover{background:#eef2ff}.legal-doc .tableWrapper{overflow-x:auto}.legal-doc table{border-collapse:collapse;width:100%;margin:.6em 0;font-size:.93em}.legal-doc td,.legal-doc th{vertical-align:top;border:1px solid #555;padding:3px 6px}.legal-doc td p,.legal-doc th p{margin:0}.legal-doc .selectedCell{background:#e5edff}.le-root{background:#eef0f3;grid-template-columns:minmax(0,1fr) 260px;gap:16px;padding:16px;font:14px/1.5 system-ui,sans-serif;display:grid}.le-main{min-width:0}.le-toolbar{z-index:1;background:#fff;border-radius:8px;flex-wrap:wrap;gap:4px;max-width:760px;margin:0 auto 8px;padding:6px;display:flex;position:sticky;top:0;box-shadow:0 1px 2px #0001}.le-toolbar button,.le-toolbar .le-btn{cursor:pointer;font:inherit;background:#fff;border:1px solid #d0d4da;border-radius:6px;padding:4px 10px}.le-toolbar button:hover{background:#f3f5f8}.le-spacer{flex:1}.le-main .legal-doc{min-height:900px;box-shadow:0 1px 4px #0002}.le-var{color:#1d3f94;background:#e5edff;border-radius:3px;padding:0 3px}.le-var--empty{color:#8a5a00;background:#fff1c2}.le-panel{background:#fff;border-radius:8px;align-self:start;padding:14px;position:sticky;top:16px;box-shadow:0 1px 2px #0001}.le-panel h3{margin:0 0 10px;font-size:14px}.le-panel label{color:#444;gap:3px;margin-bottom:10px;font-size:13px;display:grid}.le-panel input{font:inherit;border:1px solid #d0d4da;border-radius:6px;padding:6px 8px}.le-hint{color:#777;margin:0;font-size:13px}.le-cases{border-top:1px solid #e3e6ea;margin-top:16px;padding-top:12px}.le-cases-bar{gap:4px;margin-bottom:8px;display:flex}.le-cases-bar input{flex:1;min-width:0}.le-cases-bar button,.le-case-list button{cursor:pointer;font:inherit;background:#fff;border:1px solid #d0d4da;border-radius:6px;padding:4px 8px}.le-case-list{gap:6px;margin:8px 0 0;padding:0;list-style:none;display:grid}.le-case-list li{gap:2px;display:grid}.le-case-list button{text-align:left;gap:2px;font-size:12px;display:grid}.le-case-list button:hover:not(:disabled){background:#f3f5f8}.le-case-list span,.le-case-list small{color:#555}.le-case-list a{color:#3a56b4;justify-self:end;font-size:12px}.le-clauses{border-top:1px solid #e3e6ea;margin-top:16px;padding-top:12px}.le-clauses>input{box-sizing:border-box;width:100%;margin-bottom:6px}.le-clause-list{gap:4px;max-height:280px;margin:0;padding:0;list-style:none;display:grid;overflow-y:auto}.le-clause-list button{cursor:pointer;text-align:left;width:100%;font:inherit;background:#fff;border:1px solid #d0d4da;border-radius:6px;padding:4px 8px;font-size:12px}.le-clause-list button:hover:not(:disabled){background:#f3f5f8}.le-clause-list small{color:#3a56b4;margin-right:6px}@media (width<=800px){.le-root{grid-template-columns:1fr}.legal-doc{padding:32px 24px}}@media print{.le-printing body *{visibility:hidden}.le-printing .legal-doc,.le-printing .legal-doc *{visibility:visible}.le-printing .legal-doc{width:100%;max-width:none;box-shadow:none;padding:0;position:absolute;top:0;left:0}.le-printing .le-var{color:inherit;background:0 0;padding:0}.le-printing .le-cite{background:0 0;text-decoration:none}.le-printing .le-var--empty{color:#0000;border-bottom:1px solid #000}}
2
2
  /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "legal-doc-editor",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "법률문서(계약서·소장·고소장) 생성용 React 에디터 — 조항·번호 자동 매김, {{변수}} 채우기, 한글(hwp·hwpx)·Word(docx) 열기·저장",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -50,6 +50,7 @@
50
50
  "react-dom": ">=18"
51
51
  },
52
52
  "dependencies": {
53
+ "@rhwp/core": "^0.8.6",
53
54
  "@tiptap/core": "^3.31.3",
54
55
  "@tiptap/extension-table": "^3.31.3",
55
56
  "@tiptap/pm": "^3.31.3",