hwpx
npx skills add https://github.com/canine89/gonggong_hwpxskills --skill hwpx
Agent 安装分布
Skill 文档
HWPX 문ì ìì±Â·í¸ì§ ì¤í¬
ê°ì
HWPXë íì»´ì¤í¼ì¤ íê¸ì ê°ë°©í 문ì í¬ë§·ì´ë¤. ë´ë¶ë ZIP í¨í¤ì§ + XML íí¸ êµ¬ì¡°ì´ë©°, KS X 6101(OWPML) íì¤ì 기ë°íë¤. ì´ ì¤í¬ì python-hwpx ë¼ì´ë¸ë¬ë¦¬ë¥¼ ì¬ì©íì¬ HWPX 문ì를 íë¡ê·¸ëë° ë°©ìì¼ë¡ ìì±Â·í¸ì§Â·í
í릿 ì¹ííë¤.
ì¤ì¹
pip install python-hwpx --break-system-packages
â ï¸â ï¸â ï¸ ìµì°ì ê·ì¹: ìì(í í릿) ì í ì ì± â ï¸â ï¸â ï¸
HWPX 문ì를 ë§ë¤ ë ë°ëì ìë ìì를 ë°ë¥¸ë¤. ìì¸ ìì.
1ë¨ê³: ì¬ì©ì ì ë¡ë ììì´ ìëê°?
ì¬ì©ìê° .hwpx ìì íì¼ì ì
ë¡ëíë¤ë©´ ë°ëì í´ë¹ íì¼ì í
í릿ì¼ë¡ ì¬ì©íë¤.
/mnt/user-data/uploads/ì.hwpxíì¼ì´ ìëì§ íì¸- ìë¤ë©´ â ê·¸ íì¼ì ë³µì¬íì¬ í í릿ì¼ë¡ ì¬ì© (기본 ìì 무ì)
- ì¬ì©ìê° “ì´ ììì¼ë¡ ë§ë¤ì´ì¤”, “ì´ íì¼ ê¸°ë°ì¼ë¡” ë±ì ííì ì°ë©´ 100% í´ë¹ íì¼ ì¬ì©
2ë¨ê³: 기본 ì ê³µ ìì ì¬ì©
ì¬ì©ì ì ë¡ë ììì´ ìì¼ë©´ ë°ëì 기본 ì ê³µ ììì ì¬ì©íë¤:
- ë³´ê³ ì â
assets/report-template.hwpx - (í¥í ì¶ê°ë ë¤ë¥¸ ììë¤ë ì´ ê·ì¹ ì ì©)
3ë¨ê³: HwpxDocument.new()ë ìµíì ìë¨
HwpxDocument.new()ë¡ ë¹ ë¬¸ì를 ë§ëë ê²ì ì주 ë¨ìí ë©ëª¨Â·ëª©ë¡ ìì¤ì 문ììë§ íì©íë¤. ë³´ê³ ì, 공문, 기ì문 ë± ììì´ íìí 문ìë ì ë new()ë¡ ë§ë¤ì§ ìëë¤.
â ï¸ ìì íì© ì íì ìí¬íë¡ì° (모ë ê²½ì°ì ì ì©)
ì´ë¤ ììì ì°ë (ì¬ì©ì ì ë¡ëë , 기본 ì ê³µì´ë ) ìë ìí¬íë¡ì°ë¥¼ ë°ë¥¸ë¤:
[1] ìì íì¼ì /home/claude/ ë¡ ë³µì¬
â
[2] ObjectFinderë¡ ìì ë´ í
ì¤í¸ ì ì ì¡°ì¬
â
[3] íë ì´ì¤íë ëª©ë¡ ìì± (ì´ë¤ í
ì¤í¸ë¥¼ ëë¡ ë°ê¿ì§ 매í)
â
[4] ZIP-level ì ì²´ ì¹í (í ë´ë¶ í¬í¨)
â (ëì¼ íë ì´ì¤íëê° ì¬ë¬ ë² ëì¤ë©´ ìì°¨ ì¹í ì¬ì©)
[5] ë¤ìì¤íì´ì¤ íì²ë¦¬ (fix_namespaces.py)
â
[6] ObjectFinderë¡ ì¹í ê²°ê³¼ ê²ì¦
â
[7] /mnt/user-data/outputs/ ë¡ ë³µì¬ â present_files
íµì¬: HwpxDocument.open()ì ì¬ì©íì§ ìëë¤
python-hwpx ë²ì ì ë°ë¼ HwpxDocument.open()ì´ ë³µì¡í ìì íì¼ì íì±íì§ ëª»í ì ìë¤. ZIP-level ì¹íë§ ì¬ì©íë ê²ì´ ìì íë¤.
ZIP-level ì¹í í¨ì (ì§ì 구í)
hwpx_replace 모ëì ë³ëë¡ ì¡´ì¬íì§ ìì¼ë¯ë¡ ìë í¨ì를 ì§ì ì½ëì í¬í¨íë¤:
ì¼ê´ ì¹í (ëì¼ í ì¤í¸ë¥¼ 모ë ê°ì ê°ì¼ë¡)
import zipfile, os
def zip_replace(src_path, dst_path, replacements):
"""HWPX ZIP ë´ ëª¨ë XMLìì í
ì¤í¸ ì¹í (í ë´ë¶ í¬í¨)"""
tmp = dst_path + ".tmp"
with zipfile.ZipFile(src_path, "r") as zin:
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename.startswith("Contents/") and item.filename.endswith(".xml"):
text = data.decode("utf-8")
for old, new in replacements.items():
text = text.replace(old, new)
data = text.encode("utf-8")
zout.writestr(item, data)
if os.path.exists(dst_path):
os.remove(dst_path)
os.rename(tmp, dst_path)
ìì°¨ ì¹í (ëì¼ íë ì´ì¤íë를 ììëë¡ ë¤ë¥¸ ê°ì¼ë¡)
def zip_replace_sequential(src_path, dst_path, old, new_list):
"""section XMLìì old를 ììëë¡ new_list ê°ì¼ë¡ íëì© ì¹í"""
tmp = dst_path + ".tmp"
with zipfile.ZipFile(src_path, "r") as zin:
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if "section" in item.filename and item.filename.endswith(".xml"):
text = data.decode("utf-8")
for new_val in new_list:
text = text.replace(old, new_val, 1) # 1ë²ë§ ì¹í
data = text.encode("utf-8")
zout.writestr(item, data)
if os.path.exists(dst_path):
os.remove(dst_path)
os.rename(tmp, dst_path)
ìì ë´ í ì¤í¸ ì ì ì¡°ì¬ ë°©ë²
from hwpx import ObjectFinder
finder = ObjectFinder("ììíì¼.hwpx")
results = finder.find_all(tag="t")
for r in results:
if r.text and r.text.strip():
print(repr(r.text))
ì´ ê²°ê³¼ë¥¼ ë³´ê³ ì´ë¤ í ì¤í¸ê° íë ì´ì¤íëì¸ì§ íì í í, ì¹í 매íì ìì±íë¤.
기본 ìì(report-template.hwpx) íì© ê°ì´ë
ìì 구조
1쪽: íì§ â 기ê´ëª
(30pt) + ë³´ê³ ì ì 목(25pt) + ìì±ì¼(25pt)
2쪽: 목차 â ë¡ë§ì«ì(â
~â
¤) + ì 목 + íì´ì§, ë¶ì/ì°¸ê³
3쪽~: 본문 â ê²°ì¬ë + ì 목(22pt) + ì¹ì
ë°(â
~â
£) + â¡âââ» ê³ì¸µ 본문
본문 ê¸°í¸ ì²´ê³ (공문ìì ìì í ë¤ë¦!)
1ë¨ê³: â¡ (HYí¤ëë¼ì¸M 16pt, ë¬¸ë¨ ì 15)
2ë¨ê³: â (í´ë¨¼ëª
ì¡° 15pt, ë¬¸ë¨ ì 10)
3ë¨ê³: â (í´ë¨¼ëª
ì¡° 15pt, ë¬¸ë¨ ì 6)
4ë¨ê³: â» (íìì¤ê³ ë 13pt, ë¬¸ë¨ ì 3)
ì¹í ê°ë¥í íë ì´ì¤íë 목ë¡
| íë ì´ì¤íë | ìì¹ | ì¹í ëì | ì¹í ë°©ë² |
|---|---|---|---|
ë¸ë¼ë ê³µê¸°ê´ |
íì§ 1ì¤ | 기ê´ëª | ì¼ê´ ì¹í |
기본 ë³´ê³ ì ìì |
íì§ 2ì¤ | ë³´ê³ ì ì 목 | ì¼ê´ ì¹í |
2024. 5. 23. |
íì§ ìì±ì¼ | ì¤ì ìì±ì¼ | ì¼ê´ ì¹í |
ì 목 |
본문 íì´ì§ ì 목 | ë³´ê³ ì ì 목 | ì¼ê´ ì¹í |
. ê°ì ë± |
목차 í목 | ì¤ì 목차 ì 목 | ì¼ê´ ì¹í |
ì¶ì§ ë°°ê²½ ë± |
ì¹ì ë° ì 목 | ì¤ì ì¹ì ì 목 | ì¼ê´ ì¹í |
í¤ëë¼ì¸M í°í¸ 16í¬ì¸í¸(ë¬¸ë¨ ì 15) |
⡠본문 (8ê°) | 1ë¨ê³ ë´ì© | ìì°¨ ì¹í |
â í´ë©´ëª
ì¡° 15í¬ì¸í¸(문ë¨ì 10) |
â 본문 (8ê°) | 2ë¨ê³ ë´ì© | ìì°¨ ì¹í |
â í´ë©´ëª
ì¡° 15í¬ì¸í¸(ë¬¸ë¨ ì 6) |
â 본문 (8ê°) | 3ë¨ê³ ë´ì© | ìì°¨ ì¹í |
â» ì¤ê³ ë 13í¬ì¸í¸(ë¬¸ë¨ ì 3) |
⻠주ì (7ê°) | 4ë¨ê³ 참조 | ìì°¨ ì¹í |
1. ì¸ë¶ë´ì© / 2. ì¸ë¶ë´ì© |
ë¶ì/ì°¸ê³ | ì²¨ë¶ ëª©ë¡ | ì¼ê´ ì¹í |
기본 ìì ì¬ì© ìì (ì ì²´ ì½ë)
import shutil, subprocess
# ìì ë³µì¬
TEMPLATE = "/mnt/skills/user/hwpx/assets/report-template.hwpx"
WORK = "/home/claude/report.hwpx"
shutil.copy(TEMPLATE, WORK)
# 1. íì§ + 목차 + ì¹ì
ë° + ì 목 (ì¼ê´ ì¹í)
zip_replace(WORK, WORK, {
"ë¸ë¼ë 공기ê´": "ì¤ì 기ê´ëª
",
"기본 ë³´ê³ ì ìì": "ì¤ì ë³´ê³ ì ì 목",
"2024. 5. 23.": "2026. 2. 13.",
"ì 목": "ì¤ì ë³´ê³ ì ì 목",
". ê°ì": ". ì¤ì 목차1",
". ì¶ì§ë°°ê²½": ". ì¤ì 목차2",
# ... ëë¨¸ì§ ëª©ì°¨, ì¹ì
ë° ì¹í
})
# 2. â¡ í목 (ìì°¨ ì¹í â 8ê°)
zip_replace_sequential(WORK, WORK,
"í¤ëë¼ì¸M í°í¸ 16í¬ì¸í¸(ë¬¸ë¨ ì 15)",
["첫ë²ì§¸ â¡ ë´ì©", "ëë²ì§¸ â¡ ë´ì©", ...]
)
# 3. â, â, â» í목ë ê°ê° ìì°¨ ì¹í
# ...
# 4. ë¤ìì¤íì´ì¤ íì²ë¦¬ (íì!)
subprocess.run(
["python", "/mnt/skills/user/hwpx/scripts/fix_namespaces.py", WORK],
check=True
)
# 5. ê²°ê³¼ ê²ì¦
from hwpx import ObjectFinder
finder = ObjectFinder(WORK)
for r in finder.find_all(tag="t"):
if r.text and r.text.strip():
print(r.text)
ì¬ì©ì ì ë¡ë ìì íì© ê°ì´ë
ì¬ì©ìê° ìì ë§ì .hwpx ììì ì
ë¡ëí ê²½ì°:
import shutil, subprocess
# 1. ì¬ì©ì ììì ìì
ëë í ë¦¬ë¡ ë³µì¬
USER_TEMPLATE = "/mnt/user-data/uploads/ì¬ì©ììì.hwpx"
WORK = "/home/claude/report.hwpx"
shutil.copy(USER_TEMPLATE, WORK)
# 2. ìì ë´ í
ì¤í¸ ì ì ì¡°ì¬ (â
íì ë¨ê³!)
from hwpx import ObjectFinder
finder = ObjectFinder(WORK)
for r in finder.find_all(tag="t"):
if r.text and r.text.strip():
print(repr(r.text))
# 3. ì¡°ì¬ ê²°ê³¼ë¥¼ ë°íì¼ë¡ ì¹í 매í ìì±
# (ììë§ë¤ íë ì´ì¤íëê° ë¤ë¥´ë¯ë¡ ë°ëì ì¡°ì¬ í ì§í)
# 4. ZIP-level ì¹í ì ì©
zip_replace(WORK, WORK, {
"ììì 기존 í
ì¤í¸": "ì¤ì ë´ì©",
# ...
})
# ëì¼ íë ì´ì¤íëê° ì¬ë¬ ë² â ìì°¨ ì¹í
zip_replace_sequential(WORK, WORK, "ë°ë³µëë í
ì¤í¸", ["ê°1", "ê°2", ...])
# 5. ë¤ìì¤íì´ì¤ íì²ë¦¬
subprocess.run(
["python", "/mnt/skills/user/hwpx/scripts/fix_namespaces.py", WORK],
check=True
)
# 6. ì¹í ê²°ê³¼ ê²ì¦
finder = ObjectFinder(WORK)
for r in finder.find_all(tag="t"):
if r.text and r.text.strip():
print(r.text)
문ì ì íë³ ì¤íì¼ ê°ì´ë
ë³´ê³ ì(ë´ë¶ ë³´ê³ ì©) ìì± ì
â references/report-style.md 를 먼ì ì½ê³ ë°ë¥¼ ê²
공문ì(기ì문) ìì± ì
â references/official-doc-style.md 를 먼ì ì½ê³ ë°ë¥¼ ê²
ì ìì¤ XML ì¡°ìì´ íìí ê²½ì°
â references/xml-internals.md 를 ì½ì ê²
â ï¸ íì íì²ë¦¬: ë¤ìì¤íì´ì¤ ìì
ê°ì¥ ì¤ìí ë¨ê³. ë¹ ë¨ë¦¬ë©´ íê¸ Viewerìì ë¹ íì´ì§ë¡ íìëë¤.
ZIP-level ì¹í í ëë doc.save() í ë°ëì ì¤í:
subprocess.run(
["python", "/mnt/skills/user/hwpx/scripts/fix_namespaces.py", "output.hwpx"],
check=True
)
주ì:
exec(open(...).read())ë°©ìì ì¤í¬ë¦½í¸ìif __name__ == "__main__"ë¸ë¡ ë문ì ì¤ëìí ì ìë¤. ë°ëìsubprocess.run()ë°©ìì ì¬ì©íë¤.
Quick Reference
| ìì | ì ê·¼ ë°©ì |
|---|---|
| ë³´ê³ ì/공문/ìì 문ì ìì± | ìì íì¼ + ZIP-level ì¹í (â ê¶ì¥) |
| ì주 ë¨ìí 문ì | HwpxDocument.new() â .save() â íì²ë¦¬ |
| í(í ì´ë¸) ì¶ê° | doc.add_table(rows, cols) â set_cell_text() |
| 머리ê¸/ë°ë¥ê¸ | doc.set_header_text() / doc.set_footer_text() |
| í ì¤í¸ ê²ì/ì¶ì¶ | ObjectFinder(filepath) |
| ì ë³í© | table.merge_cells(row1, col1, row2, col2) |
주ìì¬í
- ìì ì°ì : ì¬ì©ì ì ë¡ë ìì > 기본 ì ê³µ ìì > HwpxDocument.new()
- ZIP-level ì¹í ì°ì : HwpxDocument.open()ë³´ë¤ ZIP-level ì¹íì´ ìì íê³ í¸íì±ì´ ëë¤
- ë¤ìì¤íì´ì¤ íì²ë¦¬ íì: 모ë ì ì¥/ì¹í í
fix_namespaces.pyì¤í - ìì í ì¤í¸ ì¡°ì¬ íì: ì¹í ì ì ë°ëì ObjectFinderë¡ í ì¤í¸ ì ì ì¡°ì¬
- ìì°¨ ì¹í 주ì: ëì¼ íë ì´ì¤íëê° ì¬ë¬ ë² ëì¤ë©´
zip_replace_sequentialì¬ì© - ë ì´ìì ì¶©ì¤ë: python-hwpxë ë ì´ìì ìì§ì´ ìë. íì´ì§ ëëì íê¸ ì±ì´ ê²°ì
- ê¸ê¼´ ìë² ë©: ìì± HWPXì ê¸ê¼´ 미í¬í¨. ì´ë íê²½ì í´ë¹ ê¸ê¼´ íì
- 공문ì ë ì§ íì:
2026-02-13ì´ ìë2026. 2. 13.(ìÂ·ì¼ ì 0 ìëµ) - HWPX â HWP: python-hwpxë HWPXë§ ì²ë¦¬. ë ê±°ì
.hwpë ë³ë ë구 íì - fix_namespaces í¸ì¶ë²:
exec()ë§ê³subprocess.run()ì¬ì©