claude-code-hwp-mcp 0.2.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 +409 -0
- package/dist/hwp-bridge.d.ts +67 -0
- package/dist/hwp-bridge.js +320 -0
- package/dist/hwpx-engine.d.ts +39 -0
- package/dist/hwpx-engine.js +187 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/prompts/hwp-prompts.d.ts +2 -0
- package/dist/prompts/hwp-prompts.js +368 -0
- package/dist/resources/document-resources.d.ts +3 -0
- package/dist/resources/document-resources.js +109 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +29 -0
- package/dist/tools/analysis-tools.d.ts +4 -0
- package/dist/tools/analysis-tools.js +414 -0
- package/dist/tools/composite-tools.d.ts +3 -0
- package/dist/tools/composite-tools.js +664 -0
- package/dist/tools/document-tools.d.ts +3 -0
- package/dist/tools/document-tools.js +264 -0
- package/dist/tools/editing-tools.d.ts +4 -0
- package/dist/tools/editing-tools.js +916 -0
- package/package.json +31 -0
- package/python/__pycache__/hwp_analyzer.cpython-313.pyc +0 -0
- package/python/__pycache__/hwp_editor.cpython-313.pyc +0 -0
- package/python/__pycache__/hwp_service.cpython-313.pyc +0 -0
- package/python/__pycache__/privacy_scanner.cpython-313.pyc +0 -0
- package/python/__pycache__/ref_reader.cpython-313.pyc +0 -0
- package/python/__pycache__/test_integration.cpython-313.pyc +0 -0
- package/python/hwp_analyzer.py +544 -0
- package/python/hwp_editor.py +933 -0
- package/python/hwp_service.py +1291 -0
- package/python/privacy_scanner.py +115 -0
- package/python/ref_reader.py +115 -0
- package/python/requirements.txt +2 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
"""HWP Document Editor - Fill and modify HWP documents.
|
|
2
|
+
Uses pyhwpx Hwp() only. Raw win32com is forbidden.
|
|
3
|
+
All file paths must use os.path.abspath().
|
|
4
|
+
|
|
5
|
+
Cell navigation uses sequential Tab (TableRightCell) traversal,
|
|
6
|
+
which handles merged cells better than row/col coordinate addressing.
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def insert_text_with_color(hwp, text, rgb=None):
|
|
13
|
+
"""텍스트를 지정 색상으로 삽입. rgb=(r,g,b) 또는 None(기본색)"""
|
|
14
|
+
if not rgb:
|
|
15
|
+
hwp.insert_text(text)
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
act = hwp.HAction
|
|
19
|
+
pset = hwp.HParameterSet.HCharShape
|
|
20
|
+
try:
|
|
21
|
+
act.GetDefault("CharShape", pset.HSet)
|
|
22
|
+
pset.TextColor = hwp.RGBColor(rgb[0], rgb[1], rgb[2])
|
|
23
|
+
act.Execute("CharShape", pset.HSet)
|
|
24
|
+
hwp.insert_text(text)
|
|
25
|
+
finally:
|
|
26
|
+
# 색상 복원 (기본 검정) — 에러 시에도 반드시 실행
|
|
27
|
+
try:
|
|
28
|
+
act.GetDefault("CharShape", pset.HSet)
|
|
29
|
+
pset.TextColor = hwp.RGBColor(0, 0, 0)
|
|
30
|
+
act.Execute("CharShape", pset.HSet)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def insert_text_with_style(hwp, text, style=None):
|
|
36
|
+
"""서식 지정 텍스트 삽입.
|
|
37
|
+
style: {
|
|
38
|
+
"color": [r,g,b], # 글자 색상
|
|
39
|
+
"bold": True/False, # 굵게
|
|
40
|
+
"italic": True/False, # 기울임
|
|
41
|
+
"underline": True/False, # 밑줄
|
|
42
|
+
"font_size": 12.0, # 글자 크기 (pt)
|
|
43
|
+
"font_name": "맑은 고딕", # 글꼴
|
|
44
|
+
"bg_color": [r,g,b], # 배경 색상
|
|
45
|
+
"strikeout": True/False, # 취소선
|
|
46
|
+
"char_spacing": -5, # 자간 (%, 기본 0)
|
|
47
|
+
"width_ratio": 90, # 장평 (%, 기본 100)
|
|
48
|
+
"font_name_hanja": "바탕",# 한자 글꼴
|
|
49
|
+
"font_name_japanese": "", # 일본어 글꼴
|
|
50
|
+
}
|
|
51
|
+
삽입 후 원래 서식으로 복원.
|
|
52
|
+
"""
|
|
53
|
+
if not style:
|
|
54
|
+
hwp.insert_text(text)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
act = hwp.HAction
|
|
58
|
+
pset = hwp.HParameterSet.HCharShape
|
|
59
|
+
|
|
60
|
+
# 현재 서식 저장
|
|
61
|
+
act.GetDefault("CharShape", pset.HSet)
|
|
62
|
+
saved_color = pset.TextColor
|
|
63
|
+
saved_bold = pset.Bold
|
|
64
|
+
saved_italic = pset.Italic
|
|
65
|
+
saved_underline = pset.UnderlineType
|
|
66
|
+
saved_height = pset.Height
|
|
67
|
+
saved_strikeout = pset.StrikeOutType
|
|
68
|
+
saved_char_spacing = None
|
|
69
|
+
saved_width_ratio = None
|
|
70
|
+
try:
|
|
71
|
+
saved_char_spacing = pset.SpacingHangul
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
try:
|
|
75
|
+
saved_width_ratio = pset.RatioHangul
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# 새 서식 적용
|
|
80
|
+
act.GetDefault("CharShape", pset.HSet)
|
|
81
|
+
|
|
82
|
+
if "color" in style:
|
|
83
|
+
c = style["color"]
|
|
84
|
+
pset.TextColor = hwp.RGBColor(c[0], c[1], c[2])
|
|
85
|
+
if "bold" in style:
|
|
86
|
+
pset.Bold = 1 if style["bold"] else 0
|
|
87
|
+
if "italic" in style:
|
|
88
|
+
pset.Italic = 1 if style["italic"] else 0
|
|
89
|
+
if "underline" in style:
|
|
90
|
+
pset.UnderlineType = 1 if style["underline"] else 0
|
|
91
|
+
if "font_size" in style:
|
|
92
|
+
pset.Height = int(style["font_size"] * 100) # pt → HWP 단위
|
|
93
|
+
if "font_name" in style:
|
|
94
|
+
pset.FaceNameHangul = style["font_name"]
|
|
95
|
+
pset.FaceNameLatin = style["font_name"]
|
|
96
|
+
if "bg_color" in style:
|
|
97
|
+
bg = style["bg_color"]
|
|
98
|
+
pset.ShadeColor = hwp.RGBColor(bg[0], bg[1], bg[2])
|
|
99
|
+
if "strikeout" in style:
|
|
100
|
+
pset.StrikeOutType = 1 if style["strikeout"] else 0
|
|
101
|
+
if "char_spacing" in style:
|
|
102
|
+
try:
|
|
103
|
+
pset.SpacingHangul = int(style["char_spacing"])
|
|
104
|
+
pset.SpacingLatin = int(style["char_spacing"])
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
if "width_ratio" in style:
|
|
108
|
+
try:
|
|
109
|
+
pset.RatioHangul = int(style["width_ratio"])
|
|
110
|
+
pset.RatioLatin = int(style["width_ratio"])
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
if "font_name_hanja" in style:
|
|
114
|
+
try:
|
|
115
|
+
pset.FaceNameHanja = style["font_name_hanja"]
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
if "font_name_japanese" in style:
|
|
119
|
+
try:
|
|
120
|
+
pset.FaceNameJapanese = style["font_name_japanese"]
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
act.Execute("CharShape", pset.HSet)
|
|
125
|
+
|
|
126
|
+
hwp.insert_text(text)
|
|
127
|
+
|
|
128
|
+
# 원래 서식 복원
|
|
129
|
+
act.GetDefault("CharShape", pset.HSet)
|
|
130
|
+
pset.TextColor = saved_color
|
|
131
|
+
pset.Bold = saved_bold
|
|
132
|
+
pset.Italic = saved_italic
|
|
133
|
+
pset.UnderlineType = saved_underline
|
|
134
|
+
pset.Height = saved_height
|
|
135
|
+
pset.StrikeOutType = saved_strikeout
|
|
136
|
+
if saved_char_spacing is not None:
|
|
137
|
+
try:
|
|
138
|
+
pset.SpacingHangul = saved_char_spacing
|
|
139
|
+
pset.SpacingLatin = saved_char_spacing
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
if saved_width_ratio is not None:
|
|
143
|
+
try:
|
|
144
|
+
pset.RatioHangul = saved_width_ratio
|
|
145
|
+
pset.RatioLatin = saved_width_ratio
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
act.Execute("CharShape", pset.HSet)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def set_paragraph_style(hwp, style=None):
|
|
152
|
+
"""현재 커서 위치의 단락 서식을 설정.
|
|
153
|
+
style: {
|
|
154
|
+
"align": "left"|"center"|"right"|"justify", # 정렬
|
|
155
|
+
"line_spacing": 160, # 줄간격 (%)
|
|
156
|
+
"space_before": 0, # 문단 앞 간격 (pt)
|
|
157
|
+
"space_after": 0, # 문단 뒤 간격 (pt)
|
|
158
|
+
"indent": 0, # 들여쓰기 (pt)
|
|
159
|
+
}
|
|
160
|
+
"""
|
|
161
|
+
if not style:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
act = hwp.HAction
|
|
165
|
+
pset = hwp.HParameterSet.HParaShape
|
|
166
|
+
|
|
167
|
+
act.GetDefault("ParaShape", pset.HSet)
|
|
168
|
+
|
|
169
|
+
align_map = {"left": 0, "center": 1, "right": 2, "justify": 3}
|
|
170
|
+
if "align" in style:
|
|
171
|
+
try:
|
|
172
|
+
pset.AlignType = align_map.get(style["align"], 0)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
if "line_spacing" in style:
|
|
176
|
+
try:
|
|
177
|
+
pset.LineSpacingType = style.get("line_spacing_type", 0)
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
try:
|
|
181
|
+
pset.LineSpacing = int(style["line_spacing"])
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
if "space_before" in style:
|
|
185
|
+
try:
|
|
186
|
+
pset.PrevSpacing = int(style["space_before"] * 100)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
if "space_after" in style:
|
|
190
|
+
try:
|
|
191
|
+
pset.NextSpacing = int(style["space_after"] * 100)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
if "indent" in style:
|
|
195
|
+
try:
|
|
196
|
+
pset.Indentation = int(style["indent"] * 100)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
if "left_margin" in style:
|
|
200
|
+
try:
|
|
201
|
+
pset.LeftMargin = int(style["left_margin"] * 100)
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
if "right_margin" in style:
|
|
205
|
+
try:
|
|
206
|
+
pset.RightMargin = int(style["right_margin"] * 100)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
act.Execute("ParaShape", pset.HSet)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_char_shape(hwp):
|
|
214
|
+
"""현재 커서 위치의 글자 서식 정보를 반환."""
|
|
215
|
+
act = hwp.HAction
|
|
216
|
+
pset = hwp.HParameterSet.HCharShape
|
|
217
|
+
act.GetDefault("CharShape", pset.HSet)
|
|
218
|
+
|
|
219
|
+
font_hangul = ""
|
|
220
|
+
font_latin = ""
|
|
221
|
+
try:
|
|
222
|
+
font_hangul = pset.FaceNameHangul or ""
|
|
223
|
+
font_latin = pset.FaceNameLatin or ""
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# 자간: SpacingHangul (언어별 분리, 한글 기준)
|
|
228
|
+
# 장평: RatioHangul (언어별 분리, 한글 기준)
|
|
229
|
+
char_spacing = 0
|
|
230
|
+
width_ratio = 100
|
|
231
|
+
try:
|
|
232
|
+
char_spacing = pset.SpacingHangul
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
try:
|
|
236
|
+
width_ratio = pset.RatioHangul
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"font_name_hangul": font_hangul,
|
|
242
|
+
"font_name_latin": font_latin,
|
|
243
|
+
"font_size": pset.Height / 100.0, # HWP 단위 → pt
|
|
244
|
+
"bold": bool(pset.Bold),
|
|
245
|
+
"italic": bool(pset.Italic),
|
|
246
|
+
"underline": pset.UnderlineType,
|
|
247
|
+
"strikeout": pset.StrikeOutType,
|
|
248
|
+
"color": pset.TextColor,
|
|
249
|
+
"char_spacing": char_spacing,
|
|
250
|
+
"width_ratio": width_ratio,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_para_shape(hwp):
|
|
255
|
+
"""현재 커서 위치의 단락 서식 정보를 반환.
|
|
256
|
+
|
|
257
|
+
pyhwpx HParaShape 실제 속성명 (dir() 확인 결과):
|
|
258
|
+
- AlignType (정렬), LineSpacing, LineSpacingType
|
|
259
|
+
- PrevSpacing (문단 앞), NextSpacing (문단 뒤)
|
|
260
|
+
- Indentation (들여쓰기), LeftMargin, RightMargin
|
|
261
|
+
"""
|
|
262
|
+
act = hwp.HAction
|
|
263
|
+
pset = hwp.HParameterSet.HParaShape
|
|
264
|
+
act.GetDefault("ParaShape", pset.HSet)
|
|
265
|
+
|
|
266
|
+
align_names = {0: "left", 1: "center", 2: "right", 3: "justify"}
|
|
267
|
+
spacing_type_names = {0: "percent", 1: "fixed", 2: "multiple"}
|
|
268
|
+
|
|
269
|
+
alignment = 0
|
|
270
|
+
try:
|
|
271
|
+
alignment = pset.AlignType
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
line_spacing = 160
|
|
276
|
+
try:
|
|
277
|
+
line_spacing = pset.LineSpacing
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
line_spacing_type = 0
|
|
282
|
+
try:
|
|
283
|
+
line_spacing_type = pset.LineSpacingType
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
space_before = 0
|
|
288
|
+
try:
|
|
289
|
+
space_before = pset.PrevSpacing / 100.0
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
space_after = 0
|
|
294
|
+
try:
|
|
295
|
+
space_after = pset.NextSpacing / 100.0
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
indent = 0
|
|
300
|
+
try:
|
|
301
|
+
val = pset.Indentation
|
|
302
|
+
if val:
|
|
303
|
+
indent = val / 100.0
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
left_margin = 0
|
|
308
|
+
try:
|
|
309
|
+
left_margin = pset.LeftMargin / 100.0
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
right_margin = 0
|
|
314
|
+
try:
|
|
315
|
+
right_margin = pset.RightMargin / 100.0
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"align": align_names.get(alignment, "left"),
|
|
321
|
+
"line_spacing": line_spacing,
|
|
322
|
+
"line_spacing_type": spacing_type_names.get(line_spacing_type, "percent"),
|
|
323
|
+
"space_before": space_before,
|
|
324
|
+
"space_after": space_after,
|
|
325
|
+
"indent": indent, # 첫 줄 들여쓰기 (양수=들여쓰기, 음수=내어쓰기)
|
|
326
|
+
"left_margin": left_margin, # 왼쪽 여백 = 나머지 줄 시작위치
|
|
327
|
+
"right_margin": right_margin, # 오른쪽 여백
|
|
328
|
+
# 첫 줄 시작위치 = left_margin + indent
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_cell_format(hwp, table_idx, cell_tab):
|
|
333
|
+
"""특정 표 셀의 글자+단락 서식을 조회.
|
|
334
|
+
|
|
335
|
+
table_idx: 표 인덱스
|
|
336
|
+
cell_tab: Tab 인덱스 (hwp_map_table_cells로 확인)
|
|
337
|
+
Returns: {"char": {...}, "para": {...}, "text_preview": str}
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
hwp.get_into_nth_table(table_idx)
|
|
341
|
+
for _ in range(cell_tab):
|
|
342
|
+
hwp.TableRightCell()
|
|
343
|
+
|
|
344
|
+
text_preview = ""
|
|
345
|
+
try:
|
|
346
|
+
hwp.HAction.Run("SelectAll")
|
|
347
|
+
text_preview = hwp.GetTextFile("TEXT", "saveblock").strip()[:100]
|
|
348
|
+
hwp.HAction.Run("Cancel")
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
char = get_char_shape(hwp)
|
|
353
|
+
para = get_para_shape(hwp)
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
"table_index": table_idx,
|
|
357
|
+
"cell_tab": cell_tab,
|
|
358
|
+
"text_preview": text_preview,
|
|
359
|
+
"char": char,
|
|
360
|
+
"para": para,
|
|
361
|
+
}
|
|
362
|
+
finally:
|
|
363
|
+
try:
|
|
364
|
+
hwp.Cancel()
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def get_table_format_summary(hwp, table_idx, sample_tabs=None):
|
|
370
|
+
"""표 전체의 서식 요약을 반환. sample_tabs 미지정 시 첫 5개 + 마지막 셀."""
|
|
371
|
+
from hwp_analyzer import map_table_cells
|
|
372
|
+
|
|
373
|
+
cell_data = map_table_cells(hwp, table_idx)
|
|
374
|
+
cell_map = cell_data.get("cell_map", [])
|
|
375
|
+
|
|
376
|
+
if not cell_map:
|
|
377
|
+
return {"table_index": table_idx, "cell_formats": [], "error": "표에 셀이 없습니다"}
|
|
378
|
+
|
|
379
|
+
if sample_tabs is None:
|
|
380
|
+
total = len(cell_map)
|
|
381
|
+
tabs = list(range(min(5, total)))
|
|
382
|
+
if total > 5:
|
|
383
|
+
tabs.append(total - 1)
|
|
384
|
+
sample_tabs = tabs
|
|
385
|
+
|
|
386
|
+
formats = []
|
|
387
|
+
for tab in sample_tabs:
|
|
388
|
+
if tab >= len(cell_map):
|
|
389
|
+
continue
|
|
390
|
+
try:
|
|
391
|
+
fmt = get_cell_format(hwp, table_idx, tab)
|
|
392
|
+
fmt["text_preview"] = cell_map[tab]["text"][:50] if tab < len(cell_map) else ""
|
|
393
|
+
formats.append(fmt)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
formats.append({"cell_tab": tab, "error": str(e)})
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
"table_index": table_idx,
|
|
399
|
+
"total_cells": len(cell_map),
|
|
400
|
+
"sampled_cells": len(formats),
|
|
401
|
+
"cell_formats": formats,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _goto_cell(hwp, table_idx, cell_positions, target_cell_idx):
|
|
406
|
+
"""Navigate to a specific cell by its sequential index using Tab."""
|
|
407
|
+
hwp.get_into_nth_table(table_idx)
|
|
408
|
+
|
|
409
|
+
for _ in range(target_cell_idx):
|
|
410
|
+
try:
|
|
411
|
+
hwp.TableRightCell()
|
|
412
|
+
except Exception:
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _navigate_to_tab(hwp, table_idx, target_tab, current_tab):
|
|
417
|
+
"""셀 네비게이션 공통 로직. 새 current_tab을 반환."""
|
|
418
|
+
moves = target_tab - current_tab
|
|
419
|
+
if moves < 0:
|
|
420
|
+
try:
|
|
421
|
+
hwp.Cancel()
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
hwp.get_into_nth_table(table_idx)
|
|
425
|
+
moves = target_tab
|
|
426
|
+
for _ in range(moves):
|
|
427
|
+
hwp.TableRightCell()
|
|
428
|
+
return target_tab
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def fill_table_cells_by_tab(hwp, table_idx, cells):
|
|
432
|
+
"""Fill table cells using Tab index navigation.
|
|
433
|
+
|
|
434
|
+
Handles merged cells correctly by using sequential Tab traversal
|
|
435
|
+
instead of row/col coordinate navigation.
|
|
436
|
+
|
|
437
|
+
cells: list of {"tab": int, "text": str, "style": {...} (optional)}
|
|
438
|
+
"""
|
|
439
|
+
# Filter out cells with invalid tab values
|
|
440
|
+
cells = [c for c in cells if isinstance(c.get("tab"), int) and c["tab"] >= 0]
|
|
441
|
+
|
|
442
|
+
if not cells:
|
|
443
|
+
return {"filled": 0, "failed": 0, "errors": []}
|
|
444
|
+
|
|
445
|
+
result = {"filled": 0, "failed": 0, "errors": []}
|
|
446
|
+
|
|
447
|
+
# Sort by tab index for sequential forward navigation
|
|
448
|
+
sorted_cells = sorted(cells, key=lambda c: c["tab"])
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
hwp.get_into_nth_table(table_idx)
|
|
452
|
+
current_tab = 0
|
|
453
|
+
|
|
454
|
+
for cell in sorted_cells:
|
|
455
|
+
try:
|
|
456
|
+
target_tab = cell["tab"]
|
|
457
|
+
text = str(cell.get("text", ""))
|
|
458
|
+
|
|
459
|
+
current_tab = _navigate_to_tab(hwp, table_idx, target_tab, current_tab)
|
|
460
|
+
|
|
461
|
+
# 선택 영역 대체 — style 지정 시 명시적 서식, 미지정 시 기존 서식 상속
|
|
462
|
+
hwp.HAction.Run("SelectAll")
|
|
463
|
+
cell_style = cell.get("style")
|
|
464
|
+
if cell_style:
|
|
465
|
+
insert_text_with_style(hwp, text, cell_style)
|
|
466
|
+
else:
|
|
467
|
+
hwp.insert_text(text)
|
|
468
|
+
result["filled"] += 1
|
|
469
|
+
|
|
470
|
+
except Exception as e:
|
|
471
|
+
result["failed"] += 1
|
|
472
|
+
result["errors"].append(
|
|
473
|
+
f"Table{table_idx} tab{cell.get('tab')} failed: {e}"
|
|
474
|
+
)
|
|
475
|
+
print(f"[WARN] Tab cell fill error: {e}", file=sys.stderr)
|
|
476
|
+
|
|
477
|
+
finally:
|
|
478
|
+
try:
|
|
479
|
+
hwp.Cancel()
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
return result
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def smart_fill_table_cells(hwp, table_idx, cells):
|
|
487
|
+
"""서식 감지 후 자동 적용하는 표 셀 채우기.
|
|
488
|
+
|
|
489
|
+
각 셀에 진입 → 기존 서식 읽기 → 서식 보존하며 텍스트 삽입.
|
|
490
|
+
적용된 서식 정보도 함께 반환하여 AI가 서식을 "볼 수 있게" 함.
|
|
491
|
+
|
|
492
|
+
cells: [{"tab": int, "text": str}, ...]
|
|
493
|
+
"""
|
|
494
|
+
cells = [c for c in cells if isinstance(c.get("tab"), int) and c["tab"] >= 0]
|
|
495
|
+
if not cells:
|
|
496
|
+
return {"filled": 0, "failed": 0, "errors": [], "formats_applied": []}
|
|
497
|
+
|
|
498
|
+
result = {"filled": 0, "failed": 0, "errors": [], "formats_applied": []}
|
|
499
|
+
sorted_cells = sorted(cells, key=lambda c: c["tab"])
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
hwp.get_into_nth_table(table_idx)
|
|
503
|
+
current_tab = 0
|
|
504
|
+
|
|
505
|
+
for cell in sorted_cells:
|
|
506
|
+
try:
|
|
507
|
+
target_tab = cell["tab"]
|
|
508
|
+
text = str(cell.get("text", ""))
|
|
509
|
+
|
|
510
|
+
current_tab = _navigate_to_tab(hwp, table_idx, target_tab, current_tab)
|
|
511
|
+
|
|
512
|
+
detected_char = get_char_shape(hwp)
|
|
513
|
+
detected_para = get_para_shape(hwp)
|
|
514
|
+
|
|
515
|
+
hwp.HAction.Run("SelectAll")
|
|
516
|
+
hwp.insert_text(text)
|
|
517
|
+
|
|
518
|
+
result["filled"] += 1
|
|
519
|
+
result["formats_applied"].append({
|
|
520
|
+
"tab": target_tab,
|
|
521
|
+
"char": detected_char,
|
|
522
|
+
"para": detected_para,
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
except Exception as e:
|
|
526
|
+
result["failed"] += 1
|
|
527
|
+
result["errors"].append(f"Table{table_idx} tab{cell.get('tab')} failed: {e}")
|
|
528
|
+
print(f"[WARN] Smart fill error: {e}", file=sys.stderr)
|
|
529
|
+
|
|
530
|
+
finally:
|
|
531
|
+
try:
|
|
532
|
+
hwp.Cancel()
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
return result
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def insert_markdown(hwp, md_text):
|
|
540
|
+
"""마크다운 텍스트를 한글 서식으로 변환하여 삽입.
|
|
541
|
+
|
|
542
|
+
지원: # 제목, **굵게**, *기울임*, - 목록, 일반 텍스트.
|
|
543
|
+
"""
|
|
544
|
+
import re
|
|
545
|
+
|
|
546
|
+
lines = md_text.split('\n')
|
|
547
|
+
inserted = 0
|
|
548
|
+
|
|
549
|
+
for line in lines:
|
|
550
|
+
stripped = line.strip()
|
|
551
|
+
if not stripped:
|
|
552
|
+
hwp.insert_text('\r\n')
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
# 제목 (# ~ ###)
|
|
556
|
+
heading_match = re.match(r'^(#{1,3})\s+(.+)$', stripped)
|
|
557
|
+
if heading_match:
|
|
558
|
+
level = len(heading_match.group(1))
|
|
559
|
+
title_text = heading_match.group(2)
|
|
560
|
+
# 제목: Bold + 큰 글씨
|
|
561
|
+
sizes = {1: 22, 2: 16, 3: 13}
|
|
562
|
+
insert_text_with_style(hwp, title_text + '\r\n', {
|
|
563
|
+
"bold": True,
|
|
564
|
+
"font_size": sizes.get(level, 13),
|
|
565
|
+
})
|
|
566
|
+
inserted += 1
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
# 목록 (- 또는 * 또는 숫자.)
|
|
570
|
+
list_match = re.match(r'^[\-\*]\s+(.+)$', stripped)
|
|
571
|
+
if list_match:
|
|
572
|
+
hwp.insert_text(' ◦ ' + list_match.group(1) + '\r\n')
|
|
573
|
+
inserted += 1
|
|
574
|
+
continue
|
|
575
|
+
|
|
576
|
+
numbered_match = re.match(r'^(\d+)\.\s+(.+)$', stripped)
|
|
577
|
+
if numbered_match:
|
|
578
|
+
hwp.insert_text(' ' + numbered_match.group(1) + '. ' + numbered_match.group(2) + '\r\n')
|
|
579
|
+
inserted += 1
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
# 인라인 서식 처리 (**굵게**, *기울임*)
|
|
583
|
+
# 간단 처리: **text** → text (bold로 삽입), 나머지 일반
|
|
584
|
+
parts = re.split(r'(\*\*[^*]+\*\*|\*[^*]+\*)', stripped)
|
|
585
|
+
for part in parts:
|
|
586
|
+
if part.startswith('**') and part.endswith('**'):
|
|
587
|
+
insert_text_with_style(hwp, part[2:-2], {"bold": True})
|
|
588
|
+
elif part.startswith('*') and part.endswith('*'):
|
|
589
|
+
insert_text_with_style(hwp, part[1:-1], {"italic": True})
|
|
590
|
+
else:
|
|
591
|
+
hwp.insert_text(part)
|
|
592
|
+
hwp.insert_text('\r\n')
|
|
593
|
+
inserted += 1
|
|
594
|
+
|
|
595
|
+
return {"status": "ok", "lines_inserted": inserted}
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def auto_map_reference_to_table(hwp, table_idx, ref_headers, ref_row):
|
|
599
|
+
"""참고자료의 헤더와 표의 라벨을 자동 매칭하여 채울 데이터 생성.
|
|
600
|
+
|
|
601
|
+
ref_headers: ["기업명", "대표자", "전화번호", ...]
|
|
602
|
+
ref_row: ["(주)플랜아이", "이명기", "042-934-3508", ...]
|
|
603
|
+
|
|
604
|
+
Returns: {"mappings": [{header, matched_label, tab, text}, ...], "unmapped": [...]}
|
|
605
|
+
"""
|
|
606
|
+
from hwp_analyzer import map_table_cells, _match_label
|
|
607
|
+
|
|
608
|
+
cell_data = map_table_cells(hwp, table_idx)
|
|
609
|
+
cell_map = cell_data.get("cell_map", [])
|
|
610
|
+
|
|
611
|
+
mappings = []
|
|
612
|
+
unmapped = []
|
|
613
|
+
|
|
614
|
+
for i, header in enumerate(ref_headers):
|
|
615
|
+
if i >= len(ref_row):
|
|
616
|
+
break
|
|
617
|
+
value = ref_row[i]
|
|
618
|
+
if not value or not header:
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
matched = False
|
|
622
|
+
for j, cell in enumerate(cell_map):
|
|
623
|
+
is_match, is_exact, ratio = _match_label(cell["text"], header)
|
|
624
|
+
if is_match and (is_exact or ratio > 0.5):
|
|
625
|
+
target_tab = j + 1
|
|
626
|
+
if target_tab < len(cell_map):
|
|
627
|
+
mappings.append({
|
|
628
|
+
"header": header,
|
|
629
|
+
"matched_label": cell["text"].strip()[:30],
|
|
630
|
+
"tab": target_tab,
|
|
631
|
+
"text": str(value),
|
|
632
|
+
})
|
|
633
|
+
matched = True
|
|
634
|
+
break
|
|
635
|
+
if not matched:
|
|
636
|
+
unmapped.append({"header": header, "value": str(value)})
|
|
637
|
+
|
|
638
|
+
return {"mappings": mappings, "unmapped": unmapped, "total_matched": len(mappings)}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def insert_picture(hwp, file_path, width=0, height=0):
|
|
642
|
+
"""현재 커서 위치에 이미지 삽입.
|
|
643
|
+
|
|
644
|
+
file_path: 이미지 파일 경로
|
|
645
|
+
width/height: mm 단위 (0이면 원본 크기)
|
|
646
|
+
"""
|
|
647
|
+
file_path = os.path.abspath(file_path)
|
|
648
|
+
if not os.path.exists(file_path):
|
|
649
|
+
raise FileNotFoundError(f"이미지 파일을 찾을 수 없습니다: {file_path}")
|
|
650
|
+
|
|
651
|
+
# pyhwpx insert_picture: Width/Height는 HWPUNIT (1mm = 283.46 HWP 단위)
|
|
652
|
+
hwp.insert_picture(file_path,
|
|
653
|
+
Width=int(width * 283.46) if width else 0,
|
|
654
|
+
Height=int(height * 283.46) if height else 0)
|
|
655
|
+
return {"status": "ok", "file_path": file_path, "width_mm": width, "height_mm": height}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def fill_table_cells_by_label(hwp, table_idx, cells):
|
|
659
|
+
"""라벨 기반으로 표 셀을 채운다.
|
|
660
|
+
|
|
661
|
+
cells: [{"label": str, "text": str, "direction": "right"|"below" (optional)}, ...]
|
|
662
|
+
|
|
663
|
+
1. resolve_labels_to_tabs()로 tab 인덱스 확보
|
|
664
|
+
2. fill_table_cells_by_tab()으로 실제 채우기
|
|
665
|
+
3. 매칭 실패한 라벨은 errors에 포함
|
|
666
|
+
"""
|
|
667
|
+
from hwp_analyzer import resolve_labels_to_tabs
|
|
668
|
+
|
|
669
|
+
resolution = resolve_labels_to_tabs(hwp, table_idx, cells)
|
|
670
|
+
resolved = resolution.get("resolved", [])
|
|
671
|
+
errors = resolution.get("errors", [])
|
|
672
|
+
|
|
673
|
+
result = {"filled": 0, "failed": len(errors), "errors": list(errors)}
|
|
674
|
+
|
|
675
|
+
if resolved:
|
|
676
|
+
tab_cells = [{"tab": r["tab"], "text": r["text"]} for r in resolved]
|
|
677
|
+
tab_result = fill_table_cells_by_tab(hwp, table_idx, tab_cells)
|
|
678
|
+
result["filled"] += tab_result["filled"]
|
|
679
|
+
result["failed"] += tab_result["failed"]
|
|
680
|
+
result["errors"].extend(tab_result["errors"])
|
|
681
|
+
|
|
682
|
+
# Include matched labels info for debugging
|
|
683
|
+
result["matched"] = [
|
|
684
|
+
{"label": r["matched_label"], "tab": r["tab"]} for r in resolved
|
|
685
|
+
]
|
|
686
|
+
|
|
687
|
+
return result
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def verify_after_fill(hwp, table_idx, expected_cells):
|
|
691
|
+
"""채우기 후 실제 값을 map_table_cells로 대조하여 검증.
|
|
692
|
+
|
|
693
|
+
expected_cells: [{"tab": int, "text": str}, ...]
|
|
694
|
+
Returns: {"verified": int, "mismatched": int, "details": [...]}
|
|
695
|
+
"""
|
|
696
|
+
from hwp_analyzer import map_table_cells
|
|
697
|
+
|
|
698
|
+
actual = map_table_cells(hwp, table_idx)
|
|
699
|
+
actual_map = {c["tab"]: c["text"] for c in actual.get("cell_map", [])}
|
|
700
|
+
|
|
701
|
+
result = {"verified": 0, "mismatched": 0, "details": []}
|
|
702
|
+
for cell in expected_cells:
|
|
703
|
+
tab = cell["tab"]
|
|
704
|
+
expected_text = str(cell.get("text", "")).strip()
|
|
705
|
+
actual_text = actual_map.get(tab, "").strip()
|
|
706
|
+
|
|
707
|
+
if expected_text in actual_text or actual_text in expected_text:
|
|
708
|
+
result["verified"] += 1
|
|
709
|
+
else:
|
|
710
|
+
result["mismatched"] += 1
|
|
711
|
+
result["details"].append({
|
|
712
|
+
"tab": tab,
|
|
713
|
+
"expected": expected_text[:50],
|
|
714
|
+
"actual": actual_text[:50],
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
return result
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def fill_document(hwp, fill_data):
|
|
721
|
+
"""Fill document with AI-generated content.
|
|
722
|
+
|
|
723
|
+
fill_data format:
|
|
724
|
+
{
|
|
725
|
+
"file_path": "...", # optional: open file first
|
|
726
|
+
"fields": {"name": "value"}, # field-based fill
|
|
727
|
+
"tables": [ # table-based fill
|
|
728
|
+
{
|
|
729
|
+
"index": 0,
|
|
730
|
+
"cells": [
|
|
731
|
+
{"row": 0, "col": 0, "text": "value"},
|
|
732
|
+
...
|
|
733
|
+
]
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
}
|
|
737
|
+
"""
|
|
738
|
+
# Open file if specified
|
|
739
|
+
if "file_path" in fill_data:
|
|
740
|
+
file_path = os.path.abspath(fill_data["file_path"])
|
|
741
|
+
hwp.open(file_path)
|
|
742
|
+
|
|
743
|
+
result = {"filled": 0, "failed": 0, "errors": []}
|
|
744
|
+
|
|
745
|
+
# Fill fields
|
|
746
|
+
if "fields" in fill_data and fill_data["fields"]:
|
|
747
|
+
try:
|
|
748
|
+
hwp.put_field_text(fill_data["fields"])
|
|
749
|
+
result["filled"] += len(fill_data["fields"])
|
|
750
|
+
except Exception as e:
|
|
751
|
+
result["errors"].append(f"Field fill failed: {e}")
|
|
752
|
+
result["failed"] += len(fill_data["fields"])
|
|
753
|
+
|
|
754
|
+
# Fill tables - each cell independently (re-enter table each time)
|
|
755
|
+
if "tables" in fill_data:
|
|
756
|
+
for table_data in fill_data["tables"]:
|
|
757
|
+
table_idx = table_data.get("index", 0)
|
|
758
|
+
cells = table_data.get("cells", [])
|
|
759
|
+
|
|
760
|
+
# Split: tab-based cells vs row/col cells
|
|
761
|
+
tab_cells = [c for c in cells if "tab" in c]
|
|
762
|
+
rowcol_cells = [c for c in cells if "tab" not in c]
|
|
763
|
+
|
|
764
|
+
if tab_cells:
|
|
765
|
+
tab_result = fill_table_cells_by_tab(hwp, table_idx, tab_cells)
|
|
766
|
+
result["filled"] += tab_result["filled"]
|
|
767
|
+
result["failed"] += tab_result["failed"]
|
|
768
|
+
result["errors"].extend(tab_result["errors"])
|
|
769
|
+
|
|
770
|
+
for cell in rowcol_cells:
|
|
771
|
+
try:
|
|
772
|
+
row = cell.get("row", 0)
|
|
773
|
+
col = cell.get("col", 0)
|
|
774
|
+
text = str(cell.get("text", cell.get("value", "")))
|
|
775
|
+
|
|
776
|
+
# Enter table fresh each time
|
|
777
|
+
hwp.get_into_nth_table(table_idx)
|
|
778
|
+
|
|
779
|
+
try:
|
|
780
|
+
# Navigate: first go down, then right
|
|
781
|
+
# Use HAction.Run for more reliable navigation
|
|
782
|
+
for _ in range(row):
|
|
783
|
+
hwp.HAction.Run("TableLowerCell")
|
|
784
|
+
for _ in range(col):
|
|
785
|
+
hwp.HAction.Run("TableRightCell")
|
|
786
|
+
|
|
787
|
+
# 선택 영역 대체 — 기존 서식 상속
|
|
788
|
+
hwp.HAction.Run("SelectAll")
|
|
789
|
+
hwp.insert_text(text)
|
|
790
|
+
result["filled"] += 1
|
|
791
|
+
|
|
792
|
+
finally:
|
|
793
|
+
try:
|
|
794
|
+
hwp.Cancel()
|
|
795
|
+
except Exception:
|
|
796
|
+
pass
|
|
797
|
+
|
|
798
|
+
except Exception as e:
|
|
799
|
+
result["failed"] += 1
|
|
800
|
+
result["errors"].append(
|
|
801
|
+
f"Table{table_idx} ({row},{col}) failed: {e}"
|
|
802
|
+
)
|
|
803
|
+
print(f"[WARN] Cell fill error: {e}", file=sys.stderr)
|
|
804
|
+
|
|
805
|
+
return result
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _hex_to_rgb(hex_color):
|
|
809
|
+
"""#RRGGBB 헥스 색상을 (r, g, b) 튜플로 변환."""
|
|
810
|
+
hex_color = hex_color.lstrip('#')
|
|
811
|
+
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def set_cell_background_color(hwp, table_idx, cells):
|
|
815
|
+
"""표 셀의 배경색을 설정합니다.
|
|
816
|
+
|
|
817
|
+
table_idx: 표 인덱스 (-1이면 현재 위치한 표)
|
|
818
|
+
cells: [{"tab": int, "color": "#RRGGBB"}, ...]
|
|
819
|
+
"""
|
|
820
|
+
if not cells:
|
|
821
|
+
return {"status": "ok", "colored": 0}
|
|
822
|
+
|
|
823
|
+
result = {"colored": 0, "failed": 0, "errors": []}
|
|
824
|
+
sorted_cells = sorted(cells, key=lambda c: c["tab"])
|
|
825
|
+
|
|
826
|
+
try:
|
|
827
|
+
if table_idx >= 0:
|
|
828
|
+
hwp.get_into_nth_table(table_idx)
|
|
829
|
+
current_tab = 0
|
|
830
|
+
|
|
831
|
+
for cell in sorted_cells:
|
|
832
|
+
try:
|
|
833
|
+
target_tab = cell["tab"]
|
|
834
|
+
color_hex = cell.get("color", "#E8E8E8")
|
|
835
|
+
r, g, b = _hex_to_rgb(color_hex)
|
|
836
|
+
|
|
837
|
+
# 셀 이동
|
|
838
|
+
if table_idx >= 0:
|
|
839
|
+
current_tab = _navigate_to_tab(hwp, table_idx, target_tab, current_tab)
|
|
840
|
+
else:
|
|
841
|
+
# 현재 표에서 탭 이동
|
|
842
|
+
moves = target_tab - current_tab
|
|
843
|
+
if moves < 0:
|
|
844
|
+
hwp.get_into_nth_table(0)
|
|
845
|
+
moves = target_tab
|
|
846
|
+
current_tab = 0
|
|
847
|
+
for _ in range(moves):
|
|
848
|
+
hwp.TableRightCell()
|
|
849
|
+
current_tab = target_tab
|
|
850
|
+
|
|
851
|
+
# 셀 배경색 설정 (CellShape → BackColor)
|
|
852
|
+
act = hwp.HAction
|
|
853
|
+
pset = hwp.HParameterSet.HCellShape
|
|
854
|
+
act.GetDefault("CellShape", pset.HSet)
|
|
855
|
+
pset.BackColor = hwp.RGBColor(r, g, b)
|
|
856
|
+
act.Execute("CellShape", pset.HSet)
|
|
857
|
+
result["colored"] += 1
|
|
858
|
+
|
|
859
|
+
except Exception as e:
|
|
860
|
+
result["failed"] += 1
|
|
861
|
+
result["errors"].append(f"tab {cell.get('tab')}: {e}")
|
|
862
|
+
print(f"[WARN] Cell color error: {e}", file=sys.stderr)
|
|
863
|
+
|
|
864
|
+
finally:
|
|
865
|
+
try:
|
|
866
|
+
hwp.Cancel()
|
|
867
|
+
except Exception:
|
|
868
|
+
pass
|
|
869
|
+
|
|
870
|
+
return {"status": "ok", **result}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def set_table_border_style(hwp, table_idx, cells=None, style=None):
|
|
874
|
+
"""표 테두리 스타일을 설정합니다.
|
|
875
|
+
|
|
876
|
+
table_idx: 표 인덱스
|
|
877
|
+
cells: 특정 셀만 적용 시 [{"tab": int}, ...] (None이면 표 전체)
|
|
878
|
+
style: {"line_type": int, "line_width": int} — HWP 테두리 속성
|
|
879
|
+
line_type: 0=없음, 1=실선, 2=파선, 3=점선, 4=1점쇄선, 5=2점쇄선
|
|
880
|
+
line_width: pt 단위 (기본 0.12mm → 약 0.4pt)
|
|
881
|
+
"""
|
|
882
|
+
if style is None:
|
|
883
|
+
style = {}
|
|
884
|
+
line_type = style.get("line_type", 1) # 기본: 실선
|
|
885
|
+
line_width = style.get("line_width", 0)
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
hwp.get_into_nth_table(table_idx)
|
|
889
|
+
|
|
890
|
+
if cells:
|
|
891
|
+
# 특정 셀만 테두리 적용
|
|
892
|
+
sorted_cells = sorted(cells, key=lambda c: c["tab"])
|
|
893
|
+
current_tab = 0
|
|
894
|
+
modified = 0
|
|
895
|
+
for cell in sorted_cells:
|
|
896
|
+
try:
|
|
897
|
+
target_tab = cell["tab"]
|
|
898
|
+
current_tab = _navigate_to_tab(hwp, table_idx, target_tab, current_tab)
|
|
899
|
+
act = hwp.HAction
|
|
900
|
+
pset = hwp.HParameterSet.HCellBorderFill
|
|
901
|
+
act.GetDefault("CellBorderFill", pset.HSet)
|
|
902
|
+
# 4방향 테두리 설정
|
|
903
|
+
for attr in ["Left", "Right", "Top", "Bottom"]:
|
|
904
|
+
line = getattr(pset, f"{attr}Border", None)
|
|
905
|
+
if line:
|
|
906
|
+
line.Type = line_type
|
|
907
|
+
if line_width:
|
|
908
|
+
line.Width = line_width
|
|
909
|
+
act.Execute("CellBorderFill", pset.HSet)
|
|
910
|
+
modified += 1
|
|
911
|
+
except Exception as e:
|
|
912
|
+
print(f"[WARN] Border error tab {cell.get('tab')}: {e}", file=sys.stderr)
|
|
913
|
+
return {"status": "ok", "modified": modified}
|
|
914
|
+
else:
|
|
915
|
+
# 표 전체: 표 블록 선택 후 적용
|
|
916
|
+
hwp.HAction.Run("TableCellBlockExtend")
|
|
917
|
+
hwp.HAction.Run("TableCellBlock")
|
|
918
|
+
act = hwp.HAction
|
|
919
|
+
pset = hwp.HParameterSet.HCellBorderFill
|
|
920
|
+
act.GetDefault("CellBorderFill", pset.HSet)
|
|
921
|
+
for attr in ["Left", "Right", "Top", "Bottom"]:
|
|
922
|
+
line = getattr(pset, f"{attr}Border", None)
|
|
923
|
+
if line:
|
|
924
|
+
line.Type = line_type
|
|
925
|
+
if line_width:
|
|
926
|
+
line.Width = line_width
|
|
927
|
+
act.Execute("CellBorderFill", pset.HSet)
|
|
928
|
+
return {"status": "ok", "applied": "whole_table"}
|
|
929
|
+
finally:
|
|
930
|
+
try:
|
|
931
|
+
hwp.Cancel()
|
|
932
|
+
except Exception:
|
|
933
|
+
pass
|