Skip to content

Commit 0c643dc

Browse files
master0419claude
andcommitted
release: v0.15.0 + v0.16.0 — 문서 생성 계층 신설(docx/xlsx) + pptx/hwpx 확장
v0.15.0 — 편집 전용 엔진에 "무에서 생성" 계층 추가. create_document(path, markdown=/sheets=) 진입점, generate/ 서브패키지 (markdown_parser / docx_writer / xlsx_writer), MCP 도구 create_document. v0.16.0 — markdown 생성 대상을 .pptx / .hwpx 로 확장. pptx_writer(--- · 레벨1~2 헤딩 슬라이드 분할), hwpx_writer(OWPML 직접 조립). 3종 렌더러가 markdown_parser 공유, 생성 직후 load() 왕복 성립. 테스트 184 passed. edit2docs(Apache-2.0) 이식분 NOTICE 표기 유지. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 670c866 commit 0c643dc

15 files changed

Lines changed: 1964 additions & 4 deletions

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,45 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning:
77

88
## [Unreleased]
99

10+
## [0.16.0] — 2026-07-13
11+
12+
**생성 대상 확장 — .pptx / .hwpx** — v0.15 의 docx/xlsx 생성 계층을
13+
프레젠테이션·한글 문서로 넓혔다. markdown 렌더러 3종(.docx/.pptx/.hwpx)이
14+
같은 `markdown_parser`(블록 IR)를 공유해, 하나의 markdown 입력이 포맷만
15+
바꿔 재사용된다. 생성 직후 `load()` 왕복은 신규 포맷에도 동일하게 성립한다.
16+
17+
### Added
18+
- **`.pptx` 생성** (`generate/pptx_writer.py`) — `create_document("x.pptx",
19+
markdown=...)`. `---` 또는 레벨 1~2 헤딩이 슬라이드 경계, 나머지 블록
20+
(불릿/문단/표)은 본문 플레이스홀더로 배치. python-pptx 내장 레이아웃 사용.
21+
- **`.hwpx` 생성** (`generate/hwpx_writer.py`) — `create_document("x.hwpx",
22+
markdown=...)`. v0.15 에서 "v0.16 예정" 안내 에러였던 경로가 실제 렌더러로
23+
대체됨. HWPX(OWPML) 패키지를 직접 조립, `load()` 가 곧바로 성립.
24+
- `create_document` 디스패처의 markdown 대상이 `{.docx, .pptx, .hwpx}` 로 확장.
25+
26+
## [0.15.0] — 2026-07-09
27+
28+
**문서 생성** — 편집 전용이던 엔진에 "무에서 생성" 계층 추가. LLM 은 경량
29+
중간 산출물(제약된 markdown / sheet spec dict)만 쓰고 결정적 렌더러가
30+
스타일 잡힌 문서로 변환한다 (OOXML 직접 생성 대비 토큰 ~96% 절감).
31+
생성 직후 `load()` 가 성립해 기존 편집 도구(set_cell / insert_row / ...)와
32+
같은 좌표계로 이어진다 — 생성-편집 왕복 보장.
33+
34+
### Added
35+
- **`create_document(path, *, markdown=None, sheets=None, lang="ko",
36+
overwrite=False)`** — 확장자 디스패치 생성 진입점 (`load()` 와 대칭).
37+
`.docx`=markdown, `.xlsx`=sheet spec. `.hwpx` 는 v0.16 예정 안내 에러.
38+
- **`generate/` 서브패키지**`markdown_parser`(블록 IR: 포맷 무관 공용
39+
파서 — 이후 HWPX writer 가 공유), `docx_writer`(python-docx 내장 스타일만
40+
사용, CJK `w:eastAsia` 폰트 명시, hr=pBdr), `xlsx_writer`(헤더 스타일·
41+
틀고정·자동 열폭·`=` 수식 통과·`number_formats`).
42+
- MCP/Claude 도구 **`create_document`** (총 17개) — 반환값에 생성 직후 표
43+
좌표 요약(`tables`/`table_shapes`) 포함, LLM 이 후속 편집을 바로 이어감.
44+
- markdown 서브셋: 헤딩/문단/불릿/번호/굵게/기울임/코드/파이프표/인용/
45+
수평선/코드펜스. 지원 외 문법은 에러 없이 일반 텍스트 관용 처리.
46+
- 모든 스펙 검증 실패는 이중어(EN/KO) `ValueError` — 호출 레이어의 1회
47+
재시도 계약용 (렌더러가 곧 검증기).
48+
1049
## [0.14.0] — 2026-07-07
1150

1251
표에 **위치를 지정해** 행/열을 삽입하는 계층 추가 — "2026년 행을 2025년

README.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ resp = client.messages.create(
221221

222222
## 노출되는 도구
223223

224-
**12** 도구 (DOCX/PPTX/HWPX/XLSX 공통, 확장자로 자동 디스패치):
224+
**17** 도구 (DOCX/PPTX/HWPX/XLSX 공통, 확장자로 자동 디스패치):
225225

226226
| 도구 | 설명 |
227227
|---|---|
@@ -231,11 +231,48 @@ resp = client.messages.create(
231231
| `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 (병합 anchor만) |
232232
| `append_to_cell` | 기존 텍스트 뒤에 값 덧붙임 (라벨 유지용, 예: `"성 명"``"성 명 홍길동"`) |
233233
| `fill_form` (v0.7+) | **라벨 이름**으로 일괄 채우기. 좌표 계산 없이 `{"접수번호": "...", "성명": "..."}` dict. dot-path 섹션 해소 + `overflow_warnings` |
234-
| `append_row` | 표 끝에 새 행 추가 (전 포맷, v0.5+) |
234+
| `append_row` | 표에 새 행 추가 (전 포맷, v0.5+). v0.14: `insert_row`/`insert_column` 어댑터 API 로 위치 지정 삽입 + 서식 상속 |
235+
| `create_document` (v0.15) | **새 문서 생성** — .docx 는 제약된 markdown, .xlsx 는 sheet spec 을 결정적 렌더러가 스타일 잡힌 문서로 변환. 생성 직후 기존 편집 도구와 같은 좌표계로 이어짐 |
236+
| `get_text_map` / `find_text` / `replace_text` / `insert_text` (v0.13, DOCX/HWPX) | 표 밖 본문 텍스트 지도·검색·치환·삽입 (run 분할 무관, 서식 보존) |
235237
| `get_shapes` / `set_shape_text` (v0.8, PPTX) | 표 외 shape(textbox/placeholder/도형) 텍스트 조회·편집 |
236238
| `get_form_controls` / `set_form_control` (v0.10, HWPX) | 폼 컨트롤(체크박스·라디오·에디트·콤보) 조회·설정 |
237239
| `diff_documents` (v0.11) | 두 문서(편집 전/후)를 셀 단위 비교 → 변경 셀 before/after + `overflow_risk`. **편집 후 검증용** |
238240

241+
### 새 문서 생성 (v0.15+)
242+
243+
```python
244+
from document_adapter import create_document, load
245+
246+
# DOCX — LLM 은 제약된 markdown 만 쓰면 된다 (OOXML 대비 토큰 ~96% 절감)
247+
create_document("회의록.docx", lang="ko", markdown="""
248+
# 주간 회의록
249+
250+
| 이름 | 부서 |
251+
|---|---|
252+
| 김경윤 | AI플랫폼 |
253+
254+
- 결정: v0.15 배포
255+
""")
256+
257+
# XLSX — sheet spec (숫자는 숫자로, `=` 는 살아있는 수식)
258+
create_document("매출.xlsx", sheets=[
259+
{"name": "매출 요약", "headers": ["분기", "매출(억원)"],
260+
"rows": [["1분기", 120], ["합계", "=SUM(B2:B2)"]],
261+
"number_formats": {"B": "#,##0"}},
262+
])
263+
264+
# 생성-편집 왕복: 생성 직후 같은 좌표계로 편집 가능
265+
doc = load("회의록.docx")
266+
doc.insert_row(0, ["유지수", "기획"], at_row=2) # 표에 행 추가
267+
doc.save(); doc.close()
268+
```
269+
270+
지원 markdown 서브셋: `#`~`######` 헤딩 · 문단 · `-` 불릿 · `1.` 번호 ·
271+
`**굵게**` `*기울임*` `` `코드` `` · 파이프 표 · `>` 인용 · `---` 수평선 ·
272+
코드펜스. (HTML/이미지/각주/중첩 리스트는 미지원 — 일반 텍스트로 관용 처리.
273+
정교한 양식은 생성이 아니라 **기존 템플릿 편집**이 이 라이브러리의 철학입니다.)
274+
HWPX 생성은 v0.16 예정.
275+
239276
### `inspect_document` 반환 예시 (v0.2+)
240277

241278
```json

document_adapter/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
"""Document template editing — 통합 어댑터.
22
33
사용법:
4-
from document_adapter import load
4+
from document_adapter import load, create_document
5+
6+
# 기존 문서 편집
57
doc = load("report.docx")
68
schema = doc.get_schema()
79
doc.set_cell(0, 1, 1, "홍길동")
810
doc.save("report_filled.docx")
11+
12+
# 새 문서 생성 (v0.15+) — markdown / sheet spec → 스타일 잡힌 문서
13+
create_document("회의록.docx", markdown="# 주간 회의록\\n...", lang="ko")
914
"""
1015
from __future__ import annotations
1116

1217
from pathlib import Path
1318

1419
from .base import DocumentAdapter, DocumentSchema, TableSchema, TextMatch
1520
from .docx_adapter import DocxAdapter
21+
from .generate import create_document
1622
from .hwpx_adapter import HwpxAdapter
1723
from .pptx_adapter import PptxAdapter
1824
from .xlsx_adapter import XlsxAdapter
1925

2026
__all__ = [
2127
"load",
28+
"create_document",
2229
"DocumentAdapter",
2330
"DocumentSchema",
2431
"TableSchema",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""문서 생성 (v0.15+) — 확장자 디스패치 진입점.
2+
3+
from document_adapter import create_document
4+
5+
create_document("회의록.docx", markdown="# 주간 회의록\\n...", lang="ko")
6+
create_document("보고.pptx", markdown="# 표지\\n---\\n# 슬라이드2\\n- 불릿")
7+
create_document("공문.hwpx", markdown="# 제목\\n본문...")
8+
create_document("매출.xlsx", sheets=[{"name": "...", "headers": [...], "rows": [...]}])
9+
10+
설계 원칙:
11+
- LLM 산출물은 markdown / sheet spec(dict) 뿐 — XML 을 직접 쓰는 경로 없음.
12+
- 렌더러가 곧 검증기: 잘못된 산출물은 이중어 ValueError. 재시도는 호출
13+
레이어(도구/에이전트)의 몫이다 (1회 재시도 계약).
14+
- 생성-편집 왕복 보장: 반환 즉시 ``load(path)`` 가 성립해 기존 편집 도구
15+
(inspect_document → set_cell / insert_row / ...)로 이어진다.
16+
- markdown 렌더러 3종(.docx/.pptx/.hwpx)은 같은 파서(markdown_parser)를
17+
공유한다 — .pptx 는 ``---``/레벨 1~2 헤딩으로 슬라이드를 나눈다.
18+
"""
19+
from __future__ import annotations
20+
21+
from pathlib import Path
22+
from typing import Any
23+
24+
from .docx_writer import base_font_for_lang, docx_from_markdown
25+
from .hwpx_writer import hwpx_from_markdown
26+
from .markdown_parser import Block, Span, parse_inline, parse_markdown
27+
from .pptx_writer import pptx_from_markdown
28+
from .xlsx_writer import xlsx_from_sheets
29+
30+
__all__ = [
31+
"create_document",
32+
"docx_from_markdown",
33+
"pptx_from_markdown",
34+
"hwpx_from_markdown",
35+
"xlsx_from_sheets",
36+
"parse_markdown",
37+
"parse_inline",
38+
"base_font_for_lang",
39+
"Block",
40+
"Span",
41+
]
42+
43+
_MARKDOWN_EXTS = {".docx", ".pptx", ".hwpx"}
44+
_SHEETS_EXTS = {".xlsx"}
45+
_SUPPORTED = sorted(_MARKDOWN_EXTS | _SHEETS_EXTS)
46+
47+
_MARKDOWN_RENDERERS = {
48+
".docx": docx_from_markdown,
49+
".pptx": pptx_from_markdown,
50+
".hwpx": hwpx_from_markdown,
51+
}
52+
53+
54+
def create_document(
55+
path: str | Path,
56+
*,
57+
markdown: str | None = None,
58+
sheets: list[dict[str, Any]] | None = None,
59+
lang: str = "ko",
60+
overwrite: bool = False,
61+
) -> Path:
62+
"""확장자에 맞는 렌더러로 새 문서를 생성해 path 에 저장한다.
63+
64+
Args:
65+
path: 출력 경로. 확장자가 렌더러를 고른다 (.docx/.pptx/.hwpx=markdown,
66+
.xlsx=sheets). 부모 디렉토리는 자동 생성.
67+
markdown: .docx/.pptx/.hwpx 용 본문 (제약된 markdown 서브셋 —
68+
.pptx 는 ``---`` 또는 레벨 1~2 헤딩이 슬라이드 구분).
69+
sheets: .xlsx 용 sheet spec 리스트 (xlsx_writer 스키마).
70+
lang: 기본 폰트 스택 선택 (ko → 맑은 고딕).
71+
overwrite: False(기본)면 기존 파일이 있을 때 ValueError.
72+
73+
Returns:
74+
생성된 파일 경로. 반환 즉시 ``document_adapter.load()`` 가능.
75+
76+
Raises:
77+
ValueError: 확장자-인자 불일치 / 스펙 검증 실패 / 파일 존재.
78+
전부 이중어 메시지 — 호출 레이어의 재시도 계약용.
79+
"""
80+
out = Path(path)
81+
suffix = out.suffix.lower()
82+
83+
if suffix not in _MARKDOWN_EXTS | _SHEETS_EXTS:
84+
raise ValueError(
85+
f"unsupported extension {suffix!r} for create_document "
86+
f"(supported: {', '.join(_SUPPORTED)}). "
87+
f"create_document 가 지원하지 않는 확장자입니다: {suffix!r} "
88+
f"(지원: {', '.join(_SUPPORTED)})."
89+
)
90+
91+
if markdown is not None and sheets is not None:
92+
raise ValueError(
93+
"pass either `markdown` or `sheets`, not both. "
94+
"`markdown` 과 `sheets` 는 동시에 줄 수 없습니다."
95+
)
96+
97+
if suffix in _MARKDOWN_EXTS:
98+
if markdown is None:
99+
raise ValueError(
100+
f"{suffix} generation requires `markdown`. "
101+
f"{suffix} 생성에는 `markdown` 인자가 필요합니다."
102+
)
103+
content = _MARKDOWN_RENDERERS[suffix](markdown, lang=lang)
104+
else: # _SHEETS_EXTS
105+
if sheets is None:
106+
raise ValueError(
107+
f"{suffix} generation requires `sheets` (sheet spec list). "
108+
f"{suffix} 생성에는 `sheets`(sheet spec 리스트) 인자가 필요합니다."
109+
)
110+
content = xlsx_from_sheets(sheets)
111+
112+
if out.exists() and not overwrite:
113+
raise ValueError(
114+
f"file already exists: {out} — pass overwrite=True to replace. "
115+
f"파일이 이미 존재합니다: {out} — 덮어쓰려면 overwrite=True 를 전달하세요."
116+
)
117+
118+
out.parent.mkdir(parents=True, exist_ok=True)
119+
out.write_bytes(content)
120+
return out

0 commit comments

Comments
 (0)