ADR-004: Element-level API β€” Run / Paragraph / Cell / Section

Published

April 29, 2026

λ§₯락 (Context)

v3 의 ADR-003 으둜 닀쀑 λ¬Έμ„œκ°€ κΉ”λ”νžˆ μ •μ°©ν–ˆκ³ , 각 Document 에 7개 μ»¬λ ‰μ…˜ (fields, bookmarks, hyperlinks, images, paragraphs, styles, tables) 이 μžλ¦¬μž‘μ•˜ μŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ»¬λ ‰μ…˜μ΄ λ°˜ν™˜ν•˜λŠ” κ°œλ³„ element 듀은 μ—¬μ „νžˆ λΉˆμ•½ν•©λ‹ˆλ‹€:

  • doc.paragraphs[0] β†’ Paragraph β€” ν˜„μž¬ text, index μ •λ„λ§Œ
  • doc.tables[0] β†’ Table β€” cell(r, c) μ •λ„λ§Œ
  • doc.tables[0].cell(1, 1) β†’ Cell β€” text, border, fill setter κ°€ μ•½μ†λ§Œ 있음 (migration v1β†’v2 κ°€μ΄λ“œ μ°Έμ‘°)
  • Run (κΈ€μž λ‹¨μœ„ ν˜•μ‹ λ‹¨μœ„) 클래슀 μžμ²΄κ°€ μ—†μŒ

결과적으둜 μ‚¬μš©μžκ°€ β€œμ΄ λ‹¨λ½μ˜ 정렬을 λ°”κΎΈκ³  κΈ€μž μƒ‰λ§Œ λ°”κΎΈκ³  싢닀” 같은 μž‘μ—…μ„ ν•  λ•Œ μ—¬μ „νžˆ doc.actions.X.run() raw 둜 λ‚΄λ €κ°€μ•Ό ν•©λ‹ˆλ‹€. v3 의 doc.X 일관성이 element λ ˆλ²¨μ—μ„œ κΉ¨μ§€λŠ” μ…ˆμž…λ‹ˆλ‹€.

xlwings 와 비ꡐ:

# xlwings β€” element κΉŒμ§€ ν’€ API
sheet.range("A1:B5").value = [[1,2],[3,4]]
sheet.range("A1").color = (255, 200, 200)
sheet.range("A1").font.bold = True

hwpapi κ°€ 도달해야 ν•  λΉ„μŠ·ν•œ μˆ˜μ€€:

# λͺ©ν‘œ (v3.x β€” 이 ADR)
para = doc.paragraphs[0]
para.text = "μƒˆ λ³Έλ¬Έ"
para.parashape.align = "center"
for run in para.runs:
    run.charshape.bold = True

cell = doc.tables[0].cell(1, 1)
cell.text = "Q1"
cell.fill = "#FFE0E0"
cell.border = {"top": "double", "bottom": "solid"}

κ²°μ • (Decision)

4 μ’… element 클래슀λ₯Ό 정식 μ •μ˜ + 각자 자기 μ±…μž„ λ²”μœ„ λͺ…ν™•ν™”. λͺ¨λ“  element λŠ” ADR-003 의 doc-activate νŒ¨ν„΄μ„ κ·ΈλŒ€λ‘œ 따름 β€” element 의 λ©”μ†Œλ“œ μ§„μž… μ‹œ owning doc μžλ™ ν™œμ„±ν™”.

Element 맀트릭슀

Element μ»¬λ ‰μ…˜ μ§„μž… μ±…μž„
Paragraph doc.paragraphs[i] 단락 ν…μŠ€νŠΈ, ParaShape, runs μ»¬λ ‰μ…˜
Run paragraph.runs[i] ν•œ κΈ€μžν˜•μ‹ κ΅¬κ°„μ˜ text, CharShape
Table doc.tables[i] rows/cols, cell(r,c), ν‘œ λ‹¨μœ„ BorderFill
Cell table.cell(r, c) text, fill, border, charshape, parashape
Section (보λ₯˜) doc.sections[i] νŽ˜μ΄μ§€ μ„€μ •, 머리말/꼬리말 (v3.1)

ν‘œλ©΄ (각 element 의 핡심 surface)

class Paragraph:
    index           # 단락 번호 (0-based)
    text            # r/w property
    parashape       # ParaShape (r/w)
    charshape       # 단락 μ‹œμž‘ κΈ€μžμ˜ CharShape (r/w convenience)
    runs            # RunCollection
    insert_text(s)  # 이 단락 끝에 μΆ”κ°€
    delete()        # 이 단락 제거
    activate()      # μ»€μ„œλ₯Ό 이 λ‹¨λ½μœΌλ‘œ

class Run:
    paragraph       # owning Paragraph
    text            # r/w
    charshape       # CharShape (r/w)
    start, end      # 단락 μ•ˆμ˜ κΈ€μž μœ„μΉ˜ λ²”μœ„

class Table:
    index, rows, cols
    cell(r, c) β†’ Cell
    iter_cells()    # row-major
    delete()
    border_fill     # ν‘œ 전체 BorderFill
    activate()

class Cell:
    table           # owning Table
    row, col, address (e.g. "B2")
    text            # r/w
    charshape / parashape   # r/w
    fill            # str | Color | None β€” μ…€ λ°°κ²½
    border          # dict ν˜• setter ({"top": "solid", ...})
    width / height  # r/w (HWPUNIT)
    activate()      # 이 μ…€λ‘œ μ»€μ„œ 이동

doc-activate νŒ¨ν„΄ 적용

λͺ¨λ“  element λŠ” owning doc 의 reference λ₯Ό κ°–κ³ , 자기 λ©”μ†Œλ“œ/setter μ§„μž… μ‹œ doc.activate() λ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€ (ADR-003 의 _DocActions 와 동일 νŒ¨ν„΄):

class Paragraph:
    def __init__(self, doc, index):
        self._doc = doc
        self._index = index

    @property
    def text(self):
        self._doc.activate()
        # ... GetTextFile + λ²”μœ„ μΆ”μΆœ
        return ...

    @text.setter
    def text(self, value):
        self._doc.activate()
        # ... select range + insert_text

닀쀑 λ¬Έμ„œ ν™˜κ²½μ—μ„œ for p in doc1.paragraphs: p.text 와 for p in doc2.paragraphs: p.text κ°€ μžλ™ doc-switching λ˜λ©΄μ„œ μ•ˆμ „ν•˜κ²Œ λ™μž‘.

CharShape / ParaShape β€” κΈ°μ‘΄ ParameterSet μž¬μ‚¬μš©

v2 μ—μ„œ 이미 hwpapi.low.parametersets.CharShape / ParaShape κ°€ 있음. element 의 .charshape / .parashape λŠ” μ΄λ“€μ˜ μΈμŠ€ν„΄μŠ€λ₯Ό λ°˜ν™˜/ν• λ‹Ή. μ‚¬μš©μžλŠ” μ΅μˆ™ν•œ dict-like μ ‘κ·Ό:

cs = run.charshape      # CharShape μΈμŠ€ν„΄μŠ€
cs.bold = True
cs.height = 1400
run.charshape = cs      # setter β€” 적용

# λ˜λŠ” dict ν•œλ°©
run.charshape = {"bold": True, "height": 1400}

μ‚¬μš©μž λ©˜νƒˆ λͺ¨λΈ

App (process)
└── Document (per-doc)
    β”œβ”€β”€ μ»¬λ ‰μ…˜ (fields, bookmarks, ...)
    β”œβ”€β”€ paragraphs : Collection[Paragraph]
    β”‚   └── runs   : Collection[Run]
    └── tables : Collection[Table]
        └── cell(r, c) : Cell

각 λ ˆμ΄μ–΄λŠ” 자기 μ±…μž„λ§Œ β€” App 은 lifecycle, Document λŠ” doc-scoped ops, Element λŠ” element-scoped ops.

λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ (Phases)

Phase 1 β€” Paragraph + Run

  • Paragraph.text getter/setter
  • Paragraph.parashape getter/setter
  • Paragraph.runs (RunCollection)
  • Run.text, Run.charshape getter/setter
  • ν…ŒμŠ€νŠΈ: tests/test_paragraph.py, tests/test_run.py

Phase 2 β€” Table + Cell

  • Table.cell(r, c) κ°€ Cell λ°˜ν™˜
  • Cell.text, Cell.fill, Cell.border (border dict ν˜•)
  • Cell.charshape, Cell.parashape
  • Cell.activate() κ°€ μ»€μ„œλ₯Ό μ…€λ‘œ
  • ν…ŒμŠ€νŠΈ: tests/test_table.py, tests/test_cell.py

Phase 3 β€” RunCollection + iter_cells

  • paragraph.runs 의 iteration / 인덱싱
  • Table.iter_cells() row-major + filter
  • ν…ŒμŠ€νŠΈ μΆ”κ°€

Phase 4 β€” Section (보λ₯˜)

  • νŽ˜μ΄μ§€ μ„€μ •, 머리말/꼬리말 β€” v3.1 λ˜λŠ” 별도 ADR

μ‚¬μš© 예

단락 / Run

doc = app.docs.open("report.hwp")

# 1단락 ν…μŠ€νŠΈμ™€ μ •λ ¬
para = doc.paragraphs[0]
para.text = "1λΆ„κΈ° 보고"
para.parashape = {"align": "center", "line_spacing": 180}

# μ•ˆμ— μžˆλŠ” λͺ¨λ“  run 을 ꡡ게
for run in para.runs:
    run.charshape = {"bold": True}

# λ˜λŠ” ν•œ 단락 μ•ˆμ˜ μΌλΆ€λ§Œ
para.runs[0].charshape = {"text_color": "#E74C3C"}

Table / Cell

doc.insert_table(rows=4, cols=4)
table = doc.tables[-1]                  # 방금 λ§Œλ“  ν‘œ

table.cell(0, 0).text = "μ œν’ˆ"
table.cell(0, 1).text = "Q1"
table.cell(0, 2).text = "Q2"
table.cell(0, 3).text = "Q3"

# 헀더 μ…€ 일괄 μ„œμ‹
for c in range(4):
    cell = table.cell(0, c)
    cell.fill = "#34495E"
    cell.charshape = {"bold": True, "text_color": "#FFFFFF"}

# 첫 μ—΄λ§Œ ꡡ은 ν…Œλ‘λ¦¬
for r in range(1, 4):
    table.cell(r, 0).border = {"right": "double"}

닀쀑 λ¬Έμ„œ + element

src = app.docs.open("source.hwp")
dst = app.docs.add()

# doc-activate μžλ™ β€” element κ°€ owning doc 으둜 μžλ™ μ „ν™˜
for para in src.paragraphs:
    if para.text.startswith("β–‘"):
        # 이 μ‹œμ μ—” src ν™œμ„±, src.paragraphs[i].text λŠ” 정상
        pass

# dst μž‘μ—… β€” μžλ™ μ „ν™˜
for row in [["A", "B"], ["C", "D"]]:
    dst.insert_table(rows=1, cols=2)
    for c, val in enumerate(row):
        dst.tables[-1].cell(0, c).text = val

κ²°κ³Ό (Consequences)

긍정

  • API 일관성 β€” doc.X, paragraph.X, cell.X λͺ¨λ‘ 같은 νŒ¨ν„΄
  • 닀쀑 λ¬Έμ„œ μ•ˆμ „ β€” element 도 doc-activate μžλ™
  • raw action μ˜μ‘΄λ„ κ°μ†Œ β€” Cell μ„œμ‹ λ“± 일상 μž‘μ—…μ΄ element λ©”μ†Œλ“œλ‘œ κ°€λŠ₯
  • xlwings/openpyxl μ‚¬μš©μž μΉœμˆ™ β€” sheet.range, cell.font λ“±κ³Ό λ™ν˜•
  • ν…ŒμŠ€νŠΈ κ°€λŠ₯ β€” Mock 으둜 element λ‹¨μœ„ 검증

λΆ€μ • / μœ„ν—˜

  • 클래슀 수 증가 β€” Paragraph/Run/Table/Cell 4개 μ‹ κ·œ. μ»¬λ ‰μ…˜ 포함 8개 (RunCollection, CellCollection 도) 이미 λŠ˜μ–΄λ‚¨.
  • HWP 의 element 인덱싱 λΆˆμ•ˆμ • β€” λ¬Έμ„œ νŽΈμ§‘ 쀑 paragraph index κ°€ 밀릴 수 있음. cell 도 ν‘œ ν–‰/μ—΄ μΆ”κ°€ μ‹œ 변동. μ•ˆμ •μ  μ‹λ³„μžκ°€ ν•„μš”ν•œ 경우 μ‚¬μš©μžκ°€ reload ν•΄μ•Ό 함 (xlwings 도 동일).
  • CharShape/ParaShape 의 process-wide pset 문제 β€” element λ§ˆλ‹€ 자기 pset 을 λ³΄μœ ν•˜μ§€ μ•Šκ³  process 레벨 곡유. doc-activate 와 κ²°ν•©ν•΄ μ•ˆμ „ν•˜λ‚˜, λ™μ‹œμ„±μ€ μ—¬μ „νžˆ 보μž₯ μ•ˆ 됨 (Python GIL κ°€μ •).
  • Phase 1/2 κ΅¬ν˜„ λΉ„μš© β€” 각 element 에 λŒ€ν•΄ ν…μŠ€νŠΈ λ²”μœ„ 좔적, selection 관리, charshape 적용/읽기 인프라 ν•„μš”. μ•½ 600~1000 LOC μΆ”μ •.

λ―Έν•΄κ²° 질문 (Open questions)

  1. Run 의 경계 μ •μ˜ β€” HWP μ—μ„œ β€œλ™μΌ charshape 의 연속 κΈ€μžβ€ κ°€ run 의 μžμ—°μŠ€λŸ¬μš΄ μ •μ˜μ΄μ§€λ§Œ, 단락 μ•ˆμ—μ„œ 이λ₯Ό 효율적으둜 μˆœνšŒν•˜λŠ” API κ°€ μžˆλŠ”μ§€ 검증 ν•„μš”. μ—†μœΌλ©΄ paragraph.text λ₯Ό 캐릭터 λ‹¨μœ„λ‘œ μŠ€μΊ”ν•˜λ©° charshape λΉ„κ΅λ‘œ ꡬ간 κ²°μ • (느림).
  2. Cell.border 의 dict ν˜• 섀계 β€” {"top": "solid"} κ°€ 단일 λ³€, {"all": "solid"} κ°€ 4λ³€ 일괄, {"outer": "solid", "inner": "dot"} 같은 alias κΉŒμ§€ 지원할지.
  3. Element 의 동일성 (==) β€” 같은 ν‘œμ˜ 같은 셀을 두 번 κ°€μ Έμ˜¨ Cell 두 κ°œκ°€ == 인가? row/col 비ꡐ? raw COM ν•Έλ“€ 비ꡐ?
  4. Section / Header / Footer β€” 이번 ADR λ²”μœ„μ— 포함 μ—¬λΆ€. 뢄리 (v3.1) ꢌμž₯.
  5. iter λ°©μ‹μ˜ μ•ˆμ •μ„± β€” for p in doc.paragraphs: 쀑에 단락이 μΆ”κ°€/μ‚­μ œλ˜λ©΄? generator-based 으둜 lazy ν•˜κ²Œ κ°ˆμ§€, snapshot 으둜 κ³ μ •ν• μ§€.

μ°Έκ³ 

Back to top