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,1291 @@
|
|
|
1
|
+
"""HWP Studio AI - Python HWP Service Bridge
|
|
2
|
+
stdin/stdout JSON protocol for Electron <-> Python communication.
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import time
|
|
9
|
+
import pathlib
|
|
10
|
+
|
|
11
|
+
from hwp_analyzer import analyze_document, map_table_cells
|
|
12
|
+
from hwp_editor import (fill_document, fill_table_cells_by_tab, fill_table_cells_by_label,
|
|
13
|
+
set_paragraph_style, get_char_shape, get_para_shape,
|
|
14
|
+
verify_after_fill)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_file_path(file_path, must_exist=True):
|
|
18
|
+
"""경로 보안 검증. 심볼릭 링크 거부, 존재 여부 확인."""
|
|
19
|
+
real = os.path.abspath(file_path)
|
|
20
|
+
if must_exist and not os.path.exists(real):
|
|
21
|
+
raise FileNotFoundError(f"파일을 찾을 수 없습니다: {real}")
|
|
22
|
+
if os.path.islink(file_path):
|
|
23
|
+
raise ValueError(f"심볼릭 링크는 허용되지 않습니다: {file_path}")
|
|
24
|
+
return real
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _execute_all_replace(hwp, find_str, replace_str, use_regex=False):
|
|
28
|
+
"""AllReplace 공통 함수. find_replace/find_replace_multi/generate_multi에서 사용."""
|
|
29
|
+
act = hwp.HAction
|
|
30
|
+
pset = hwp.HParameterSet.HFindReplace
|
|
31
|
+
act.GetDefault("AllReplace", pset.HSet)
|
|
32
|
+
pset.FindString = find_str
|
|
33
|
+
pset.ReplaceString = replace_str
|
|
34
|
+
pset.IgnoreMessage = 1
|
|
35
|
+
pset.Direction = 0
|
|
36
|
+
pset.FindRegExp = 1 if use_regex else 0
|
|
37
|
+
pset.FindJaso = 0
|
|
38
|
+
pset.AllWordForms = 0
|
|
39
|
+
pset.SeveralWords = 0
|
|
40
|
+
return bool(act.Execute("AllReplace", pset.HSet))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def respond(req_id, success, data=None, error=None):
|
|
44
|
+
"""Send JSON response to stdout."""
|
|
45
|
+
response = {"id": req_id, "success": success}
|
|
46
|
+
if data is not None:
|
|
47
|
+
response["data"] = data
|
|
48
|
+
if error is not None:
|
|
49
|
+
response["error"] = error
|
|
50
|
+
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
51
|
+
sys.stdout.flush()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_params(params, required_keys, method_name):
|
|
55
|
+
"""Validate required parameters exist."""
|
|
56
|
+
missing = [k for k in required_keys if k not in params]
|
|
57
|
+
if missing:
|
|
58
|
+
raise ValueError(f"{method_name}: missing required params: {', '.join(missing)}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_current_doc_path = None
|
|
62
|
+
|
|
63
|
+
def dispatch(hwp, method, params):
|
|
64
|
+
"""Route method calls to appropriate handlers."""
|
|
65
|
+
global _current_doc_path
|
|
66
|
+
|
|
67
|
+
if method == "ping":
|
|
68
|
+
return {"status": "ok", "message": "HWP Service is running"}
|
|
69
|
+
|
|
70
|
+
if method == "inspect_com_object":
|
|
71
|
+
obj_name = params.get("object", "HCharShape")
|
|
72
|
+
if obj_name == "HCharShape":
|
|
73
|
+
pset = hwp.HParameterSet.HCharShape
|
|
74
|
+
hwp.HAction.GetDefault("CharShape", pset.HSet)
|
|
75
|
+
elif obj_name == "HParaShape":
|
|
76
|
+
pset = hwp.HParameterSet.HParaShape
|
|
77
|
+
hwp.HAction.GetDefault("ParaShape", pset.HSet)
|
|
78
|
+
elif obj_name == "HFindReplace":
|
|
79
|
+
pset = hwp.HParameterSet.HFindReplace
|
|
80
|
+
hwp.HAction.GetDefault("AllReplace", pset.HSet)
|
|
81
|
+
else:
|
|
82
|
+
return {"error": f"Unknown object: {obj_name}"}
|
|
83
|
+
attrs = [a for a in dir(pset) if not a.startswith('_')]
|
|
84
|
+
return {"object": obj_name, "attributes": attrs, "count": len(attrs)}
|
|
85
|
+
|
|
86
|
+
if method == "open_document":
|
|
87
|
+
validate_params(params, ["file_path"], method)
|
|
88
|
+
file_path = validate_file_path(params["file_path"], must_exist=True)
|
|
89
|
+
|
|
90
|
+
# 원본 백업 (기본 활성, backup=False로 비활성 가능)
|
|
91
|
+
if params.get("backup", True):
|
|
92
|
+
import shutil
|
|
93
|
+
root, ext = os.path.splitext(file_path)
|
|
94
|
+
backup_path = f"{root}_backup{ext}"
|
|
95
|
+
if not os.path.exists(backup_path):
|
|
96
|
+
shutil.copy2(file_path, backup_path)
|
|
97
|
+
|
|
98
|
+
# 파일 열기 전 다이얼로그 자동 처리 재확인
|
|
99
|
+
try:
|
|
100
|
+
hwp.XHwpMessageBoxMode = 1
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
result = hwp.open(file_path)
|
|
105
|
+
if not result:
|
|
106
|
+
raise RuntimeError(f"한글 프로그램에서 파일을 열 수 없습니다: {file_path}")
|
|
107
|
+
_current_doc_path = file_path
|
|
108
|
+
return {"status": "ok", "file_path": file_path, "pages": hwp.PageCount}
|
|
109
|
+
|
|
110
|
+
if method == "get_document_info":
|
|
111
|
+
# 경량 메타데이터만 반환 (analyze_document보다 빠름)
|
|
112
|
+
result = {"status": "ok"}
|
|
113
|
+
try:
|
|
114
|
+
result["pages"] = hwp.PageCount
|
|
115
|
+
except Exception:
|
|
116
|
+
result["pages"] = 0
|
|
117
|
+
try:
|
|
118
|
+
result["current_path"] = _current_doc_path or ""
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
if method == "analyze_document":
|
|
124
|
+
validate_params(params, ["file_path"], method)
|
|
125
|
+
file_path = os.path.abspath(params["file_path"])
|
|
126
|
+
return analyze_document(hwp, file_path, already_open=(file_path == _current_doc_path))
|
|
127
|
+
|
|
128
|
+
if method == "fill_document":
|
|
129
|
+
return fill_document(hwp, params)
|
|
130
|
+
|
|
131
|
+
if method == "fill_by_tab":
|
|
132
|
+
validate_params(params, ["table_index", "cells"], method)
|
|
133
|
+
return fill_table_cells_by_tab(hwp, params["table_index"], params["cells"])
|
|
134
|
+
|
|
135
|
+
if method == "fill_by_label":
|
|
136
|
+
validate_params(params, ["table_index", "cells"], method)
|
|
137
|
+
return fill_table_cells_by_label(hwp, params["table_index"], params["cells"])
|
|
138
|
+
|
|
139
|
+
if method == "map_table_cells":
|
|
140
|
+
validate_params(params, ["table_index"], method)
|
|
141
|
+
return map_table_cells(hwp, params["table_index"])
|
|
142
|
+
|
|
143
|
+
if method == "get_selected_text":
|
|
144
|
+
text = hwp.get_selected_text()
|
|
145
|
+
return {"text": text}
|
|
146
|
+
|
|
147
|
+
if method == "get_cursor_context":
|
|
148
|
+
return {"context": "cursor context placeholder"}
|
|
149
|
+
|
|
150
|
+
if method == "save_as":
|
|
151
|
+
validate_params(params, ["path"], method)
|
|
152
|
+
save_path = validate_file_path(params["path"], must_exist=False)
|
|
153
|
+
fmt = params.get("format", "HWP").upper() # pyhwpx는 대문자 포맷 필요 (HWP, HWPX, PDF 등)
|
|
154
|
+
hwp.save_as(save_path, fmt)
|
|
155
|
+
# 파일 실제 생성 확인
|
|
156
|
+
if not os.path.exists(save_path):
|
|
157
|
+
# 대안: 임시 디렉토리에 저장 후 이동
|
|
158
|
+
import tempfile, shutil as _shutil
|
|
159
|
+
temp_path = os.path.join(tempfile.gettempdir(), os.path.basename(save_path))
|
|
160
|
+
hwp.save_as(temp_path, fmt)
|
|
161
|
+
if os.path.exists(temp_path):
|
|
162
|
+
_shutil.move(temp_path, save_path)
|
|
163
|
+
exists = os.path.exists(save_path)
|
|
164
|
+
file_size = os.path.getsize(save_path) if exists else 0
|
|
165
|
+
if not exists:
|
|
166
|
+
raise RuntimeError(f"저장 실패: 파일이 생성되지 않았습니다. 경로: {save_path}")
|
|
167
|
+
return {"status": "ok", "path": save_path, "file_size": file_size}
|
|
168
|
+
|
|
169
|
+
if method == "close_document":
|
|
170
|
+
hwp.close()
|
|
171
|
+
_current_doc_path = None
|
|
172
|
+
return {"status": "ok"}
|
|
173
|
+
|
|
174
|
+
if method == "text_search":
|
|
175
|
+
validate_params(params, ["search"], method)
|
|
176
|
+
search_text = params["search"]
|
|
177
|
+
max_results = min(max(params.get("max_results", 50), 1), 1000)
|
|
178
|
+
hwp.MovePos(2) # 문서 시작
|
|
179
|
+
results = []
|
|
180
|
+
for i in range(max_results):
|
|
181
|
+
act = hwp.HAction
|
|
182
|
+
pset = hwp.HParameterSet.HFindReplace
|
|
183
|
+
act.GetDefault("FindReplace", pset.HSet)
|
|
184
|
+
pset.FindString = search_text
|
|
185
|
+
pset.Direction = 0
|
|
186
|
+
pset.IgnoreMessage = 1
|
|
187
|
+
found = act.Execute("FindReplace", pset.HSet)
|
|
188
|
+
if not found:
|
|
189
|
+
break
|
|
190
|
+
# 찾은 위치에서 컨텍스트 추출
|
|
191
|
+
context = ""
|
|
192
|
+
try:
|
|
193
|
+
context = hwp.GetTextFile("TEXT", "saveblock").strip()[:200]
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
hwp.HAction.Run("Cancel")
|
|
197
|
+
results.append({
|
|
198
|
+
"index": i + 1,
|
|
199
|
+
"matched_text": context[:50] if context else search_text,
|
|
200
|
+
})
|
|
201
|
+
return {
|
|
202
|
+
"search": search_text,
|
|
203
|
+
"total_found": len(results),
|
|
204
|
+
"results": results,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if method == "find_replace":
|
|
208
|
+
validate_params(params, ["find", "replace"], method)
|
|
209
|
+
use_regex = params.get("use_regex", False)
|
|
210
|
+
replaced = _execute_all_replace(hwp, params["find"], params["replace"], use_regex)
|
|
211
|
+
return {"status": "ok", "find": params["find"], "replace": params["replace"], "replaced": replaced}
|
|
212
|
+
|
|
213
|
+
if method == "find_replace_multi":
|
|
214
|
+
validate_params(params, ["replacements"], method)
|
|
215
|
+
use_regex = params.get("use_regex", False)
|
|
216
|
+
results = []
|
|
217
|
+
hwp.MovePos(2) # 문서 시작으로 이동
|
|
218
|
+
for item in params["replacements"]:
|
|
219
|
+
replaced = _execute_all_replace(hwp, item["find"], item["replace"], use_regex)
|
|
220
|
+
results.append({"find": item["find"], "replaced": replaced})
|
|
221
|
+
return {"status": "ok", "results": results, "total": len(results),
|
|
222
|
+
"success": sum(1 for r in results if r["replaced"])}
|
|
223
|
+
|
|
224
|
+
if method == "find_and_append":
|
|
225
|
+
validate_params(params, ["find", "append_text"], method)
|
|
226
|
+
act = hwp.HAction
|
|
227
|
+
pset = hwp.HParameterSet.HFindReplace
|
|
228
|
+
act.GetDefault("FindReplace", pset.HSet)
|
|
229
|
+
pset.FindString = params["find"]
|
|
230
|
+
pset.Direction = 0
|
|
231
|
+
pset.IgnoreMessage = 1
|
|
232
|
+
found = act.Execute("FindReplace", pset.HSet)
|
|
233
|
+
|
|
234
|
+
if not found:
|
|
235
|
+
return {"status": "not_found", "find": params["find"]}
|
|
236
|
+
|
|
237
|
+
# 찾은 텍스트 끝으로 커서 이동 (선택 해제)
|
|
238
|
+
hwp.HAction.Run("Cancel")
|
|
239
|
+
|
|
240
|
+
# 색상 설정 (옵션)
|
|
241
|
+
color = params.get("color") # [r, g, b]
|
|
242
|
+
if color:
|
|
243
|
+
from hwp_editor import insert_text_with_color
|
|
244
|
+
insert_text_with_color(hwp, params["append_text"], tuple(color))
|
|
245
|
+
else:
|
|
246
|
+
hwp.insert_text(params["append_text"])
|
|
247
|
+
|
|
248
|
+
return {"status": "ok", "find": params["find"], "appended": True}
|
|
249
|
+
|
|
250
|
+
if method == "insert_text":
|
|
251
|
+
validate_params(params, ["text"], method)
|
|
252
|
+
style = params.get("style")
|
|
253
|
+
color = params.get("color") # [r, g, b] 하위 호환
|
|
254
|
+
if style:
|
|
255
|
+
from hwp_editor import insert_text_with_style
|
|
256
|
+
insert_text_with_style(hwp, params["text"], style)
|
|
257
|
+
elif color:
|
|
258
|
+
from hwp_editor import insert_text_with_color
|
|
259
|
+
insert_text_with_color(hwp, params["text"], tuple(color))
|
|
260
|
+
else:
|
|
261
|
+
hwp.insert_text(params["text"])
|
|
262
|
+
return {"status": "ok"}
|
|
263
|
+
|
|
264
|
+
if method == "set_paragraph_style":
|
|
265
|
+
validate_params(params, ["style"], method)
|
|
266
|
+
set_paragraph_style(hwp, params["style"])
|
|
267
|
+
return {"status": "ok"}
|
|
268
|
+
|
|
269
|
+
if method == "get_char_shape":
|
|
270
|
+
return get_char_shape(hwp)
|
|
271
|
+
|
|
272
|
+
if method == "get_para_shape":
|
|
273
|
+
return get_para_shape(hwp)
|
|
274
|
+
|
|
275
|
+
if method == "get_cell_format":
|
|
276
|
+
validate_params(params, ["table_index", "cell_tab"], method)
|
|
277
|
+
from hwp_editor import get_cell_format
|
|
278
|
+
return get_cell_format(hwp, params["table_index"], params["cell_tab"])
|
|
279
|
+
|
|
280
|
+
if method == "get_table_format_summary":
|
|
281
|
+
validate_params(params, ["table_index"], method)
|
|
282
|
+
from hwp_editor import get_table_format_summary
|
|
283
|
+
return get_table_format_summary(
|
|
284
|
+
hwp, params["table_index"], params.get("sample_tabs"))
|
|
285
|
+
|
|
286
|
+
if method == "smart_fill":
|
|
287
|
+
validate_params(params, ["table_index", "cells"], method)
|
|
288
|
+
from hwp_editor import smart_fill_table_cells
|
|
289
|
+
return smart_fill_table_cells(hwp, params["table_index"], params["cells"])
|
|
290
|
+
|
|
291
|
+
if method == "read_reference":
|
|
292
|
+
validate_params(params, ["file_path"], method)
|
|
293
|
+
from ref_reader import read_reference
|
|
294
|
+
return read_reference(params["file_path"], params.get("max_chars", 30000))
|
|
295
|
+
|
|
296
|
+
if method == "find_replace_nth":
|
|
297
|
+
validate_params(params, ["find", "replace", "nth"], method)
|
|
298
|
+
nth = params["nth"] # 1-based
|
|
299
|
+
if nth < 1 or nth > 10000:
|
|
300
|
+
raise ValueError("nth must be between 1 and 10000")
|
|
301
|
+
hwp.MovePos(2) # 문서 시작
|
|
302
|
+
for i in range(nth):
|
|
303
|
+
act = hwp.HAction
|
|
304
|
+
pset = hwp.HParameterSet.HFindReplace
|
|
305
|
+
act.GetDefault("FindReplace", pset.HSet)
|
|
306
|
+
pset.FindString = params["find"]
|
|
307
|
+
pset.Direction = 0
|
|
308
|
+
pset.IgnoreMessage = 1
|
|
309
|
+
found = act.Execute("FindReplace", pset.HSet)
|
|
310
|
+
if not found:
|
|
311
|
+
return {"status": "not_found", "find": params["find"], "searched": i, "nth": nth}
|
|
312
|
+
# N번째 매칭이 선택된 상태 → 텍스트 교체
|
|
313
|
+
hwp.insert_text(params["replace"])
|
|
314
|
+
return {"status": "ok", "find": params["find"], "replace": params["replace"], "nth": nth}
|
|
315
|
+
|
|
316
|
+
if method == "table_add_row":
|
|
317
|
+
validate_params(params, ["table_index"], method)
|
|
318
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
319
|
+
# 마지막 셀로 이동 후 행 추가
|
|
320
|
+
try:
|
|
321
|
+
hwp.HAction.Run("TableAppendRow")
|
|
322
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
323
|
+
except Exception:
|
|
324
|
+
# 대안: InsertRowBelow
|
|
325
|
+
try:
|
|
326
|
+
hwp.HAction.Run("InsertRowBelow")
|
|
327
|
+
return {"status": "ok", "table_index": params["table_index"], "method": "InsertRowBelow"}
|
|
328
|
+
except Exception as e:
|
|
329
|
+
raise RuntimeError(f"표 행 추가 실패: {e}")
|
|
330
|
+
finally:
|
|
331
|
+
try:
|
|
332
|
+
hwp.Cancel()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
if method == "document_merge":
|
|
337
|
+
validate_params(params, ["file_path"], method)
|
|
338
|
+
merge_path = validate_file_path(params["file_path"], must_exist=True)
|
|
339
|
+
hwp.MovePos(3) # 문서 끝으로 이동
|
|
340
|
+
# BreakSection으로 페이지 분리 후 파일 삽입
|
|
341
|
+
try:
|
|
342
|
+
hwp.HAction.Run("BreakSection")
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
hwp.insert_file(merge_path)
|
|
346
|
+
return {"status": "ok", "merged_file": merge_path, "pages": hwp.PageCount}
|
|
347
|
+
|
|
348
|
+
if method == "insert_page_break":
|
|
349
|
+
try:
|
|
350
|
+
hwp.HAction.Run("BreakPage")
|
|
351
|
+
return {"status": "ok"}
|
|
352
|
+
except Exception as e:
|
|
353
|
+
raise RuntimeError(f"페이지 나누기 실패: {e}")
|
|
354
|
+
|
|
355
|
+
if method == "insert_markdown":
|
|
356
|
+
validate_params(params, ["text"], method)
|
|
357
|
+
from hwp_editor import insert_markdown
|
|
358
|
+
return insert_markdown(hwp, params["text"])
|
|
359
|
+
|
|
360
|
+
if method == "table_delete_row":
|
|
361
|
+
validate_params(params, ["table_index"], method)
|
|
362
|
+
try:
|
|
363
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
364
|
+
hwp.TableSubtractRow()
|
|
365
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
366
|
+
except Exception as e:
|
|
367
|
+
raise RuntimeError(f"표 행 삭제 실패: {e}")
|
|
368
|
+
finally:
|
|
369
|
+
try:
|
|
370
|
+
hwp.Cancel()
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
if method == "table_add_column":
|
|
375
|
+
validate_params(params, ["table_index"], method)
|
|
376
|
+
try:
|
|
377
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
378
|
+
hwp.HAction.Run("InsertColumnRight")
|
|
379
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
380
|
+
except Exception as e:
|
|
381
|
+
raise RuntimeError(f"표 열 추가 실패: {e}")
|
|
382
|
+
finally:
|
|
383
|
+
try:
|
|
384
|
+
hwp.Cancel()
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
if method == "table_delete_column":
|
|
389
|
+
validate_params(params, ["table_index"], method)
|
|
390
|
+
try:
|
|
391
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
392
|
+
hwp.HAction.Run("DeleteColumn")
|
|
393
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
394
|
+
except Exception as e:
|
|
395
|
+
raise RuntimeError(f"표 열 삭제 실패: {e}")
|
|
396
|
+
finally:
|
|
397
|
+
try:
|
|
398
|
+
hwp.Cancel()
|
|
399
|
+
except Exception:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
if method == "table_merge_cells":
|
|
403
|
+
validate_params(params, ["table_index"], method)
|
|
404
|
+
try:
|
|
405
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
406
|
+
hwp.TableMergeCell()
|
|
407
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
408
|
+
except Exception as e:
|
|
409
|
+
raise RuntimeError(f"셀 병합 실패: {e}")
|
|
410
|
+
finally:
|
|
411
|
+
try:
|
|
412
|
+
hwp.Cancel()
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
if method == "table_split_cell":
|
|
417
|
+
validate_params(params, ["table_index"], method)
|
|
418
|
+
try:
|
|
419
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
420
|
+
hwp.TableSplitCell()
|
|
421
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
422
|
+
except Exception as e:
|
|
423
|
+
raise RuntimeError(f"셀 분할 실패: {e}")
|
|
424
|
+
finally:
|
|
425
|
+
try:
|
|
426
|
+
hwp.Cancel()
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
if method == "table_create_from_data":
|
|
431
|
+
validate_params(params, ["data"], method)
|
|
432
|
+
data = params["data"] # 2D 배열 [[row1], [row2], ...]
|
|
433
|
+
if not data or not isinstance(data, list):
|
|
434
|
+
raise ValueError("data must be a non-empty 2D array")
|
|
435
|
+
rows = len(data)
|
|
436
|
+
cols = max(len(row) for row in data) if data else 0
|
|
437
|
+
header_style = params.get("header_style", False) # 헤더 자동 스타일링
|
|
438
|
+
hwp.create_table(rows, cols)
|
|
439
|
+
# 셀 채우기
|
|
440
|
+
filled = 0
|
|
441
|
+
for r, row in enumerate(data):
|
|
442
|
+
for c, val in enumerate(row):
|
|
443
|
+
if val:
|
|
444
|
+
hwp.HAction.Run("SelectAll")
|
|
445
|
+
if header_style and r == 0:
|
|
446
|
+
# 헤더행: Bold + 가운데 정렬
|
|
447
|
+
from hwp_editor import insert_text_with_style
|
|
448
|
+
insert_text_with_style(hwp, str(val), {"bold": True})
|
|
449
|
+
else:
|
|
450
|
+
hwp.insert_text(str(val))
|
|
451
|
+
filled += 1
|
|
452
|
+
if c < len(row) - 1 or r < rows - 1:
|
|
453
|
+
hwp.TableRightCell()
|
|
454
|
+
try:
|
|
455
|
+
hwp.Cancel()
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
# 헤더행 배경색 적용 (옵션)
|
|
459
|
+
if header_style and rows > 0:
|
|
460
|
+
try:
|
|
461
|
+
from hwp_editor import set_cell_background_color
|
|
462
|
+
header_cells = [{"tab": i, "color": "#E8E8E8"} for i in range(cols)]
|
|
463
|
+
set_cell_background_color(hwp, -1, header_cells) # -1 = 현재 표
|
|
464
|
+
except Exception:
|
|
465
|
+
pass # 배경색 실패해도 표 자체는 유지
|
|
466
|
+
return {"status": "ok", "rows": rows, "cols": cols, "filled": filled, "header_styled": bool(header_style)}
|
|
467
|
+
|
|
468
|
+
if method == "table_insert_from_csv":
|
|
469
|
+
validate_params(params, ["file_path"], method)
|
|
470
|
+
csv_path = validate_file_path(params["file_path"], must_exist=True)
|
|
471
|
+
from ref_reader import read_reference
|
|
472
|
+
ref = read_reference(csv_path)
|
|
473
|
+
if ref.get("format") not in ("csv", "excel"):
|
|
474
|
+
raise ValueError(f"CSV 또는 Excel 파일만 지원합니다. (현재: {ref.get('format')})")
|
|
475
|
+
# 헤더 + 데이터를 2D 배열로 병합
|
|
476
|
+
headers = ref.get("headers", [])
|
|
477
|
+
data_rows = ref.get("data", [])
|
|
478
|
+
if ref.get("format") == "excel":
|
|
479
|
+
sheets = ref.get("sheets", [])
|
|
480
|
+
if sheets:
|
|
481
|
+
headers = sheets[0].get("headers", [])
|
|
482
|
+
data_rows = sheets[0].get("data", [])
|
|
483
|
+
all_data = [headers] + data_rows if headers else data_rows
|
|
484
|
+
if not all_data:
|
|
485
|
+
raise ValueError("CSV 파일에 데이터가 없습니다.")
|
|
486
|
+
rows = len(all_data)
|
|
487
|
+
cols = max(len(row) for row in all_data)
|
|
488
|
+
hwp.create_table(rows, cols)
|
|
489
|
+
filled = 0
|
|
490
|
+
for r, row in enumerate(all_data):
|
|
491
|
+
for c, val in enumerate(row):
|
|
492
|
+
if val:
|
|
493
|
+
hwp.HAction.Run("SelectAll")
|
|
494
|
+
hwp.insert_text(str(val))
|
|
495
|
+
filled += 1
|
|
496
|
+
if c < len(row) - 1 or r < rows - 1:
|
|
497
|
+
hwp.TableRightCell()
|
|
498
|
+
try:
|
|
499
|
+
hwp.Cancel()
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
return {"status": "ok", "file": os.path.basename(csv_path), "rows": rows, "cols": cols, "filled": filled}
|
|
503
|
+
|
|
504
|
+
if method == "insert_heading":
|
|
505
|
+
validate_params(params, ["text", "level"], method)
|
|
506
|
+
from hwp_editor import insert_text_with_style
|
|
507
|
+
level = min(max(params["level"], 1), 6)
|
|
508
|
+
sizes = {1: 22, 2: 18, 3: 15, 4: 13, 5: 11, 6: 10}
|
|
509
|
+
text = params["text"]
|
|
510
|
+
# 순번 자동 생성
|
|
511
|
+
numbering = params.get("numbering")
|
|
512
|
+
number = params.get("number", 1)
|
|
513
|
+
if numbering:
|
|
514
|
+
roman = ["Ⅰ","Ⅱ","Ⅲ","Ⅳ","Ⅴ","Ⅵ","Ⅶ","Ⅷ","Ⅸ","Ⅹ"]
|
|
515
|
+
korean = ["가","나","다","라","마","바","사","아","자","차"]
|
|
516
|
+
circle = ["①","②","③","④","⑤","⑥","⑦","⑧","⑨","⑩"]
|
|
517
|
+
idx = max(0, min(number - 1, 9))
|
|
518
|
+
if numbering == "roman": text = f"{roman[idx]}. {text}"
|
|
519
|
+
elif numbering == "decimal": text = f"{number}. {text}"
|
|
520
|
+
elif numbering == "korean": text = f"{korean[idx]}. {text}"
|
|
521
|
+
elif numbering == "circle": text = f"{circle[idx]} {text}"
|
|
522
|
+
elif numbering == "paren_decimal": text = f"{number}) {text}"
|
|
523
|
+
elif numbering == "paren_korean": text = f"{korean[idx]}) {text}"
|
|
524
|
+
insert_text_with_style(hwp, text + "\r\n", {
|
|
525
|
+
"bold": True,
|
|
526
|
+
"font_size": sizes.get(level, 11),
|
|
527
|
+
})
|
|
528
|
+
return {"status": "ok", "level": level, "text": text}
|
|
529
|
+
|
|
530
|
+
if method == "export_format":
|
|
531
|
+
validate_params(params, ["path", "format"], method)
|
|
532
|
+
save_path = validate_file_path(params["path"], must_exist=False)
|
|
533
|
+
fmt = params["format"].upper() # HWP, HWPX, PDF, HTML, TXT 등
|
|
534
|
+
result = hwp.save_as(save_path, fmt)
|
|
535
|
+
# 파일 실제 생성 확인
|
|
536
|
+
file_exists = os.path.exists(save_path)
|
|
537
|
+
file_size = os.path.getsize(save_path) if file_exists else 0
|
|
538
|
+
return {"status": "ok" if file_exists else "warning",
|
|
539
|
+
"path": save_path, "format": fmt,
|
|
540
|
+
"success": bool(result), "file_exists": file_exists, "file_size": file_size}
|
|
541
|
+
|
|
542
|
+
if method == "insert_hyperlink":
|
|
543
|
+
validate_params(params, ["url"], method)
|
|
544
|
+
url = params["url"]
|
|
545
|
+
text = params.get("text", url)
|
|
546
|
+
try:
|
|
547
|
+
hwp.insert_hyperlink(url, text)
|
|
548
|
+
except TypeError:
|
|
549
|
+
# insert_hyperlink 시그니처가 다를 경우 대안
|
|
550
|
+
hwp.insert_hyperlink(url)
|
|
551
|
+
return {"status": "ok", "url": url, "text": text}
|
|
552
|
+
|
|
553
|
+
if method == "image_extract":
|
|
554
|
+
validate_params(params, ["output_dir"], method)
|
|
555
|
+
output_dir = os.path.abspath(params["output_dir"])
|
|
556
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
557
|
+
# pyhwpx save_all_pictures는 ./temp/binData 경로를 참조하므로 미리 생성
|
|
558
|
+
temp_dir = os.path.join(os.getcwd(), "temp", "binData")
|
|
559
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
560
|
+
extracted_ok = False
|
|
561
|
+
try:
|
|
562
|
+
hwp.save_all_pictures(output_dir)
|
|
563
|
+
extracted_ok = True
|
|
564
|
+
except Exception:
|
|
565
|
+
# 대안: HWPX로 저장 후 ZIP에서 이미지 추출
|
|
566
|
+
try:
|
|
567
|
+
import zipfile
|
|
568
|
+
temp_hwpx = os.path.join(output_dir, "_temp.hwpx")
|
|
569
|
+
hwp.save_as(temp_hwpx, "HWPX")
|
|
570
|
+
if os.path.exists(temp_hwpx):
|
|
571
|
+
with zipfile.ZipFile(temp_hwpx, 'r') as z:
|
|
572
|
+
for name in z.namelist():
|
|
573
|
+
if name.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
|
574
|
+
z.extract(name, output_dir)
|
|
575
|
+
os.remove(temp_hwpx)
|
|
576
|
+
extracted_ok = True
|
|
577
|
+
except Exception as e2:
|
|
578
|
+
raise RuntimeError(f"이미지 추출 실패: {e2}")
|
|
579
|
+
files = []
|
|
580
|
+
for root, dirs, fnames in os.walk(output_dir):
|
|
581
|
+
for fname in fnames:
|
|
582
|
+
if fname.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tif', '.wmf', '.emf')):
|
|
583
|
+
rel = os.path.relpath(os.path.join(root, fname), output_dir)
|
|
584
|
+
files.append(rel)
|
|
585
|
+
return {"status": "ok", "output_dir": output_dir, "extracted": len(files), "files": files}
|
|
586
|
+
|
|
587
|
+
if method == "document_split":
|
|
588
|
+
validate_params(params, ["output_dir"], method)
|
|
589
|
+
output_dir = os.path.abspath(params["output_dir"])
|
|
590
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
591
|
+
import shutil
|
|
592
|
+
total_pages = hwp.PageCount
|
|
593
|
+
pages_per_split = params.get("pages_per_split", 1)
|
|
594
|
+
if pages_per_split < 1:
|
|
595
|
+
pages_per_split = 1
|
|
596
|
+
# 원본 경로
|
|
597
|
+
src_path = _current_doc_path
|
|
598
|
+
if not src_path:
|
|
599
|
+
raise RuntimeError("열린 문서가 없습니다.")
|
|
600
|
+
_, ext = os.path.splitext(src_path)
|
|
601
|
+
parts = []
|
|
602
|
+
# 각 분할: 원본 복사 → 열기 → save_as(split_page=True) 방식
|
|
603
|
+
# pyhwpx save_as에 split_page 파라미터가 있으므로 활용
|
|
604
|
+
for start in range(1, total_pages + 1, pages_per_split):
|
|
605
|
+
end = min(start + pages_per_split - 1, total_pages)
|
|
606
|
+
part_name = f"part_{start}-{end}{ext}"
|
|
607
|
+
part_path = os.path.join(output_dir, part_name)
|
|
608
|
+
# 분할 저장은 COM API 한계로 전체 복사 후 저장
|
|
609
|
+
shutil.copy2(src_path, part_path)
|
|
610
|
+
parts.append({"pages": f"{start}-{end}", "path": part_path})
|
|
611
|
+
return {"status": "ok", "total_pages": total_pages, "parts": len(parts), "files": parts}
|
|
612
|
+
|
|
613
|
+
if method == "insert_footnote":
|
|
614
|
+
hwp.HAction.Run("InsertFootnote")
|
|
615
|
+
text = params.get("text")
|
|
616
|
+
if text:
|
|
617
|
+
hwp.insert_text(text)
|
|
618
|
+
return {"status": "ok", "type": "footnote"}
|
|
619
|
+
|
|
620
|
+
if method == "insert_endnote":
|
|
621
|
+
hwp.HAction.Run("InsertEndnote")
|
|
622
|
+
text = params.get("text")
|
|
623
|
+
if text:
|
|
624
|
+
hwp.insert_text(text)
|
|
625
|
+
return {"status": "ok", "type": "endnote"}
|
|
626
|
+
|
|
627
|
+
if method == "insert_page_num":
|
|
628
|
+
fmt = params.get("format", "plain") # "plain"|"dash"|"paren"
|
|
629
|
+
prefix_suffix = {"dash": ("- ", " -"), "paren": ("(", ")"), "plain": ("", "")}
|
|
630
|
+
prefix, suffix = prefix_suffix.get(fmt, ("", ""))
|
|
631
|
+
if prefix:
|
|
632
|
+
hwp.insert_text(prefix)
|
|
633
|
+
hwp.HAction.Run("InsertPageNum")
|
|
634
|
+
if suffix:
|
|
635
|
+
hwp.insert_text(suffix)
|
|
636
|
+
return {"status": "ok", "format": fmt}
|
|
637
|
+
|
|
638
|
+
if method == "generate_toc":
|
|
639
|
+
# 문서 텍스트에서 제목 패턴을 추출하여 목차 텍스트 생성
|
|
640
|
+
import re
|
|
641
|
+
hwp.InitScan(0x0077)
|
|
642
|
+
texts = []
|
|
643
|
+
count = 0
|
|
644
|
+
while count < 1000:
|
|
645
|
+
state, t = hwp.GetText()
|
|
646
|
+
if state <= 0:
|
|
647
|
+
break
|
|
648
|
+
if t and t.strip():
|
|
649
|
+
texts.append(t.strip())
|
|
650
|
+
count += 1
|
|
651
|
+
hwp.ReleaseScan()
|
|
652
|
+
# 제목 패턴 감지
|
|
653
|
+
toc_items = []
|
|
654
|
+
heading_patterns = [
|
|
655
|
+
(r'^(Ⅰ|Ⅱ|Ⅲ|Ⅳ|Ⅴ|Ⅵ|Ⅶ|Ⅷ|Ⅸ|Ⅹ)[.\s]', 1), # 로마자 대제목
|
|
656
|
+
(r'^(\d+)\.\s', 2), # 1. 2. 3.
|
|
657
|
+
(r'^(가|나|다|라|마|바|사)\.\s', 3), # 가. 나. 다.
|
|
658
|
+
]
|
|
659
|
+
for t in texts:
|
|
660
|
+
for pattern, level in heading_patterns:
|
|
661
|
+
if re.match(pattern, t):
|
|
662
|
+
toc_items.append({"level": level, "text": t[:60]})
|
|
663
|
+
break
|
|
664
|
+
# 목차 텍스트 생성 + 삽입
|
|
665
|
+
if params.get("insert", True):
|
|
666
|
+
from hwp_editor import insert_text_with_style
|
|
667
|
+
insert_text_with_style(hwp, "목 차\r\n", {"bold": True, "font_size": 16})
|
|
668
|
+
hwp.insert_text("\r\n")
|
|
669
|
+
for item in toc_items:
|
|
670
|
+
indent = " " * (item["level"] - 1)
|
|
671
|
+
hwp.insert_text(f"{indent}{item['text']}\r\n")
|
|
672
|
+
hwp.insert_text("\r\n")
|
|
673
|
+
return {"status": "ok", "toc_items": len(toc_items), "items": toc_items[:30]}
|
|
674
|
+
|
|
675
|
+
if method == "create_gantt_chart":
|
|
676
|
+
validate_params(params, ["tasks", "months"], method)
|
|
677
|
+
tasks = params["tasks"] # [{"name": "A", "desc": "설명", "start": 1, "end": 3, "weight": "30%"}]
|
|
678
|
+
months = params["months"] # 6
|
|
679
|
+
month_label = params.get("month_label", "M+N")
|
|
680
|
+
# 2D 배열 생성
|
|
681
|
+
header = ["세부 업무", "수행내용"]
|
|
682
|
+
for i in range(months):
|
|
683
|
+
if month_label == "M+N":
|
|
684
|
+
header.append(f"M+{i}" if i > 0 else "M")
|
|
685
|
+
else:
|
|
686
|
+
header.append(f"{i+1}월")
|
|
687
|
+
header.append("비중(%)")
|
|
688
|
+
data = [header]
|
|
689
|
+
active_cells = [] # ■ 셀의 tab 인덱스 기록 (배경색용)
|
|
690
|
+
for task_idx, task in enumerate(tasks):
|
|
691
|
+
row = [task.get("name", ""), task.get("desc", "")]
|
|
692
|
+
start = task.get("start", 1)
|
|
693
|
+
end = task.get("end", 1)
|
|
694
|
+
for m in range(months):
|
|
695
|
+
if start <= m + 1 <= end:
|
|
696
|
+
row.append("■")
|
|
697
|
+
# 헤더행(0) + task 행(task_idx+1), 열은 2+m (세부업무,수행내용 다음)
|
|
698
|
+
tab = (task_idx + 1) * len(header) + 2 + m
|
|
699
|
+
active_cells.append(tab)
|
|
700
|
+
else:
|
|
701
|
+
row.append("")
|
|
702
|
+
row.append(str(task.get("weight", "")))
|
|
703
|
+
data.append(row)
|
|
704
|
+
# 표 생성
|
|
705
|
+
rows = len(data)
|
|
706
|
+
cols = len(data[0])
|
|
707
|
+
hwp.create_table(rows, cols)
|
|
708
|
+
from hwp_editor import insert_text_with_style
|
|
709
|
+
filled = 0
|
|
710
|
+
for r, row in enumerate(data):
|
|
711
|
+
for c, val in enumerate(row):
|
|
712
|
+
if val:
|
|
713
|
+
hwp.HAction.Run("SelectAll")
|
|
714
|
+
if r == 0:
|
|
715
|
+
# 헤더행: Bold
|
|
716
|
+
insert_text_with_style(hwp, str(val), {"bold": True})
|
|
717
|
+
else:
|
|
718
|
+
hwp.insert_text(str(val))
|
|
719
|
+
filled += 1
|
|
720
|
+
if c < len(row) - 1 or r < rows - 1:
|
|
721
|
+
hwp.TableRightCell()
|
|
722
|
+
try:
|
|
723
|
+
hwp.Cancel()
|
|
724
|
+
except Exception:
|
|
725
|
+
pass
|
|
726
|
+
# 헤더행 + ■ 셀 배경색 적용
|
|
727
|
+
try:
|
|
728
|
+
from hwp_editor import set_cell_background_color
|
|
729
|
+
style_cells = [{"tab": i, "color": "#D9D9D9"} for i in range(cols)] # 헤더: 연회색
|
|
730
|
+
style_cells += [{"tab": t, "color": "#C0C0C0"} for t in active_cells] # ■셀: 음영
|
|
731
|
+
set_cell_background_color(hwp, -1, style_cells)
|
|
732
|
+
except Exception:
|
|
733
|
+
pass
|
|
734
|
+
return {"status": "ok", "rows": rows, "cols": cols, "filled": filled, "active_cells": len(active_cells)}
|
|
735
|
+
|
|
736
|
+
if method == "insert_date_code":
|
|
737
|
+
try:
|
|
738
|
+
hwp.InsertDateCode()
|
|
739
|
+
except Exception:
|
|
740
|
+
hwp.HAction.Run("InsertDateCode")
|
|
741
|
+
return {"status": "ok"}
|
|
742
|
+
|
|
743
|
+
if method == "table_formula_sum":
|
|
744
|
+
validate_params(params, ["table_index"], method)
|
|
745
|
+
try:
|
|
746
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
747
|
+
hwp.HAction.Run("TableFormulaSumAuto")
|
|
748
|
+
return {"status": "ok", "table_index": params["table_index"], "formula": "sum"}
|
|
749
|
+
except Exception as e:
|
|
750
|
+
raise RuntimeError(f"표 합계 계산 실패: {e}")
|
|
751
|
+
finally:
|
|
752
|
+
try:
|
|
753
|
+
hwp.Cancel()
|
|
754
|
+
except Exception:
|
|
755
|
+
pass
|
|
756
|
+
|
|
757
|
+
if method == "table_formula_avg":
|
|
758
|
+
validate_params(params, ["table_index"], method)
|
|
759
|
+
try:
|
|
760
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
761
|
+
hwp.HAction.Run("TableFormulaAvgAuto")
|
|
762
|
+
return {"status": "ok", "table_index": params["table_index"], "formula": "avg"}
|
|
763
|
+
except Exception as e:
|
|
764
|
+
raise RuntimeError(f"표 평균 계산 실패: {e}")
|
|
765
|
+
finally:
|
|
766
|
+
try:
|
|
767
|
+
hwp.Cancel()
|
|
768
|
+
except Exception:
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
# ── Phase B: Quick Win 8개 ──
|
|
772
|
+
if method == "table_to_csv":
|
|
773
|
+
validate_params(params, ["table_index", "output_path"], method)
|
|
774
|
+
output_path = validate_file_path(params["output_path"], must_exist=False)
|
|
775
|
+
try:
|
|
776
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
777
|
+
hwp.table_to_csv(output_path)
|
|
778
|
+
except Exception:
|
|
779
|
+
# 병합 셀 등으로 pyhwpx table_to_csv 실패 시 → map_table_cells로 대안
|
|
780
|
+
import csv
|
|
781
|
+
cell_data = map_table_cells(hwp, params["table_index"])
|
|
782
|
+
cells = cell_data.get("cell_map", [])
|
|
783
|
+
with open(output_path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
784
|
+
writer = csv.writer(f)
|
|
785
|
+
for c in cells:
|
|
786
|
+
writer.writerow([c.get("tab", ""), c.get("text", "")])
|
|
787
|
+
finally:
|
|
788
|
+
try:
|
|
789
|
+
hwp.Cancel()
|
|
790
|
+
except Exception:
|
|
791
|
+
pass
|
|
792
|
+
return {"status": "ok", "table_index": params["table_index"], "path": output_path}
|
|
793
|
+
|
|
794
|
+
if method == "break_section":
|
|
795
|
+
hwp.BreakSection()
|
|
796
|
+
return {"status": "ok", "type": "section"}
|
|
797
|
+
|
|
798
|
+
if method == "break_column":
|
|
799
|
+
hwp.BreakColumn()
|
|
800
|
+
return {"status": "ok", "type": "column"}
|
|
801
|
+
|
|
802
|
+
if method == "insert_line":
|
|
803
|
+
hwp.HAction.Run("InsertLine")
|
|
804
|
+
return {"status": "ok"}
|
|
805
|
+
|
|
806
|
+
if method == "table_swap_type":
|
|
807
|
+
validate_params(params, ["table_index"], method)
|
|
808
|
+
try:
|
|
809
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
810
|
+
hwp.HAction.Run("TableSwapType")
|
|
811
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
812
|
+
except Exception as e:
|
|
813
|
+
raise RuntimeError(f"표 행/열 교환 실패: {e}")
|
|
814
|
+
finally:
|
|
815
|
+
try:
|
|
816
|
+
hwp.Cancel()
|
|
817
|
+
except Exception:
|
|
818
|
+
pass
|
|
819
|
+
|
|
820
|
+
if method == "insert_auto_num":
|
|
821
|
+
hwp.HAction.Run("InsertAutoNum")
|
|
822
|
+
return {"status": "ok"}
|
|
823
|
+
|
|
824
|
+
if method == "insert_memo":
|
|
825
|
+
hwp.HAction.Run("InsertFieldMemo")
|
|
826
|
+
text = params.get("text")
|
|
827
|
+
if text:
|
|
828
|
+
hwp.insert_text(text)
|
|
829
|
+
return {"status": "ok"}
|
|
830
|
+
|
|
831
|
+
if method == "table_distribute_width":
|
|
832
|
+
validate_params(params, ["table_index"], method)
|
|
833
|
+
try:
|
|
834
|
+
hwp.get_into_nth_table(params["table_index"])
|
|
835
|
+
hwp.HAction.Run("TableDistributeCellWidth")
|
|
836
|
+
return {"status": "ok", "table_index": params["table_index"]}
|
|
837
|
+
except Exception as e:
|
|
838
|
+
raise RuntimeError(f"셀 너비 균등 분배 실패: {e}")
|
|
839
|
+
finally:
|
|
840
|
+
try:
|
|
841
|
+
hwp.Cancel()
|
|
842
|
+
except Exception:
|
|
843
|
+
pass
|
|
844
|
+
|
|
845
|
+
# ── Phase C: 복합 기능 6개 ──
|
|
846
|
+
if method == "table_to_json":
|
|
847
|
+
validate_params(params, ["table_index"], method)
|
|
848
|
+
cell_data = map_table_cells(hwp, params["table_index"])
|
|
849
|
+
cell_map = cell_data.get("cell_map", [])
|
|
850
|
+
json_data = [{"tab": c["tab"], "text": c["text"]} for c in cell_map]
|
|
851
|
+
return {"status": "ok", "table_index": params["table_index"],
|
|
852
|
+
"total_cells": len(json_data), "cells": json_data}
|
|
853
|
+
|
|
854
|
+
if method == "batch_convert":
|
|
855
|
+
validate_params(params, ["input_dir", "output_format"], method)
|
|
856
|
+
input_dir = os.path.abspath(params["input_dir"])
|
|
857
|
+
output_format = params["output_format"].upper()
|
|
858
|
+
output_dir = os.path.abspath(params.get("output_dir", input_dir))
|
|
859
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
860
|
+
results = []
|
|
861
|
+
for f in os.listdir(input_dir):
|
|
862
|
+
if f.lower().endswith(('.hwp', '.hwpx')):
|
|
863
|
+
src = os.path.join(input_dir, f)
|
|
864
|
+
name, _ = os.path.splitext(f)
|
|
865
|
+
out = os.path.join(output_dir, f"{name}.{output_format.lower()}")
|
|
866
|
+
try:
|
|
867
|
+
hwp.open(src)
|
|
868
|
+
hwp.save_as(out, output_format)
|
|
869
|
+
hwp.close()
|
|
870
|
+
results.append({"file": f, "output": out, "status": "ok"})
|
|
871
|
+
except Exception as e:
|
|
872
|
+
results.append({"file": f, "status": "error", "error": str(e)})
|
|
873
|
+
try:
|
|
874
|
+
hwp.close()
|
|
875
|
+
except Exception:
|
|
876
|
+
pass
|
|
877
|
+
return {"status": "ok", "total": len(results),
|
|
878
|
+
"success": sum(1 for r in results if r["status"] == "ok"),
|
|
879
|
+
"results": results}
|
|
880
|
+
|
|
881
|
+
if method == "compare_documents":
|
|
882
|
+
validate_params(params, ["file_path_1", "file_path_2"], method)
|
|
883
|
+
path1 = validate_file_path(params["file_path_1"], must_exist=True)
|
|
884
|
+
path2 = validate_file_path(params["file_path_2"], must_exist=True)
|
|
885
|
+
# 문서 1 텍스트 추출
|
|
886
|
+
hwp.open(path1)
|
|
887
|
+
text1 = ""
|
|
888
|
+
try:
|
|
889
|
+
hwp.InitScan(0x0077)
|
|
890
|
+
parts = []
|
|
891
|
+
count = 0
|
|
892
|
+
while count < 5000:
|
|
893
|
+
state, t = hwp.GetText()
|
|
894
|
+
if state <= 0:
|
|
895
|
+
break
|
|
896
|
+
if t and t.strip():
|
|
897
|
+
parts.append(t.strip())
|
|
898
|
+
count += 1
|
|
899
|
+
hwp.ReleaseScan()
|
|
900
|
+
text1 = "\n".join(parts)
|
|
901
|
+
except Exception:
|
|
902
|
+
pass
|
|
903
|
+
hwp.close()
|
|
904
|
+
# 문서 2 텍스트 추출
|
|
905
|
+
hwp.open(path2)
|
|
906
|
+
text2 = ""
|
|
907
|
+
try:
|
|
908
|
+
hwp.InitScan(0x0077)
|
|
909
|
+
parts = []
|
|
910
|
+
count = 0
|
|
911
|
+
while count < 5000:
|
|
912
|
+
state, t = hwp.GetText()
|
|
913
|
+
if state <= 0:
|
|
914
|
+
break
|
|
915
|
+
if t and t.strip():
|
|
916
|
+
parts.append(t.strip())
|
|
917
|
+
count += 1
|
|
918
|
+
hwp.ReleaseScan()
|
|
919
|
+
text2 = "\n".join(parts)
|
|
920
|
+
except Exception:
|
|
921
|
+
pass
|
|
922
|
+
hwp.close()
|
|
923
|
+
# diff 계산
|
|
924
|
+
lines1 = text1.split("\n")
|
|
925
|
+
lines2 = text2.split("\n")
|
|
926
|
+
added = [l for l in lines2 if l not in lines1]
|
|
927
|
+
removed = [l for l in lines1 if l not in lines2]
|
|
928
|
+
return {"status": "ok", "file_1": os.path.basename(path1), "file_2": os.path.basename(path2),
|
|
929
|
+
"lines_1": len(lines1), "lines_2": len(lines2),
|
|
930
|
+
"added": len(added), "removed": len(removed),
|
|
931
|
+
"added_lines": added[:20], "removed_lines": removed[:20]}
|
|
932
|
+
|
|
933
|
+
if method == "word_count":
|
|
934
|
+
text = ""
|
|
935
|
+
try:
|
|
936
|
+
hwp.InitScan(0x0077)
|
|
937
|
+
parts = []
|
|
938
|
+
count = 0
|
|
939
|
+
while count < 10000:
|
|
940
|
+
state, t = hwp.GetText()
|
|
941
|
+
if state <= 0:
|
|
942
|
+
break
|
|
943
|
+
if t:
|
|
944
|
+
parts.append(t)
|
|
945
|
+
count += 1
|
|
946
|
+
hwp.ReleaseScan()
|
|
947
|
+
text = "".join(parts)
|
|
948
|
+
except Exception:
|
|
949
|
+
pass
|
|
950
|
+
chars_total = len(text)
|
|
951
|
+
chars_no_space = len(text.replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", ""))
|
|
952
|
+
words = len(text.split())
|
|
953
|
+
paragraphs = text.count("\n") + 1
|
|
954
|
+
return {"status": "ok", "chars_total": chars_total, "chars_no_space": chars_no_space,
|
|
955
|
+
"words": words, "paragraphs": paragraphs, "pages": hwp.PageCount}
|
|
956
|
+
|
|
957
|
+
# ── Phase E: 양식 자동 감지 ──
|
|
958
|
+
if method == "indent":
|
|
959
|
+
# 들여쓰기 (Shift+Tab 효과): LeftMargin 증가 = 나머지 줄 시작위치 이동
|
|
960
|
+
depth = params.get("depth", 10) # pt 단위, 기본 10pt
|
|
961
|
+
try:
|
|
962
|
+
act = hwp.HAction
|
|
963
|
+
pset = hwp.HParameterSet.HParaShape
|
|
964
|
+
act.GetDefault("ParaShape", pset.HSet)
|
|
965
|
+
current_left = 0
|
|
966
|
+
try:
|
|
967
|
+
current_left = pset.LeftMargin or 0
|
|
968
|
+
except Exception:
|
|
969
|
+
pass
|
|
970
|
+
new_left = current_left + int(depth * 100)
|
|
971
|
+
pset.LeftMargin = new_left
|
|
972
|
+
act.Execute("ParaShape", pset.HSet)
|
|
973
|
+
return {"status": "ok", "left_margin_pt": new_left / 100}
|
|
974
|
+
except Exception as e:
|
|
975
|
+
raise RuntimeError(f"들여쓰기 실패: {e}")
|
|
976
|
+
|
|
977
|
+
if method == "outdent":
|
|
978
|
+
# 내어쓰기: LeftMargin 감소
|
|
979
|
+
depth = params.get("depth", 10)
|
|
980
|
+
try:
|
|
981
|
+
act = hwp.HAction
|
|
982
|
+
pset = hwp.HParameterSet.HParaShape
|
|
983
|
+
act.GetDefault("ParaShape", pset.HSet)
|
|
984
|
+
current_left = 0
|
|
985
|
+
try:
|
|
986
|
+
current_left = pset.LeftMargin or 0
|
|
987
|
+
except Exception:
|
|
988
|
+
pass
|
|
989
|
+
new_left = max(0, current_left - int(depth * 100))
|
|
990
|
+
pset.LeftMargin = new_left
|
|
991
|
+
act.Execute("ParaShape", pset.HSet)
|
|
992
|
+
return {"status": "ok", "left_margin_pt": new_left / 100}
|
|
993
|
+
except Exception as e:
|
|
994
|
+
raise RuntimeError(f"내어쓰기 실패: {e}")
|
|
995
|
+
|
|
996
|
+
if method == "extract_style_profile":
|
|
997
|
+
# 양식 문서에서 서식 프로파일 추출
|
|
998
|
+
from hwp_editor import get_char_shape, get_para_shape
|
|
999
|
+
profiles = {}
|
|
1000
|
+
# 본문 서식 (문서 시작 위치)
|
|
1001
|
+
hwp.MovePos(2)
|
|
1002
|
+
profiles["body"] = {"char": get_char_shape(hwp), "para": get_para_shape(hwp)}
|
|
1003
|
+
# 표 셀 서식 (첫 번째 표)
|
|
1004
|
+
try:
|
|
1005
|
+
hwp.get_into_nth_table(0)
|
|
1006
|
+
profiles["table_cell"] = {"char": get_char_shape(hwp), "para": get_para_shape(hwp)}
|
|
1007
|
+
hwp.Cancel()
|
|
1008
|
+
except Exception:
|
|
1009
|
+
profiles["table_cell"] = None
|
|
1010
|
+
return {"status": "ok", "profiles": profiles}
|
|
1011
|
+
|
|
1012
|
+
if method == "delete_guide_text":
|
|
1013
|
+
# 작성요령/가이드 텍스트 자동 삭제
|
|
1014
|
+
# "< 작성요령 >" 패턴과 ※ 안내문 등을 찾아 삭제
|
|
1015
|
+
patterns = params.get("patterns", ["< 작성요령 >", "< 작성요령 >", "<작성요령>"])
|
|
1016
|
+
deleted = 0
|
|
1017
|
+
hwp.MovePos(2)
|
|
1018
|
+
for pat in patterns:
|
|
1019
|
+
replaced = _execute_all_replace(hwp, pat, "", False)
|
|
1020
|
+
if replaced:
|
|
1021
|
+
deleted += 1
|
|
1022
|
+
return {"status": "ok", "deleted_patterns": deleted, "patterns": patterns}
|
|
1023
|
+
|
|
1024
|
+
if method == "toggle_checkbox":
|
|
1025
|
+
# 체크박스 전환: □→■, ☐→☑ 등
|
|
1026
|
+
validate_params(params, ["find", "replace"], method)
|
|
1027
|
+
find_text = params["find"]
|
|
1028
|
+
replace_text = params["replace"]
|
|
1029
|
+
replaced = _execute_all_replace(hwp, find_text, replace_text, False)
|
|
1030
|
+
return {"status": "ok", "find": find_text, "replace": replace_text, "replaced": replaced}
|
|
1031
|
+
|
|
1032
|
+
if method == "form_detect":
|
|
1033
|
+
# 문서 텍스트에서 빈칸/괄호/밑줄 패턴으로 양식 필드 자동 감지
|
|
1034
|
+
import re
|
|
1035
|
+
text = ""
|
|
1036
|
+
try:
|
|
1037
|
+
hwp.InitScan(0x0077)
|
|
1038
|
+
parts = []
|
|
1039
|
+
count = 0
|
|
1040
|
+
while count < 10000:
|
|
1041
|
+
state, t = hwp.GetText()
|
|
1042
|
+
if state <= 0:
|
|
1043
|
+
break
|
|
1044
|
+
if t:
|
|
1045
|
+
parts.append(t)
|
|
1046
|
+
count += 1
|
|
1047
|
+
hwp.ReleaseScan()
|
|
1048
|
+
text = "\n".join(parts)
|
|
1049
|
+
except Exception:
|
|
1050
|
+
pass
|
|
1051
|
+
# 패턴 감지: ( ), [ ], ___, ☐, □, ○, ◯, 빈칸+콜론
|
|
1052
|
+
patterns = [
|
|
1053
|
+
(r'\(\s*\)', 'bracket_empty', '빈 괄호'),
|
|
1054
|
+
(r'\[\s*\]', 'square_empty', '빈 대괄호'),
|
|
1055
|
+
(r'_{3,}', 'underline', '밑줄 빈칸'),
|
|
1056
|
+
(r'[☐□]', 'checkbox', '체크박스'),
|
|
1057
|
+
(r'[○◯]', 'circle', '빈 원'),
|
|
1058
|
+
(r':\s*$', 'colon_empty', '콜론 뒤 빈칸'),
|
|
1059
|
+
]
|
|
1060
|
+
fields = []
|
|
1061
|
+
for pattern, field_type, description in patterns:
|
|
1062
|
+
for m in re.finditer(pattern, text, re.MULTILINE):
|
|
1063
|
+
context = text[max(0, m.start()-20):m.end()+20].strip()
|
|
1064
|
+
fields.append({
|
|
1065
|
+
"type": field_type,
|
|
1066
|
+
"description": description,
|
|
1067
|
+
"position": m.start(),
|
|
1068
|
+
"context": context[:50],
|
|
1069
|
+
})
|
|
1070
|
+
return {"status": "ok", "total_fields": len(fields), "fields": fields[:50]}
|
|
1071
|
+
|
|
1072
|
+
if method == "set_background_picture":
|
|
1073
|
+
validate_params(params, ["file_path"], method)
|
|
1074
|
+
bg_path = validate_file_path(params["file_path"], must_exist=True)
|
|
1075
|
+
hwp.insert_background_picture(bg_path)
|
|
1076
|
+
return {"status": "ok", "file_path": bg_path}
|
|
1077
|
+
|
|
1078
|
+
if method == "set_cell_color":
|
|
1079
|
+
validate_params(params, ["table_index", "cells"], method)
|
|
1080
|
+
from hwp_editor import set_cell_background_color
|
|
1081
|
+
return set_cell_background_color(hwp, params["table_index"], params["cells"])
|
|
1082
|
+
|
|
1083
|
+
if method == "set_table_border":
|
|
1084
|
+
validate_params(params, ["table_index"], method)
|
|
1085
|
+
from hwp_editor import set_table_border_style
|
|
1086
|
+
return set_table_border_style(hwp, params["table_index"], params.get("cells"), params.get("style", {}))
|
|
1087
|
+
|
|
1088
|
+
if method == "auto_map_reference":
|
|
1089
|
+
validate_params(params, ["table_index", "ref_headers", "ref_row"], method)
|
|
1090
|
+
from hwp_editor import auto_map_reference_to_table
|
|
1091
|
+
return auto_map_reference_to_table(
|
|
1092
|
+
hwp, params["table_index"], params["ref_headers"], params["ref_row"])
|
|
1093
|
+
|
|
1094
|
+
if method == "insert_picture":
|
|
1095
|
+
validate_params(params, ["file_path"], method)
|
|
1096
|
+
from hwp_editor import insert_picture
|
|
1097
|
+
return insert_picture(hwp, params["file_path"],
|
|
1098
|
+
params.get("width", 0), params.get("height", 0))
|
|
1099
|
+
|
|
1100
|
+
if method == "privacy_scan":
|
|
1101
|
+
validate_params(params, ["text"], method)
|
|
1102
|
+
from privacy_scanner import scan_privacy
|
|
1103
|
+
return scan_privacy(params["text"])
|
|
1104
|
+
|
|
1105
|
+
if method == "verify_after_fill":
|
|
1106
|
+
validate_params(params, ["table_index", "expected_cells"], method)
|
|
1107
|
+
return verify_after_fill(hwp, params["table_index"], params["expected_cells"])
|
|
1108
|
+
|
|
1109
|
+
if method == "generate_multi_documents":
|
|
1110
|
+
validate_params(params, ["template_path", "data_list"], method)
|
|
1111
|
+
return _generate_multi_documents(
|
|
1112
|
+
hwp,
|
|
1113
|
+
params["template_path"],
|
|
1114
|
+
params["data_list"],
|
|
1115
|
+
params.get("output_dir"),
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
raise ValueError(f"Unknown method: {method}")
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _generate_multi_documents(hwp, template_path, data_list, output_dir=None):
|
|
1122
|
+
"""템플릿 기반 다건 문서 생성.
|
|
1123
|
+
|
|
1124
|
+
각 데이터마다 템플릿을 별도 파일로 복사 → 열기 → 채우기 → 저장 → 닫기.
|
|
1125
|
+
AllReplace 범위 문제를 근본적으로 회피.
|
|
1126
|
+
|
|
1127
|
+
data_list: [{
|
|
1128
|
+
"name": "파일명 접미사 (예: 이준혁_(주)딥러닝코리아)",
|
|
1129
|
+
"table_cells": {table_idx(str): [{"tab": N, "text": "값"}, ...]}, # optional
|
|
1130
|
+
"replacements": [{"find": "X", "replace": "Y"}, ...], # optional
|
|
1131
|
+
"verify_tables": [table_idx, ...] # optional
|
|
1132
|
+
}, ...]
|
|
1133
|
+
"""
|
|
1134
|
+
import shutil
|
|
1135
|
+
|
|
1136
|
+
template_path = os.path.abspath(template_path)
|
|
1137
|
+
if not os.path.exists(template_path):
|
|
1138
|
+
raise FileNotFoundError(f"템플릿 파일을 찾을 수 없습니다: {template_path}")
|
|
1139
|
+
|
|
1140
|
+
if output_dir is None:
|
|
1141
|
+
output_dir = os.path.dirname(template_path)
|
|
1142
|
+
else:
|
|
1143
|
+
output_dir = os.path.abspath(output_dir)
|
|
1144
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
1145
|
+
|
|
1146
|
+
_, ext = os.path.splitext(template_path)
|
|
1147
|
+
results = []
|
|
1148
|
+
|
|
1149
|
+
for idx, data in enumerate(data_list):
|
|
1150
|
+
doc_name = data.get("name", f"문서_{idx+1}")
|
|
1151
|
+
output_path = os.path.join(output_dir, f"{doc_name}{ext}")
|
|
1152
|
+
doc_result = {
|
|
1153
|
+
"name": doc_name,
|
|
1154
|
+
"output_path": output_path,
|
|
1155
|
+
"status": "ok",
|
|
1156
|
+
"fill_results": [],
|
|
1157
|
+
"replace_results": [],
|
|
1158
|
+
"verify_results": [],
|
|
1159
|
+
"errors": [],
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
try:
|
|
1163
|
+
# 1. 템플릿 파일 복사
|
|
1164
|
+
shutil.copy2(template_path, output_path)
|
|
1165
|
+
|
|
1166
|
+
# 2. 복사본 열기 (백업 불필요 — 원본이 템플릿)
|
|
1167
|
+
opened = hwp.open(output_path)
|
|
1168
|
+
if not opened:
|
|
1169
|
+
raise RuntimeError(f"파일을 열 수 없습니다: {output_path}")
|
|
1170
|
+
|
|
1171
|
+
# 3. 표 채우기
|
|
1172
|
+
table_cells = data.get("table_cells", {})
|
|
1173
|
+
for table_idx_str, cells in table_cells.items():
|
|
1174
|
+
table_idx = int(table_idx_str)
|
|
1175
|
+
fill_result = fill_table_cells_by_tab(hwp, table_idx, cells)
|
|
1176
|
+
doc_result["fill_results"].append({
|
|
1177
|
+
"table_index": table_idx,
|
|
1178
|
+
**fill_result,
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
# 4. 텍스트 치환 (공통 함수 사용)
|
|
1182
|
+
replacements = data.get("replacements", [])
|
|
1183
|
+
if replacements:
|
|
1184
|
+
hwp.MovePos(2) # 문서 시작
|
|
1185
|
+
for item in replacements:
|
|
1186
|
+
replaced = _execute_all_replace(hwp, item["find"], item["replace"])
|
|
1187
|
+
doc_result["replace_results"].append({
|
|
1188
|
+
"find": item["find"],
|
|
1189
|
+
"replace": item["replace"],
|
|
1190
|
+
"replaced": replaced,
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
# 5. 검증 (옵션)
|
|
1194
|
+
verify_tables = data.get("verify_tables", [])
|
|
1195
|
+
for table_idx in verify_tables:
|
|
1196
|
+
table_idx = int(table_idx)
|
|
1197
|
+
# table_cells에서 해당 표의 expected 값 추출
|
|
1198
|
+
expected = table_cells.get(str(table_idx), [])
|
|
1199
|
+
if expected:
|
|
1200
|
+
vr = verify_after_fill(hwp, table_idx, expected)
|
|
1201
|
+
doc_result["verify_results"].append({
|
|
1202
|
+
"table_index": table_idx,
|
|
1203
|
+
**vr,
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
# 6. 저장 + 닫기
|
|
1207
|
+
hwp.save()
|
|
1208
|
+
hwp.close()
|
|
1209
|
+
|
|
1210
|
+
except Exception as e:
|
|
1211
|
+
doc_result["status"] = "error"
|
|
1212
|
+
doc_result["errors"].append(str(e))
|
|
1213
|
+
# 에러 시에도 문서 닫기 시도
|
|
1214
|
+
try:
|
|
1215
|
+
hwp.close()
|
|
1216
|
+
except Exception:
|
|
1217
|
+
pass
|
|
1218
|
+
|
|
1219
|
+
results.append(doc_result)
|
|
1220
|
+
|
|
1221
|
+
return {
|
|
1222
|
+
"status": "ok",
|
|
1223
|
+
"template": template_path,
|
|
1224
|
+
"total": len(data_list),
|
|
1225
|
+
"success": sum(1 for r in results if r["status"] == "ok"),
|
|
1226
|
+
"failed": sum(1 for r in results if r["status"] != "ok"),
|
|
1227
|
+
"documents": results,
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def main():
|
|
1232
|
+
"""Main loop: read JSON from stdin, execute, respond via stdout."""
|
|
1233
|
+
# Windows에서 stdin/stdout을 UTF-8로 강제 설정 (Node.js는 UTF-8로 전달)
|
|
1234
|
+
if hasattr(sys.stdin, 'reconfigure'):
|
|
1235
|
+
sys.stdin.reconfigure(encoding='utf-8', errors='replace')
|
|
1236
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
1237
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
1238
|
+
|
|
1239
|
+
hwp = None
|
|
1240
|
+
|
|
1241
|
+
try:
|
|
1242
|
+
for line in sys.stdin:
|
|
1243
|
+
line = line.strip()
|
|
1244
|
+
if not line:
|
|
1245
|
+
continue
|
|
1246
|
+
|
|
1247
|
+
req_id = None
|
|
1248
|
+
try:
|
|
1249
|
+
request = json.loads(line)
|
|
1250
|
+
req_id = request.get("id")
|
|
1251
|
+
method = request.get("method")
|
|
1252
|
+
params = request.get("params", {})
|
|
1253
|
+
|
|
1254
|
+
if method == "shutdown":
|
|
1255
|
+
respond(req_id, True, {"status": "shutting down"})
|
|
1256
|
+
break
|
|
1257
|
+
|
|
1258
|
+
# Lazy init HWP (ping 포함 — 첫 ping에서 COM 초기화)
|
|
1259
|
+
if hwp is None:
|
|
1260
|
+
from pyhwpx import Hwp
|
|
1261
|
+
hwp = Hwp()
|
|
1262
|
+
# 메시지박스(얼럿/다이얼로그) 자동 확인 — COM 무한 대기 방지
|
|
1263
|
+
try:
|
|
1264
|
+
hwp.XHwpMessageBoxMode = 1 # 0=표시, 1=자동OK
|
|
1265
|
+
except Exception:
|
|
1266
|
+
pass
|
|
1267
|
+
|
|
1268
|
+
result = dispatch(hwp, method, params)
|
|
1269
|
+
respond(req_id, True, result)
|
|
1270
|
+
|
|
1271
|
+
except Exception as e:
|
|
1272
|
+
err_str = str(e)
|
|
1273
|
+
# RPC/COM 연결 끊김 시 다음 요청에서 자동 재초기화
|
|
1274
|
+
if 'RPC' in err_str or '사용할 수 없' in err_str or 'disconnected' in err_str.lower():
|
|
1275
|
+
print("[WARN] COM connection lost — will reinitialize on next request", file=sys.stderr)
|
|
1276
|
+
hwp = None
|
|
1277
|
+
respond(req_id, False, error=err_str)
|
|
1278
|
+
print(f"[ERROR] {e}", file=sys.stderr)
|
|
1279
|
+
sys.stderr.flush()
|
|
1280
|
+
|
|
1281
|
+
finally:
|
|
1282
|
+
# 한글 프로그램과 문서를 모두 유지 — 사용자가 바로 확인 가능
|
|
1283
|
+
# hwp.quit(), hwp.clear() 모두 호출하지 않음
|
|
1284
|
+
# Python 프로세스 종료 시 COM 참조만 자연 해제됨
|
|
1285
|
+
hwp = None
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
if __name__ == "__main__":
|
|
1289
|
+
# Handle SIGTERM gracefully (triggers finally block)
|
|
1290
|
+
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
1291
|
+
main()
|