claude-code-hwp-mcp 0.3.1 → 0.5.1
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 +66 -24
- package/dist/hwp-bridge.js +17 -12
- package/dist/index.js +2 -2
- package/dist/tools/analysis-tools.js +183 -0
- package/dist/tools/editing-tools.js +86 -7
- package/package.json +2 -2
- package/python/hwp_editor.py +276 -71
- package/python/hwp_service.py +429 -42
- package/python/ref_reader.py +167 -2
- package/python/requirements.txt +1 -0
package/python/hwp_service.py
CHANGED
|
@@ -25,12 +25,13 @@ def validate_file_path(file_path, must_exist=True):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _execute_all_replace(hwp, find_str, replace_str, use_regex=False):
|
|
28
|
-
"""AllReplace 공통 함수.
|
|
28
|
+
"""AllReplace 공통 함수. 전후 텍스트 비교로 검증. H4: 타임아웃 시 낙관적 가정."""
|
|
29
29
|
# 치환 전 텍스트 캡처
|
|
30
|
+
before = None
|
|
30
31
|
try:
|
|
31
32
|
before = hwp.get_text_file("TEXT", "")
|
|
32
|
-
except Exception:
|
|
33
|
-
before
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"[WARN] get_text_file before failed: {e}", file=sys.stderr)
|
|
34
35
|
|
|
35
36
|
act = hwp.HAction
|
|
36
37
|
pset = hwp.HParameterSet.HFindReplace
|
|
@@ -46,10 +47,20 @@ def _execute_all_replace(hwp, find_str, replace_str, use_regex=False):
|
|
|
46
47
|
act.Execute("AllReplace", pset.HSet)
|
|
47
48
|
|
|
48
49
|
# 치환 후 텍스트 비교로 실제 변경 여부 판단
|
|
50
|
+
after = None
|
|
49
51
|
try:
|
|
50
52
|
after = hwp.get_text_file("TEXT", "")
|
|
51
|
-
except Exception:
|
|
52
|
-
after
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"[WARN] get_text_file after failed: {e}", file=sys.stderr)
|
|
55
|
+
|
|
56
|
+
# 2C3: 텍스트 캡처 실패 시 구분
|
|
57
|
+
if before is None and after is None:
|
|
58
|
+
# 양쪽 모두 실패 → Execute는 실행됨 → 낙관적 True
|
|
59
|
+
return True
|
|
60
|
+
if before is None or after is None:
|
|
61
|
+
# 한쪽만 실패 → 비교 불가하지만 Execute는 실행됨 → 로깅 후 True
|
|
62
|
+
print(f"[WARN] Partial text capture: before={'ok' if before is not None else 'fail'}, after={'ok' if after is not None else 'fail'}", file=sys.stderr)
|
|
63
|
+
return True
|
|
53
64
|
return before != after
|
|
54
65
|
|
|
55
66
|
|
|
@@ -158,7 +169,33 @@ def dispatch(hwp, method, params):
|
|
|
158
169
|
return {"text": text}
|
|
159
170
|
|
|
160
171
|
if method == "get_cursor_context":
|
|
161
|
-
|
|
172
|
+
# 실제 커서 위치의 서식 + 주변 텍스트 반환
|
|
173
|
+
from hwp_editor import get_char_shape, get_para_shape
|
|
174
|
+
context = {"status": "ok"}
|
|
175
|
+
try:
|
|
176
|
+
context["char_shape"] = get_char_shape(hwp)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
context["char_shape"] = {"error": str(e)}
|
|
179
|
+
try:
|
|
180
|
+
context["para_shape"] = get_para_shape(hwp)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
context["para_shape"] = {"error": str(e)}
|
|
183
|
+
try:
|
|
184
|
+
pos = hwp.GetPos()
|
|
185
|
+
context["position"] = list(pos) if pos else None
|
|
186
|
+
except Exception:
|
|
187
|
+
context["position"] = None
|
|
188
|
+
try:
|
|
189
|
+
context["total_pages"] = hwp.PageCount
|
|
190
|
+
except Exception:
|
|
191
|
+
context["total_pages"] = None
|
|
192
|
+
try:
|
|
193
|
+
# KeyIndicator: (섹션, 페이지, 줄, 컬럼, 삽입/수정, 줄번호)
|
|
194
|
+
ki = hwp.KeyIndicator()
|
|
195
|
+
context["current_page"] = ki[1] if ki else None
|
|
196
|
+
except Exception:
|
|
197
|
+
context["current_page"] = None
|
|
198
|
+
return context
|
|
162
199
|
|
|
163
200
|
if method == "save_as":
|
|
164
201
|
validate_params(params, ["path"], method)
|
|
@@ -257,7 +294,8 @@ def dispatch(hwp, method, params):
|
|
|
257
294
|
if not selected:
|
|
258
295
|
return {"status": "not_found", "find": params["find"]}
|
|
259
296
|
|
|
260
|
-
#
|
|
297
|
+
# 2C2 fix: 찾은 텍스트 끝으로 커서 이동
|
|
298
|
+
# FindReplace가 텍스트를 선택한 상태에서 MoveRight → 선택 해제 + 선택 끝으로 이동
|
|
261
299
|
hwp.HAction.Run("MoveRight")
|
|
262
300
|
|
|
263
301
|
# 색상 설정 (옵션)
|
|
@@ -272,16 +310,20 @@ def dispatch(hwp, method, params):
|
|
|
272
310
|
|
|
273
311
|
if method == "insert_text":
|
|
274
312
|
validate_params(params, ["text"], method)
|
|
313
|
+
text = params["text"]
|
|
314
|
+
# 각 insert_text 호출을 독립 문단으로 — 끝에 줄바꿈 자동 추가
|
|
315
|
+
if not text.endswith("\r\n") and not text.endswith("\n"):
|
|
316
|
+
text += "\r\n"
|
|
275
317
|
style = params.get("style")
|
|
276
318
|
color = params.get("color") # [r, g, b] 하위 호환
|
|
277
319
|
if style:
|
|
278
320
|
from hwp_editor import insert_text_with_style
|
|
279
|
-
insert_text_with_style(hwp,
|
|
321
|
+
insert_text_with_style(hwp, text, style)
|
|
280
322
|
elif color:
|
|
281
323
|
from hwp_editor import insert_text_with_color
|
|
282
|
-
insert_text_with_color(hwp,
|
|
324
|
+
insert_text_with_color(hwp, text, tuple(color))
|
|
283
325
|
else:
|
|
284
|
-
hwp.insert_text(
|
|
326
|
+
hwp.insert_text(text)
|
|
285
327
|
return {"status": "ok"}
|
|
286
328
|
|
|
287
329
|
if method == "set_paragraph_style":
|
|
@@ -424,10 +466,36 @@ def dispatch(hwp, method, params):
|
|
|
424
466
|
|
|
425
467
|
if method == "table_merge_cells":
|
|
426
468
|
validate_params(params, ["table_index"], method)
|
|
469
|
+
table_index = params["table_index"]
|
|
470
|
+
start_row = params.get("start_row")
|
|
471
|
+
start_col = params.get("start_col")
|
|
472
|
+
end_row = params.get("end_row")
|
|
473
|
+
end_col = params.get("end_col")
|
|
427
474
|
try:
|
|
428
|
-
hwp.get_into_nth_table(
|
|
429
|
-
|
|
430
|
-
|
|
475
|
+
hwp.get_into_nth_table(table_index)
|
|
476
|
+
if start_row is not None and end_row is not None and start_col is not None and end_col is not None:
|
|
477
|
+
# 범위 지정 병합 — 시작 셀로 이동 → 블록 선택 확장 → 병합
|
|
478
|
+
hwp.HAction.Run("TableColBegin")
|
|
479
|
+
hwp.HAction.Run("TableRowBegin")
|
|
480
|
+
for _ in range(start_row):
|
|
481
|
+
hwp.HAction.Run("TableLowerCell")
|
|
482
|
+
for _ in range(start_col):
|
|
483
|
+
hwp.HAction.Run("TableRightCell")
|
|
484
|
+
# 블록 선택 시작
|
|
485
|
+
hwp.HAction.Run("TableCellBlock")
|
|
486
|
+
# TableCellBlockExtend + 방향키로 블록 확장
|
|
487
|
+
for _ in range(end_col - start_col):
|
|
488
|
+
hwp.HAction.Run("TableCellBlockExtend")
|
|
489
|
+
hwp.HAction.Run("TableRightCell")
|
|
490
|
+
for _ in range(end_row - start_row):
|
|
491
|
+
hwp.HAction.Run("TableCellBlockExtend")
|
|
492
|
+
hwp.HAction.Run("TableLowerCell")
|
|
493
|
+
hwp.HAction.Run("TableMergeCell")
|
|
494
|
+
else:
|
|
495
|
+
# 기존 방식 (현재 선택된 셀 병합)
|
|
496
|
+
hwp.TableMergeCell()
|
|
497
|
+
return {"status": "ok", "table_index": table_index,
|
|
498
|
+
"range": {"start_row": start_row, "start_col": start_col, "end_row": end_row, "end_col": end_col} if start_row is not None else None}
|
|
431
499
|
except Exception as e:
|
|
432
500
|
raise RuntimeError(f"셀 병합 실패: {e}")
|
|
433
501
|
finally:
|
|
@@ -457,14 +525,50 @@ def dispatch(hwp, method, params):
|
|
|
457
525
|
raise ValueError("data must be a non-empty 2D array")
|
|
458
526
|
rows = len(data)
|
|
459
527
|
cols = max(len(row) for row in data) if data else 0
|
|
460
|
-
header_style = params.get("header_style", False)
|
|
461
|
-
|
|
462
|
-
#
|
|
528
|
+
header_style = params.get("header_style", False)
|
|
529
|
+
col_widths = params.get("col_widths") # [mm, mm, ...] H1 fix
|
|
530
|
+
row_heights = params.get("row_heights") # [mm, mm, ...]
|
|
531
|
+
alignment = params.get("alignment") # left/center/right
|
|
532
|
+
|
|
533
|
+
# H1: col_widths/row_heights가 있으면 HTableCreation으로 정밀 생성
|
|
534
|
+
if col_widths or row_heights:
|
|
535
|
+
try:
|
|
536
|
+
tc = hwp.HParameterSet.HTableCreation
|
|
537
|
+
hwp.HAction.GetDefault("TableCreate", tc.HSet)
|
|
538
|
+
tc.Rows = rows
|
|
539
|
+
tc.Cols = cols
|
|
540
|
+
tc.WidthType = 2 # 절대 너비
|
|
541
|
+
tc.HeightType = 0
|
|
542
|
+
if col_widths:
|
|
543
|
+
tc.CreateItemArray("ColWidth", cols)
|
|
544
|
+
for i, w in enumerate(col_widths[:cols]):
|
|
545
|
+
tc.ColWidth.SetItem(i, hwp.MiliToHwpUnit(w))
|
|
546
|
+
if row_heights:
|
|
547
|
+
tc.CreateItemArray("RowHeight", rows)
|
|
548
|
+
for i, h in enumerate(row_heights[:rows]):
|
|
549
|
+
tc.RowHeight.SetItem(i, hwp.MiliToHwpUnit(h))
|
|
550
|
+
hwp.HAction.Execute("TableCreate", tc.HSet)
|
|
551
|
+
except Exception as e:
|
|
552
|
+
print(f"[WARN] HTableCreation failed, fallback to create_table: {e}", file=sys.stderr)
|
|
553
|
+
hwp.create_table(rows, cols)
|
|
554
|
+
else:
|
|
555
|
+
hwp.create_table(rows, cols)
|
|
556
|
+
# 셀 채우기 (alignment 적용 포함)
|
|
557
|
+
align_map = {"left": 0, "center": 1, "right": 2}
|
|
463
558
|
filled = 0
|
|
464
559
|
for r, row in enumerate(data):
|
|
465
560
|
for c, val in enumerate(row):
|
|
561
|
+
# alignment 적용 (각 셀에 문단 정렬)
|
|
562
|
+
if alignment and alignment in align_map:
|
|
563
|
+
try:
|
|
564
|
+
act_p = hwp.HAction
|
|
565
|
+
ps = hwp.HParameterSet.HParaShape
|
|
566
|
+
act_p.GetDefault("ParaShape", ps.HSet)
|
|
567
|
+
ps.AlignType = align_map[alignment]
|
|
568
|
+
act_p.Execute("ParaShape", ps.HSet)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"[WARN] Cell align: {e}", file=sys.stderr)
|
|
466
571
|
if val:
|
|
467
|
-
# BUG-1 fix: SelectAll 제거 — 새 표의 빈 셀에 직접 삽입
|
|
468
572
|
if header_style and r == 0:
|
|
469
573
|
from hwp_editor import insert_text_with_style
|
|
470
574
|
insert_text_with_style(hwp, str(val), {"bold": True})
|
|
@@ -473,18 +577,15 @@ def dispatch(hwp, method, params):
|
|
|
473
577
|
filled += 1
|
|
474
578
|
if c < len(row) - 1 or r < rows - 1:
|
|
475
579
|
hwp.TableRightCell()
|
|
580
|
+
# 표 밖으로 커서 이동 (표 생성 후 커서가 표 안에 남아있음)
|
|
476
581
|
try:
|
|
477
|
-
hwp.Cancel()
|
|
582
|
+
hwp.Cancel() # 셀 선택 해제
|
|
583
|
+
hwp.HAction.Run("MoveDocEnd") # 문서 끝으로 이동
|
|
584
|
+
hwp.HAction.Run("BreakPara") # 새 문단 생성 (표 아래)
|
|
478
585
|
except Exception as e:
|
|
479
|
-
print(f"[WARN] {e}", file=sys.stderr)
|
|
480
|
-
#
|
|
481
|
-
|
|
482
|
-
try:
|
|
483
|
-
from hwp_editor import set_cell_background_color
|
|
484
|
-
header_cells = [{"tab": i, "color": "#E8E8E8"} for i in range(cols)]
|
|
485
|
-
set_cell_background_color(hwp, -1, header_cells) # -1 = 현재 표
|
|
486
|
-
except Exception:
|
|
487
|
-
pass # 배경색 실패해도 표 자체는 유지
|
|
586
|
+
print(f"[WARN] Table exit: {e}", file=sys.stderr)
|
|
587
|
+
# header_style: Bold는 이미 표 생성 시 적용됨
|
|
588
|
+
# 배경색은 set_cell_color로 별도 적용 (표 진입/탈출 부작용 방지)
|
|
488
589
|
return {"status": "ok", "rows": rows, "cols": cols, "filled": filled, "header_styled": bool(header_style)}
|
|
489
590
|
|
|
490
591
|
if method == "table_insert_from_csv":
|
|
@@ -561,6 +662,260 @@ def dispatch(hwp, method, params):
|
|
|
561
662
|
"path": save_path, "format": fmt,
|
|
562
663
|
"success": bool(result), "file_exists": file_exists, "file_size": file_size}
|
|
563
664
|
|
|
665
|
+
if method == "verify_layout":
|
|
666
|
+
# PDF로 내보내고 PNG 이미지로 변환 → Claude Code의 Read로 시각적 검증
|
|
667
|
+
import tempfile
|
|
668
|
+
tmp_pdf = os.path.join(tempfile.gettempdir(), "hwp_verify_layout.pdf")
|
|
669
|
+
try:
|
|
670
|
+
hwp.save_as(tmp_pdf, "PDF")
|
|
671
|
+
if not os.path.exists(tmp_pdf):
|
|
672
|
+
return {"status": "error", "error": "PDF 생성 실패"}
|
|
673
|
+
|
|
674
|
+
# PDF → PNG 변환 (PyMuPDF)
|
|
675
|
+
try:
|
|
676
|
+
import fitz
|
|
677
|
+
doc = fitz.open(tmp_pdf)
|
|
678
|
+
image_paths = []
|
|
679
|
+
page_range = params.get("pages") # "1", "1-3" 등
|
|
680
|
+
start_page = 0
|
|
681
|
+
end_page = doc.page_count
|
|
682
|
+
|
|
683
|
+
if page_range:
|
|
684
|
+
parts = str(page_range).split("-")
|
|
685
|
+
start_page = max(0, int(parts[0]) - 1)
|
|
686
|
+
end_page = int(parts[-1]) if len(parts) > 1 else start_page + 1
|
|
687
|
+
|
|
688
|
+
for i in range(start_page, min(end_page, doc.page_count)):
|
|
689
|
+
pix = doc[i].get_pixmap(dpi=150)
|
|
690
|
+
png_path = os.path.join(tempfile.gettempdir(), f"hwp_verify_page{i+1}.png")
|
|
691
|
+
pix.save(png_path)
|
|
692
|
+
image_paths.append(png_path)
|
|
693
|
+
|
|
694
|
+
doc.close()
|
|
695
|
+
return {
|
|
696
|
+
"status": "ok",
|
|
697
|
+
"image_paths": image_paths,
|
|
698
|
+
"pages": len(image_paths),
|
|
699
|
+
"total_pages": hwp.PageCount,
|
|
700
|
+
"hint": "Read 도구로 각 PNG 이미지를 열어 레이아웃을 시각적으로 검증하세요."
|
|
701
|
+
}
|
|
702
|
+
except ImportError:
|
|
703
|
+
# PyMuPDF 미설치 → PDF 경로만 반환
|
|
704
|
+
return {
|
|
705
|
+
"status": "ok_pdf_only",
|
|
706
|
+
"pdf_path": tmp_pdf,
|
|
707
|
+
"pages": hwp.PageCount,
|
|
708
|
+
"file_size": os.path.getsize(tmp_pdf),
|
|
709
|
+
"hint": "PyMuPDF 미설치. 'pip install PyMuPDF' 실행 후 다시 시도하면 PNG 이미지로 자동 변환됩니다."
|
|
710
|
+
}
|
|
711
|
+
except Exception as e:
|
|
712
|
+
return {"status": "error", "error": f"레이아웃 검증 실패: {e}"}
|
|
713
|
+
|
|
714
|
+
if method == "set_page_setup":
|
|
715
|
+
# 페이지 설정 (여백, 용지 크기, 방향)
|
|
716
|
+
try:
|
|
717
|
+
act = hwp.HAction
|
|
718
|
+
pset = hwp.HParameterSet.HSecDef
|
|
719
|
+
act.GetDefault("PageSetup", pset.HSet)
|
|
720
|
+
pdef = pset.PageDef
|
|
721
|
+
if "top_margin" in params:
|
|
722
|
+
pdef.TopMargin = hwp.MiliToHwpUnit(params["top_margin"])
|
|
723
|
+
if "bottom_margin" in params:
|
|
724
|
+
pdef.BottomMargin = hwp.MiliToHwpUnit(params["bottom_margin"])
|
|
725
|
+
if "left_margin" in params:
|
|
726
|
+
pdef.LeftMargin = hwp.MiliToHwpUnit(params["left_margin"])
|
|
727
|
+
if "right_margin" in params:
|
|
728
|
+
pdef.RightMargin = hwp.MiliToHwpUnit(params["right_margin"])
|
|
729
|
+
if "header_margin" in params:
|
|
730
|
+
pdef.HeaderLen = hwp.MiliToHwpUnit(params["header_margin"])
|
|
731
|
+
if "footer_margin" in params:
|
|
732
|
+
pdef.FooterLen = hwp.MiliToHwpUnit(params["footer_margin"])
|
|
733
|
+
if "orientation" in params:
|
|
734
|
+
pdef.Landscape = 1 if params["orientation"] == "landscape" else 0
|
|
735
|
+
if "paper_width" in params:
|
|
736
|
+
pdef.PaperWidth = hwp.MiliToHwpUnit(params["paper_width"])
|
|
737
|
+
if "paper_height" in params:
|
|
738
|
+
pdef.PaperHeight = hwp.MiliToHwpUnit(params["paper_height"])
|
|
739
|
+
act.Execute("PageSetup", pset.HSet)
|
|
740
|
+
return {"status": "ok"}
|
|
741
|
+
except Exception as e:
|
|
742
|
+
return {"status": "error", "error": f"페이지 설정 실패: {e}"}
|
|
743
|
+
|
|
744
|
+
if method == "set_cell_property":
|
|
745
|
+
# 셀 속성 설정 (여백, 텍스트 방향, 수직 정렬, 보호)
|
|
746
|
+
validate_params(params, ["table_index", "tab"], method)
|
|
747
|
+
try:
|
|
748
|
+
from hwp_editor import _navigate_to_tab
|
|
749
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
750
|
+
_navigate_to_tab(hwp, params["table_index"], params["tab"], 0)
|
|
751
|
+
pset = hwp.HParameterSet.HCell
|
|
752
|
+
hwp.HAction.GetDefault("CellShape", pset.HSet)
|
|
753
|
+
if "vert_align" in params:
|
|
754
|
+
va_map = {"top": 0, "middle": 1, "bottom": 2}
|
|
755
|
+
pset.VertAlign = va_map.get(params["vert_align"], 0)
|
|
756
|
+
if "margin_left" in params:
|
|
757
|
+
pset.MarginLeft = hwp.MiliToHwpUnit(params["margin_left"])
|
|
758
|
+
if "margin_right" in params:
|
|
759
|
+
pset.MarginRight = hwp.MiliToHwpUnit(params["margin_right"])
|
|
760
|
+
if "margin_top" in params:
|
|
761
|
+
pset.MarginTop = hwp.MiliToHwpUnit(params["margin_top"])
|
|
762
|
+
if "margin_bottom" in params:
|
|
763
|
+
pset.MarginBottom = hwp.MiliToHwpUnit(params["margin_bottom"])
|
|
764
|
+
if "text_direction" in params:
|
|
765
|
+
pset.TextDirection = int(params["text_direction"]) # 0=가로, 1=세로
|
|
766
|
+
if "protected" in params:
|
|
767
|
+
pset.Protected = 1 if params["protected"] else 0
|
|
768
|
+
hwp.HAction.Execute("CellShape", pset.HSet)
|
|
769
|
+
return {"status": "ok", "tab": params["tab"]}
|
|
770
|
+
except Exception as e:
|
|
771
|
+
raise RuntimeError(f"셀 속성 설정 실패: {e}")
|
|
772
|
+
finally:
|
|
773
|
+
try:
|
|
774
|
+
hwp.Cancel()
|
|
775
|
+
except Exception:
|
|
776
|
+
pass
|
|
777
|
+
|
|
778
|
+
if method == "insert_textbox":
|
|
779
|
+
# 글상자 생성 (위치/크기 지정)
|
|
780
|
+
x = params.get("x", 0) # mm
|
|
781
|
+
y = params.get("y", 0) # mm
|
|
782
|
+
width = params.get("width", 60) # mm
|
|
783
|
+
height = params.get("height", 30) # mm
|
|
784
|
+
text = params.get("text", "")
|
|
785
|
+
border = params.get("border", True)
|
|
786
|
+
try:
|
|
787
|
+
act = hwp.HAction
|
|
788
|
+
# CreateAction으로 글상자 생성
|
|
789
|
+
act_tb = hwp.CreateAction("DrawTextBox")
|
|
790
|
+
ps = act_tb.CreateSet()
|
|
791
|
+
act_tb.GetDefault(ps)
|
|
792
|
+
# ShapeObject로 위치/크기 설정
|
|
793
|
+
so = ps.Item("ShapeObject")
|
|
794
|
+
so.HorzRelTo = 0 # 페이지 기준
|
|
795
|
+
so.VertRelTo = 0 # 페이지 기준
|
|
796
|
+
so.HorzOffset = hwp.MiliToHwpUnit(x)
|
|
797
|
+
so.VertOffset = hwp.MiliToHwpUnit(y)
|
|
798
|
+
so.Width = hwp.MiliToHwpUnit(width)
|
|
799
|
+
so.Height = hwp.MiliToHwpUnit(height)
|
|
800
|
+
act_tb.Execute(ps)
|
|
801
|
+
# 텍스트 삽입
|
|
802
|
+
if text:
|
|
803
|
+
hwp.insert_text(text)
|
|
804
|
+
# 글상자 밖으로 나가기
|
|
805
|
+
hwp.HAction.Run("Cancel")
|
|
806
|
+
return {"status": "ok", "x": x, "y": y, "width": width, "height": height}
|
|
807
|
+
except Exception as e:
|
|
808
|
+
# 대안: 간단한 글상자 생성 (위치/크기 무시)
|
|
809
|
+
try:
|
|
810
|
+
print(f"[WARN] 글상자 위치/크기 파라미터 무시됨 (fallback): {e}", file=sys.stderr)
|
|
811
|
+
hwp.HAction.Run("DrawTextBox")
|
|
812
|
+
if text:
|
|
813
|
+
hwp.insert_text(text)
|
|
814
|
+
hwp.HAction.Run("Cancel")
|
|
815
|
+
return {"status": "ok", "method": "fallback", "text": text,
|
|
816
|
+
"warning": "위치/크기 파라미터가 적용되지 않았습니다 (fallback 방식)"}
|
|
817
|
+
except Exception as e2:
|
|
818
|
+
raise RuntimeError(f"글상자 생성 실패: {e} / {e2}")
|
|
819
|
+
|
|
820
|
+
if method == "draw_line":
|
|
821
|
+
# 선 그리기 (두께/색상/스타일)
|
|
822
|
+
try:
|
|
823
|
+
act = hwp.HAction
|
|
824
|
+
pset = hwp.HParameterSet.HDrawLineAttr
|
|
825
|
+
act.GetDefault("DrawLine", pset.HSet)
|
|
826
|
+
if "width" in params:
|
|
827
|
+
pset.Width = int(params["width"]) # 선 두께
|
|
828
|
+
if "color" in params:
|
|
829
|
+
c = params["color"]
|
|
830
|
+
if isinstance(c, str): # "#RRGGBB"
|
|
831
|
+
r, g, b = int(c[1:3], 16), int(c[3:5], 16), int(c[5:7], 16)
|
|
832
|
+
pset.Color = hwp.RGBColor(r, g, b)
|
|
833
|
+
elif isinstance(c, list):
|
|
834
|
+
pset.Color = hwp.RGBColor(c[0], c[1], c[2])
|
|
835
|
+
if "style" in params:
|
|
836
|
+
pset.style = int(params["style"]) # 0=실선, 1=파선, 2=점선 등
|
|
837
|
+
act.Execute("DrawLine", pset.HSet)
|
|
838
|
+
return {"status": "ok"}
|
|
839
|
+
except Exception as e:
|
|
840
|
+
raise RuntimeError(f"선 그리기 실패: {e}")
|
|
841
|
+
|
|
842
|
+
if method == "set_header_footer":
|
|
843
|
+
# 머리글/바닥글 설정 (CreateAction 방식)
|
|
844
|
+
hf_type = params.get("type", "header") # "header" or "footer"
|
|
845
|
+
text = params.get("text", "")
|
|
846
|
+
try:
|
|
847
|
+
act = hwp.CreateAction("HeaderFooter")
|
|
848
|
+
ps = act.CreateSet()
|
|
849
|
+
act.GetDefault(ps)
|
|
850
|
+
# Type: 0=머리글, 1=바닥글
|
|
851
|
+
ps.SetItem("Type", 0 if hf_type == "header" else 1)
|
|
852
|
+
result = act.Execute(ps)
|
|
853
|
+
if not result:
|
|
854
|
+
raise RuntimeError("HeaderFooter Execute 실패")
|
|
855
|
+
# 머리글/바닥글 편집 모드 진입됨 — 텍스트 삽입
|
|
856
|
+
if text:
|
|
857
|
+
hwp.insert_text(text)
|
|
858
|
+
# 본문으로 복귀
|
|
859
|
+
hwp.HAction.Run("CloseEx")
|
|
860
|
+
return {"status": "ok", "type": hf_type, "text": text}
|
|
861
|
+
except Exception as e:
|
|
862
|
+
# 편집 모드에 들어갔을 수 있으므로 복귀 시도
|
|
863
|
+
try:
|
|
864
|
+
hwp.HAction.Run("CloseEx")
|
|
865
|
+
except Exception as ex:
|
|
866
|
+
print(f"[WARN] CloseEx recovery failed: {ex}", file=sys.stderr)
|
|
867
|
+
raise RuntimeError(f"머리글/바닥글 설정 실패: {e}")
|
|
868
|
+
|
|
869
|
+
if method == "apply_style":
|
|
870
|
+
# 스타일 적용 ("제목1", "본문", "개요1" 등)
|
|
871
|
+
style_name = params.get("style_name", "본문")
|
|
872
|
+
try:
|
|
873
|
+
# CharShape/ParaShape를 스타일 기반으로 변경
|
|
874
|
+
# pyhwpx의 set_style 또는 HAction 기반
|
|
875
|
+
act = hwp.HAction
|
|
876
|
+
pset = hwp.HParameterSet.HStyle
|
|
877
|
+
act.GetDefault("Style", pset.HSet)
|
|
878
|
+
pset.HSet.SetItem("StyleName", style_name)
|
|
879
|
+
act.Execute("Style", pset.HSet)
|
|
880
|
+
return {"status": "ok", "style": style_name}
|
|
881
|
+
except Exception as e:
|
|
882
|
+
raise RuntimeError(f"스타일 적용 실패: {e}")
|
|
883
|
+
|
|
884
|
+
if method == "set_column":
|
|
885
|
+
# 다단 설정
|
|
886
|
+
count = params.get("count", 2) # 단 수
|
|
887
|
+
gap = params.get("gap", 10) # 단 간격 (mm)
|
|
888
|
+
line_type = params.get("line_type", 0) # 구분선 종류
|
|
889
|
+
try:
|
|
890
|
+
act = hwp.HAction
|
|
891
|
+
pset = hwp.HParameterSet.HColDef
|
|
892
|
+
act.GetDefault("MultiColumn", pset.HSet)
|
|
893
|
+
pset.Count = int(count)
|
|
894
|
+
pset.SameSize = 1 # 같은 너비
|
|
895
|
+
pset.SameGap = hwp.MiliToHwpUnit(gap)
|
|
896
|
+
pset.LineType = int(line_type)
|
|
897
|
+
pset.type = 1 # 일반 다단
|
|
898
|
+
act.Execute("MultiColumn", pset.HSet)
|
|
899
|
+
return {"status": "ok", "count": count, "gap": gap}
|
|
900
|
+
except Exception as e:
|
|
901
|
+
raise RuntimeError(f"다단 설정 실패: {e}")
|
|
902
|
+
|
|
903
|
+
if method == "insert_caption":
|
|
904
|
+
# 캡션 삽입 (표/그림 제목)
|
|
905
|
+
text = params.get("text", "")
|
|
906
|
+
side = params.get("side", 3) # 0=왼쪽, 1=오른쪽, 2=위, 3=아래
|
|
907
|
+
try:
|
|
908
|
+
act = hwp.HAction
|
|
909
|
+
pset = hwp.HParameterSet.HCaption
|
|
910
|
+
act.GetDefault("InsertCaption", pset.HSet)
|
|
911
|
+
pset.Side = int(side)
|
|
912
|
+
act.Execute("InsertCaption", pset.HSet)
|
|
913
|
+
if text:
|
|
914
|
+
hwp.insert_text(text)
|
|
915
|
+
return {"status": "ok", "text": text, "side": side}
|
|
916
|
+
except Exception as e:
|
|
917
|
+
raise RuntimeError(f"캡션 삽입 실패: {e}")
|
|
918
|
+
|
|
564
919
|
if method == "insert_hyperlink":
|
|
565
920
|
validate_params(params, ["url"], method)
|
|
566
921
|
url = params["url"]
|
|
@@ -627,24 +982,41 @@ def dispatch(hwp, method, params):
|
|
|
627
982
|
end = min(start + pages_per_split - 1, total_pages)
|
|
628
983
|
part_name = f"part_{start}-{end}{ext}"
|
|
629
984
|
part_path = os.path.join(output_dir, part_name)
|
|
630
|
-
# 분할 저장은 COM API 한계로 전체 복사
|
|
985
|
+
# 분할 저장은 COM API 한계로 전체 복사 (실제 페이지 분할 아님)
|
|
631
986
|
shutil.copy2(src_path, part_path)
|
|
632
987
|
parts.append({"pages": f"{start}-{end}", "path": part_path})
|
|
633
|
-
return {"status": "ok", "total_pages": total_pages, "parts": len(parts), "files": parts
|
|
988
|
+
return {"status": "ok", "total_pages": total_pages, "parts": len(parts), "files": parts,
|
|
989
|
+
"warning": "COM API 한계로 각 파일은 전체 문서의 복사본입니다. 실제 페이지 분할은 한글 프로그램에서 수동으로 진행해주세요."}
|
|
634
990
|
|
|
635
991
|
if method == "insert_footnote":
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
992
|
+
try:
|
|
993
|
+
hwp.HAction.Run("InsertFootnote")
|
|
994
|
+
text = params.get("text")
|
|
995
|
+
if text:
|
|
996
|
+
hwp.insert_text(text)
|
|
997
|
+
hwp.HAction.Run("CloseEx")
|
|
998
|
+
return {"status": "ok", "type": "footnote"}
|
|
999
|
+
except Exception as e:
|
|
1000
|
+
try:
|
|
1001
|
+
hwp.HAction.Run("CloseEx")
|
|
1002
|
+
except Exception:
|
|
1003
|
+
pass
|
|
1004
|
+
raise RuntimeError(f"각주 삽입 실패: {e}")
|
|
641
1005
|
|
|
642
1006
|
if method == "insert_endnote":
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1007
|
+
try:
|
|
1008
|
+
hwp.HAction.Run("InsertEndnote")
|
|
1009
|
+
text = params.get("text")
|
|
1010
|
+
if text:
|
|
1011
|
+
hwp.insert_text(text)
|
|
1012
|
+
hwp.HAction.Run("CloseEx")
|
|
1013
|
+
return {"status": "ok", "type": "endnote"}
|
|
1014
|
+
except Exception as e:
|
|
1015
|
+
try:
|
|
1016
|
+
hwp.HAction.Run("CloseEx")
|
|
1017
|
+
except Exception:
|
|
1018
|
+
pass
|
|
1019
|
+
raise RuntimeError(f"미주 삽입 실패: {e}")
|
|
648
1020
|
|
|
649
1021
|
if method == "insert_page_num":
|
|
650
1022
|
fmt = params.get("format", "plain") # "plain"|"dash"|"paren"
|
|
@@ -821,8 +1193,15 @@ def dispatch(hwp, method, params):
|
|
|
821
1193
|
return {"status": "ok", "type": "column"}
|
|
822
1194
|
|
|
823
1195
|
if method == "insert_line":
|
|
824
|
-
|
|
825
|
-
|
|
1196
|
+
# draw_line과 동일하게 처리 (대화상자 방지)
|
|
1197
|
+
try:
|
|
1198
|
+
act = hwp.HAction
|
|
1199
|
+
pset = hwp.HParameterSet.HDrawLineAttr
|
|
1200
|
+
act.GetDefault("DrawLine", pset.HSet)
|
|
1201
|
+
act.Execute("DrawLine", pset.HSet)
|
|
1202
|
+
return {"status": "ok"}
|
|
1203
|
+
except Exception as e:
|
|
1204
|
+
raise RuntimeError(f"선 삽입 실패: {e}")
|
|
826
1205
|
|
|
827
1206
|
if method == "table_swap_type":
|
|
828
1207
|
validate_params(params, ["table_index"], method)
|
|
@@ -1280,11 +1659,19 @@ def main():
|
|
|
1280
1659
|
if hwp is None:
|
|
1281
1660
|
from pyhwpx import Hwp
|
|
1282
1661
|
hwp = Hwp()
|
|
1283
|
-
#
|
|
1662
|
+
# 모든 대화상자 자동 수락 — COM 무한 대기 방지
|
|
1284
1663
|
try:
|
|
1285
1664
|
hwp.XHwpMessageBoxMode = 1 # 0=표시, 1=자동OK
|
|
1286
1665
|
except Exception:
|
|
1287
1666
|
pass
|
|
1667
|
+
try:
|
|
1668
|
+
hwp.SetMessageBoxMode(0x10000) # 모든 대화상자 자동 OK
|
|
1669
|
+
except Exception:
|
|
1670
|
+
pass
|
|
1671
|
+
try:
|
|
1672
|
+
hwp.RegisterModule('FilePathCheckDLL', 'FilePathCheckerModule')
|
|
1673
|
+
except Exception:
|
|
1674
|
+
pass
|
|
1288
1675
|
|
|
1289
1676
|
result = dispatch(hwp, method, params)
|
|
1290
1677
|
respond(req_id, True, result)
|