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.
@@ -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 공통 함수. BUG-2 fix: COM 반환값 대신 전후 텍스트 비교로 검증."""
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
- return {"context": "cursor context placeholder"}
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
- # BUG-4 fix: Cancel 대신 MoveRight — 찾은 텍스트 끝으로 커서 이동
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, params["text"], style)
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, params["text"], tuple(color))
324
+ insert_text_with_color(hwp, text, tuple(color))
283
325
  else:
284
- hwp.insert_text(params["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(params["table_index"])
429
- hwp.TableMergeCell()
430
- return {"status": "ok", "table_index": params["table_index"]}
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
- hwp.create_table(rows, cols)
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
- if header_style and rows > 0:
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
- hwp.HAction.Run("InsertFootnote")
637
- text = params.get("text")
638
- if text:
639
- hwp.insert_text(text)
640
- return {"status": "ok", "type": "footnote"}
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
- hwp.HAction.Run("InsertEndnote")
644
- text = params.get("text")
645
- if text:
646
- hwp.insert_text(text)
647
- return {"status": "ok", "type": "endnote"}
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
- hwp.HAction.Run("InsertLine")
825
- return {"status": "ok"}
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
- # 메시지박스(얼럿/다이얼로그) 자동 확인 — COM 무한 대기 방지
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)