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.
Files changed (34) hide show
  1. package/README.md +409 -0
  2. package/dist/hwp-bridge.d.ts +67 -0
  3. package/dist/hwp-bridge.js +320 -0
  4. package/dist/hwpx-engine.d.ts +39 -0
  5. package/dist/hwpx-engine.js +187 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +54 -0
  8. package/dist/prompts/hwp-prompts.d.ts +2 -0
  9. package/dist/prompts/hwp-prompts.js +368 -0
  10. package/dist/resources/document-resources.d.ts +3 -0
  11. package/dist/resources/document-resources.js +109 -0
  12. package/dist/server.d.ts +12 -0
  13. package/dist/server.js +29 -0
  14. package/dist/tools/analysis-tools.d.ts +4 -0
  15. package/dist/tools/analysis-tools.js +414 -0
  16. package/dist/tools/composite-tools.d.ts +3 -0
  17. package/dist/tools/composite-tools.js +664 -0
  18. package/dist/tools/document-tools.d.ts +3 -0
  19. package/dist/tools/document-tools.js +264 -0
  20. package/dist/tools/editing-tools.d.ts +4 -0
  21. package/dist/tools/editing-tools.js +916 -0
  22. package/package.json +31 -0
  23. package/python/__pycache__/hwp_analyzer.cpython-313.pyc +0 -0
  24. package/python/__pycache__/hwp_editor.cpython-313.pyc +0 -0
  25. package/python/__pycache__/hwp_service.cpython-313.pyc +0 -0
  26. package/python/__pycache__/privacy_scanner.cpython-313.pyc +0 -0
  27. package/python/__pycache__/ref_reader.cpython-313.pyc +0 -0
  28. package/python/__pycache__/test_integration.cpython-313.pyc +0 -0
  29. package/python/hwp_analyzer.py +544 -0
  30. package/python/hwp_editor.py +933 -0
  31. package/python/hwp_service.py +1291 -0
  32. package/python/privacy_scanner.py +115 -0
  33. package/python/ref_reader.py +115 -0
  34. 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