document-adapter 0.2.0__tar.gz → 0.4.0__tar.gz

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 (28) hide show
  1. document_adapter-0.4.0/NOTICE +29 -0
  2. {document_adapter-0.2.0 → document_adapter-0.4.0}/PKG-INFO +74 -23
  3. {document_adapter-0.2.0 → document_adapter-0.4.0}/README.md +68 -19
  4. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter/base.py +64 -1
  5. document_adapter-0.4.0/document_adapter/docx_adapter.py +352 -0
  6. document_adapter-0.4.0/document_adapter/hwpx_adapter.py +348 -0
  7. document_adapter-0.4.0/document_adapter/hwpx_core/__init__.py +67 -0
  8. document_adapter-0.4.0/document_adapter/hwpx_core/constants.py +27 -0
  9. document_adapter-0.4.0/document_adapter/hwpx_core/grid.py +145 -0
  10. document_adapter-0.4.0/document_adapter/hwpx_core/package.py +146 -0
  11. document_adapter-0.4.0/document_adapter/hwpx_core/paragraph.py +106 -0
  12. document_adapter-0.4.0/document_adapter/pptx_adapter.py +354 -0
  13. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter/tools.py +106 -17
  14. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter.egg-info/PKG-INFO +74 -23
  15. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter.egg-info/SOURCES.txt +6 -0
  16. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter.egg-info/requires.txt +3 -2
  17. {document_adapter-0.2.0 → document_adapter-0.4.0}/pyproject.toml +7 -5
  18. {document_adapter-0.2.0 → document_adapter-0.4.0}/tests/test_smoke.py +325 -0
  19. document_adapter-0.2.0/document_adapter/docx_adapter.py +0 -133
  20. document_adapter-0.2.0/document_adapter/hwpx_adapter.py +0 -246
  21. document_adapter-0.2.0/document_adapter/pptx_adapter.py +0 -178
  22. {document_adapter-0.2.0 → document_adapter-0.4.0}/LICENSE +0 -0
  23. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter/__init__.py +0 -0
  24. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter/mcp_server.py +0 -0
  25. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter.egg-info/dependency_links.txt +0 -0
  26. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter.egg-info/entry_points.txt +0 -0
  27. {document_adapter-0.2.0 → document_adapter-0.4.0}/document_adapter.egg-info/top_level.txt +0 -0
  28. {document_adapter-0.2.0 → document_adapter-0.4.0}/setup.cfg +0 -0
@@ -0,0 +1,29 @@
1
+ document-adapter
2
+ Copyright 2026 Son Seongjun / PlateerLab
3
+
4
+ This product includes software developed at PlateerLab
5
+ (https://github.com/PlateerLab).
6
+
7
+ ===============================================================================
8
+ Third-party code attributions
9
+ ===============================================================================
10
+
11
+ HWPX table grid parsing logic
12
+ -----------------------------
13
+
14
+ Portions of the HWPX table grid construction in
15
+ `document_adapter/hwpx_core/grid.py` (cell position / cell span parsing, and
16
+ the skip-map approach for merged non-anchor cells) are adapted from:
17
+
18
+ PlateerLab/xgen-doc2chunk
19
+ https://github.com/PlateerLab/xgen-doc2chunk
20
+ Licensed under the Apache License, Version 2.0
21
+
22
+ The adapted logic is limited to read-side grid construction. All edit-side
23
+ functionality (cell writing, row insertion, ZIP round-trip preservation) is
24
+ original to this project.
25
+
26
+ You may obtain a copy of the Apache License 2.0 at:
27
+ http://www.apache.org/licenses/LICENSE-2.0
28
+
29
+ ===============================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: document-adapter
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: LLM-friendly document template editing (DOCX/PPTX/HWPX) with MCP server and Claude API tool-use support
5
5
  Author-email: Son Seongjun <sonsj97@plateer.com>
6
6
  License: MIT
@@ -9,7 +9,7 @@ Project-URL: Repository, https://github.com/PlateerLab/document-adapter
9
9
  Project-URL: Issues, https://github.com/PlateerLab/document-adapter/issues
10
10
  Project-URL: PyPI, https://pypi.org/project/document-adapter/
11
11
  Project-URL: Changelog, https://github.com/PlateerLab/document-adapter/releases
12
- Keywords: mcp,model-context-protocol,llm,claude,anthropic,tool-use,agent,document,docx,pptx,hwpx,hwp,template,template-engine,office,office-automation,word,powerpoint,hancom,korean,docxtpl,python-docx,python-pptx,python-hwpx
12
+ Keywords: mcp,model-context-protocol,llm,claude,anthropic,tool-use,agent,document,docx,pptx,hwpx,hwp,template,template-engine,office,office-automation,word,powerpoint,hancom,korean,docxtpl,python-docx,python-pptx,lxml
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
@@ -26,15 +26,17 @@ Classifier: Environment :: Console
26
26
  Requires-Python: >=3.10
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
- Requires-Dist: python-docx>=1.1
29
+ License-File: NOTICE
30
+ Requires-Dist: python-docx>=1.2
30
31
  Requires-Dist: docxtpl>=0.20
31
32
  Requires-Dist: python-pptx>=1.0
32
- Requires-Dist: python-hwpx>=2.9
33
+ Requires-Dist: lxml>=5.0
33
34
  Requires-Dist: mcp>=1.0
34
35
  Provides-Extra: claude
35
36
  Requires-Dist: anthropic>=0.40; extra == "claude"
36
37
  Provides-Extra: dev
37
38
  Requires-Dist: pytest>=8; extra == "dev"
39
+ Requires-Dist: python-hwpx>=2.9; extra == "dev"
38
40
  Dynamic: license-file
39
41
 
40
42
  # document-adapter
@@ -53,14 +55,21 @@ Dynamic: license-file
53
55
 
54
56
  ## 지원 포맷
55
57
 
56
- | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 셀 수정 | 행 추가 |
57
- |---|---|---|---|---|---|
58
- | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ |
59
- | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | ❌ (미지원) |
60
- | `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ❌ (미지원) |
58
+ | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 병합 셀 인지 | 중첩 테이블 | 셀 수정 | 행 추가 |
59
+ |---|---|---|---|---|---|---|---|
60
+ | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ | ✅ | ✅ |
61
+ | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | — (포맷 미지원) | ✅ | ❌ (미지원) |
62
+ | `.hwpx` | 자체 `hwpx_core` (lxml + zipfile) | `{{key}}` 치환 | ✅ | ✅ | ✅ | ✅ | ✅ |
61
63
 
62
64
  - HWPX는 한컴오피스 설치가 **불필요**합니다 (macOS/Linux 서버에서 그대로 동작).
63
65
  - 구버전 `.hwp`(바이너리 포맷)는 지원하지 않습니다 — `.hwpx`로 변환 후 사용하세요.
66
+ - 병합 셀: 3개 포맷 모두 preview에 `null` 슬롯 + `merges` 메타로 구조 노출. non-anchor 좌표에 쓰기는 `MergedCellWriteError`로 거부.
67
+
68
+ ## 라이선스 (상용 사용 가능)
69
+
70
+ - 본 프로젝트: **MIT License**
71
+ - 런타임 의존성(`python-docx`, `docxtpl`, `python-pptx`, `lxml`, `mcp`): 전부 **허용형 OSS** (MIT/BSD/Apache-2.0/LGPL-2.1). 상용·내부 서비스에 그대로 포함 가능.
72
+ - v0.3 이하에서 사용했던 `python-hwpx` (Non-Commercial License) 는 v0.4.0부터 **dev 환경(테스트 fixture 생성) 전용**으로 이동. HWPX 편집은 자체 `hwpx_core` 모듈이 수행합니다.
64
73
 
65
74
  ## 설치
66
75
 
@@ -106,14 +115,50 @@ doc.save("report_filled.docx")
106
115
 
107
116
  # 3. 기존 양식 파일의 표 셀 수정
108
117
  doc = load("checklist.docx")
118
+
119
+ # 빈 셀 값 교체
109
120
  old = doc.set_cell(table_index=1, row=1, col=1, value="○○전자")
110
- doc.append_row(1, ["새 항목", "값"]) # DOCX만 지원
121
+
122
+ # 라벨이 있는 셀 ("성 명")에 값 추가 → "성 명 홍길동"
123
+ doc.append_to_cell(table_index=2, row=0, col=0, value="홍길동")
124
+
125
+ # 셀 전체 텍스트 + 병합 메타 조회 (preview의 40자 잘림 없이)
126
+ cell = doc.get_cell(table_index=1, row=3, col=2)
127
+ print(cell.text, cell.is_anchor, cell.span, cell.nested_table_indices)
128
+
129
+ # DOCX/HWPX는 행 추가 지원
130
+ doc.append_row(1, ["새 항목", "값"])
131
+
111
132
  doc.save("checklist_filled.docx")
112
133
  doc.close()
113
134
  ```
114
135
 
115
136
  확장자로 자동 분기되므로 `.pptx` / `.hwpx`도 동일한 API를 사용합니다.
116
137
 
138
+ ### 병합 셀 인지 동작 (v0.2+)
139
+
140
+ ```python
141
+ schema = doc.get_schema()
142
+ t = schema.tables[0]
143
+
144
+ # preview는 logical grid. 병합된 non-anchor 슬롯은 None.
145
+ # [['HEADER', None, None], ['A1', 'A2', 'A3']]
146
+ print(t.preview)
147
+
148
+ # merges는 span>1x1인 anchor 목록
149
+ # [MergeInfo(anchor=(0,0), span=(1,3))]
150
+ print(t.merges)
151
+
152
+ # non-anchor에 쓰기 시도하면 MergedCellWriteError (ValueError 서브클래스)
153
+ try:
154
+ doc.set_cell(0, 0, 2, "X")
155
+ except ValueError as e:
156
+ print(e) # "cell (0,2) is part of a merged region anchored at (0,0)..."
157
+
158
+ # 의도적으로 앵커로 리디렉트하고 싶다면
159
+ doc.set_cell(0, 0, 2, "X", allow_merge_redirect=True) # 경고 + 실제로 (0,0) 수정
160
+ ```
161
+
117
162
  ## MCP 서버로 사용 — Claude Desktop / Claude Code
118
163
 
119
164
  ### 실행
@@ -177,39 +222,45 @@ resp = client.messages.create(
177
222
 
178
223
  전체 agent loop 예시는 [`examples/claude_api_example.py`](examples/claude_api_example.py) 참고.
179
224
 
180
- ## 노출되는 4개 도구
225
+ ## 노출되는 도구
181
226
 
182
227
  | 도구 | 설명 |
183
228
  |---|---|
184
229
  | `inspect_document` | 문서 구조(placeholders, tables)를 JSON으로 반환. **항상 첫 호출로 사용** |
185
230
  | `render_template` | `{{key}}`를 context dict 값으로 치환해 새 파일 저장 |
186
- | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 |
187
- | `append_row` | 표 끝에 새 행 추가 (DOCX 전용) |
231
+ | `get_cell` | 셀 전체 텍스트 + 병합/중첩 메타 반환 (preview의 40자 잘림 없이) |
232
+ | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 (병합 anchor만) |
233
+ | `append_to_cell` | 기존 텍스트 뒤에 값 덧붙임 (라벨 유지용, 예: `"성 명"` → `"성 명 홍길동"`) |
234
+ | `append_row` | 표 끝에 새 행 추가 (DOCX/HWPX 지원) |
188
235
 
189
- ### `inspect_document` 반환 예시
236
+ ### `inspect_document` 반환 예시 (v0.2+)
190
237
 
191
238
  ```json
192
239
  {
193
- "format": "docx",
194
- "source": "/path/to/checklist.docx",
240
+ "format": "hwpx",
241
+ "source": "/path/to/form.hwpx",
195
242
  "placeholders": [],
196
243
  "tables": [
197
244
  {
198
- "index": 1,
199
- "rows": 7,
200
- "cols": 2,
245
+ "index": 0,
246
+ "rows": 28,
247
+ "cols": 16,
201
248
  "location": null,
249
+ "parent_path": null,
202
250
  "preview": [
203
- {"row": 0, "cells": ["항목", "기입 내용"]},
204
- {"row": 1, "cells": ["고객사 / 조직", ""]},
205
- {"row": 2, "cells": ["현업 담당부서 / 책임자", ""]}
251
+ ["포상금 지급신청서", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
252
+ ["접수번호", null, null, "", "접수일자", null, null, "", ...]
253
+ ],
254
+ "merges": [
255
+ {"anchor": [0, 0], "span": [1, 16]},
256
+ {"anchor": [1, 0], "span": [1, 3]}
206
257
  ]
207
258
  }
208
259
  ]
209
260
  }
210
261
  ```
211
262
 
212
- LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell`을 호출합니다.
263
+ LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell` / `append_to_cell`을 호출합니다. `null` 슬롯은 병합된 영역이며 `merges`의 anchor 좌표로만 쓸 수 있습니다.
213
264
 
214
265
  ## 템플릿 작성 규칙
215
266
 
@@ -14,14 +14,21 @@
14
14
 
15
15
  ## 지원 포맷
16
16
 
17
- | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 셀 수정 | 행 추가 |
18
- |---|---|---|---|---|---|
19
- | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ |
20
- | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | ❌ (미지원) |
21
- | `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ❌ (미지원) |
17
+ | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 병합 셀 인지 | 중첩 테이블 | 셀 수정 | 행 추가 |
18
+ |---|---|---|---|---|---|---|---|
19
+ | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ | ✅ | ✅ |
20
+ | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | — (포맷 미지원) | ✅ | ❌ (미지원) |
21
+ | `.hwpx` | 자체 `hwpx_core` (lxml + zipfile) | `{{key}}` 치환 | ✅ | ✅ | ✅ | ✅ | ✅ |
22
22
 
23
23
  - HWPX는 한컴오피스 설치가 **불필요**합니다 (macOS/Linux 서버에서 그대로 동작).
24
24
  - 구버전 `.hwp`(바이너리 포맷)는 지원하지 않습니다 — `.hwpx`로 변환 후 사용하세요.
25
+ - 병합 셀: 3개 포맷 모두 preview에 `null` 슬롯 + `merges` 메타로 구조 노출. non-anchor 좌표에 쓰기는 `MergedCellWriteError`로 거부.
26
+
27
+ ## 라이선스 (상용 사용 가능)
28
+
29
+ - 본 프로젝트: **MIT License**
30
+ - 런타임 의존성(`python-docx`, `docxtpl`, `python-pptx`, `lxml`, `mcp`): 전부 **허용형 OSS** (MIT/BSD/Apache-2.0/LGPL-2.1). 상용·내부 서비스에 그대로 포함 가능.
31
+ - v0.3 이하에서 사용했던 `python-hwpx` (Non-Commercial License) 는 v0.4.0부터 **dev 환경(테스트 fixture 생성) 전용**으로 이동. HWPX 편집은 자체 `hwpx_core` 모듈이 수행합니다.
25
32
 
26
33
  ## 설치
27
34
 
@@ -67,14 +74,50 @@ doc.save("report_filled.docx")
67
74
 
68
75
  # 3. 기존 양식 파일의 표 셀 수정
69
76
  doc = load("checklist.docx")
77
+
78
+ # 빈 셀 값 교체
70
79
  old = doc.set_cell(table_index=1, row=1, col=1, value="○○전자")
71
- doc.append_row(1, ["새 항목", "값"]) # DOCX만 지원
80
+
81
+ # 라벨이 있는 셀 ("성 명")에 값 추가 → "성 명 홍길동"
82
+ doc.append_to_cell(table_index=2, row=0, col=0, value="홍길동")
83
+
84
+ # 셀 전체 텍스트 + 병합 메타 조회 (preview의 40자 잘림 없이)
85
+ cell = doc.get_cell(table_index=1, row=3, col=2)
86
+ print(cell.text, cell.is_anchor, cell.span, cell.nested_table_indices)
87
+
88
+ # DOCX/HWPX는 행 추가 지원
89
+ doc.append_row(1, ["새 항목", "값"])
90
+
72
91
  doc.save("checklist_filled.docx")
73
92
  doc.close()
74
93
  ```
75
94
 
76
95
  확장자로 자동 분기되므로 `.pptx` / `.hwpx`도 동일한 API를 사용합니다.
77
96
 
97
+ ### 병합 셀 인지 동작 (v0.2+)
98
+
99
+ ```python
100
+ schema = doc.get_schema()
101
+ t = schema.tables[0]
102
+
103
+ # preview는 logical grid. 병합된 non-anchor 슬롯은 None.
104
+ # [['HEADER', None, None], ['A1', 'A2', 'A3']]
105
+ print(t.preview)
106
+
107
+ # merges는 span>1x1인 anchor 목록
108
+ # [MergeInfo(anchor=(0,0), span=(1,3))]
109
+ print(t.merges)
110
+
111
+ # non-anchor에 쓰기 시도하면 MergedCellWriteError (ValueError 서브클래스)
112
+ try:
113
+ doc.set_cell(0, 0, 2, "X")
114
+ except ValueError as e:
115
+ print(e) # "cell (0,2) is part of a merged region anchored at (0,0)..."
116
+
117
+ # 의도적으로 앵커로 리디렉트하고 싶다면
118
+ doc.set_cell(0, 0, 2, "X", allow_merge_redirect=True) # 경고 + 실제로 (0,0) 수정
119
+ ```
120
+
78
121
  ## MCP 서버로 사용 — Claude Desktop / Claude Code
79
122
 
80
123
  ### 실행
@@ -138,39 +181,45 @@ resp = client.messages.create(
138
181
 
139
182
  전체 agent loop 예시는 [`examples/claude_api_example.py`](examples/claude_api_example.py) 참고.
140
183
 
141
- ## 노출되는 4개 도구
184
+ ## 노출되는 도구
142
185
 
143
186
  | 도구 | 설명 |
144
187
  |---|---|
145
188
  | `inspect_document` | 문서 구조(placeholders, tables)를 JSON으로 반환. **항상 첫 호출로 사용** |
146
189
  | `render_template` | `{{key}}`를 context dict 값으로 치환해 새 파일 저장 |
147
- | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 |
148
- | `append_row` | 표 끝에 새 행 추가 (DOCX 전용) |
190
+ | `get_cell` | 셀 전체 텍스트 + 병합/중첩 메타 반환 (preview의 40자 잘림 없이) |
191
+ | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 (병합 anchor만) |
192
+ | `append_to_cell` | 기존 텍스트 뒤에 값 덧붙임 (라벨 유지용, 예: `"성 명"` → `"성 명 홍길동"`) |
193
+ | `append_row` | 표 끝에 새 행 추가 (DOCX/HWPX 지원) |
149
194
 
150
- ### `inspect_document` 반환 예시
195
+ ### `inspect_document` 반환 예시 (v0.2+)
151
196
 
152
197
  ```json
153
198
  {
154
- "format": "docx",
155
- "source": "/path/to/checklist.docx",
199
+ "format": "hwpx",
200
+ "source": "/path/to/form.hwpx",
156
201
  "placeholders": [],
157
202
  "tables": [
158
203
  {
159
- "index": 1,
160
- "rows": 7,
161
- "cols": 2,
204
+ "index": 0,
205
+ "rows": 28,
206
+ "cols": 16,
162
207
  "location": null,
208
+ "parent_path": null,
163
209
  "preview": [
164
- {"row": 0, "cells": ["항목", "기입 내용"]},
165
- {"row": 1, "cells": ["고객사 / 조직", ""]},
166
- {"row": 2, "cells": ["현업 담당부서 / 책임자", ""]}
210
+ ["포상금 지급신청서", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
211
+ ["접수번호", null, null, "", "접수일자", null, null, "", ...]
212
+ ],
213
+ "merges": [
214
+ {"anchor": [0, 0], "span": [1, 16]},
215
+ {"anchor": [1, 0], "span": [1, 3]}
167
216
  ]
168
217
  }
169
218
  ]
170
219
  }
171
220
  ```
172
221
 
173
- LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell`을 호출합니다.
222
+ LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell` / `append_to_cell`을 호출합니다. `null` 슬롯은 병합된 영역이며 `merges`의 anchor 좌표로만 쓸 수 있습니다.
174
223
 
175
224
  ## 템플릿 작성 규칙
176
225
 
@@ -3,7 +3,7 @@
3
3
  세 포맷(DOCX/PPTX/HWPX)의 공통 작업을 추상화:
4
4
  - 템플릿 렌더링 ({{key}} 치환)
5
5
  - 표 스키마 추출 (LLM 입력용)
6
- - 셀 수정 / 행 추가
6
+ - 셀 수정 / 값 추가 / 행 추가
7
7
  """
8
8
  from __future__ import annotations
9
9
 
@@ -13,6 +13,28 @@ from pathlib import Path
13
13
  from typing import Any
14
14
 
15
15
 
16
+ # ---- custom exceptions -----------------------------------------------------
17
+ # 표준 예외를 상속해 기존 ``except ValueError/IndexError`` 흐름과 호환.
18
+
19
+ class MergedCellWriteError(ValueError):
20
+ """병합 영역의 non-anchor 좌표에 쓰기를 시도했을 때."""
21
+
22
+
23
+ class CellOutOfBoundsError(IndexError):
24
+ """(row, col)이 표 경계를 벗어남."""
25
+
26
+
27
+ class TableIndexError(IndexError):
28
+ """table_index로 표를 찾지 못함."""
29
+
30
+
31
+ class NotImplementedForFormat(NotImplementedError):
32
+ """특정 포맷이 지원하지 않는 연산."""
33
+
34
+
35
+ # ---- dataclasses -----------------------------------------------------------
36
+
37
+
16
38
  @dataclass
17
39
  class MergeInfo:
18
40
  """병합 셀 정보. anchor=(row,col)에서 span=(rows,cols)만큼 병합."""
@@ -51,6 +73,34 @@ class TableSchema:
51
73
  }
52
74
 
53
75
 
76
+ @dataclass
77
+ class CellContent:
78
+ """단일 셀의 전체 내용 + 병합/중첩 메타.
79
+
80
+ 프리뷰가 max_cell_len으로 잘리는 것과 달리 ``text``는 전체 본문을 보유.
81
+ """
82
+ row: int
83
+ col: int
84
+ text: str # 셀 전체 평문 (자르지 않음)
85
+ paragraphs: list[str] # paragraph 단위 분리
86
+ is_anchor: bool # (row,col)이 병합 앵커인지
87
+ anchor: tuple[int, int] # 이 logical 위치의 앵커 좌표
88
+ span: tuple[int, int] # (row_span, col_span)
89
+ nested_table_indices: list[int] = field(default_factory=list)
90
+
91
+ def to_dict(self) -> dict[str, Any]:
92
+ return {
93
+ "row": self.row,
94
+ "col": self.col,
95
+ "text": self.text,
96
+ "paragraphs": self.paragraphs,
97
+ "is_anchor": self.is_anchor,
98
+ "anchor": list(self.anchor),
99
+ "span": list(self.span),
100
+ "nested_table_indices": self.nested_table_indices,
101
+ }
102
+
103
+
54
104
  @dataclass
55
105
  class DocumentSchema:
56
106
  """문서 전체 스키마."""
@@ -98,6 +148,10 @@ class DocumentAdapter(ABC):
98
148
  preview_rows: int = 4, max_cell_len: int = 40) -> list[TableSchema]:
99
149
  """필터 조건을 만족하는 표 스키마 목록."""
100
150
 
151
+ @abstractmethod
152
+ def get_cell(self, table_index: int, row: int, col: int) -> CellContent:
153
+ """셀 단건 조회. preview로 잘린 전체 텍스트와 병합/중첩 메타 반환."""
154
+
101
155
  def get_schema(self) -> DocumentSchema:
102
156
  return DocumentSchema(
103
157
  format=self.format,
@@ -115,6 +169,15 @@ class DocumentAdapter(ABC):
115
169
  def set_cell(self, table_index: int, row: int, col: int, value: str) -> str:
116
170
  """셀 값 교체. 원래 값 반환."""
117
171
 
172
+ @abstractmethod
173
+ def append_to_cell(self, table_index: int, row: int, col: int, value: str,
174
+ separator: str = " ") -> str:
175
+ """기존 셀 텍스트 뒤에 ``separator + value``를 덧붙임.
176
+
177
+ 라벨(예: "성 명")을 유지한 채 사용자 입력을 추가하는 용도.
178
+ 빈 셀이면 separator 없이 value만 기록. 원래 값 반환.
179
+ """
180
+
118
181
  @abstractmethod
119
182
  def append_row(self, table_index: int, values: list[str]) -> None:
120
183
  """표 끝에 새 행 추가."""