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,664 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composite tools: smart analysis with completion rate
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
const HWP_EXTENSIONS = new Set(['.hwp', '.hwpx']);
|
|
8
|
+
const ANALYSIS_TIMEOUT = 60000;
|
|
9
|
+
export function registerCompositeTools(server, bridge) {
|
|
10
|
+
// ── 진단 도구 (개발용) ──
|
|
11
|
+
server.tool('hwp_inspect_com_object', '[개발용] pyhwpx COM 객체의 실제 속성 목록을 덤프합니다. HCharShape/HParaShape 등의 정확한 속성명을 확인할 때 사용.', {
|
|
12
|
+
object: z.enum(['HCharShape', 'HParaShape', 'HFindReplace']).optional().describe('조사할 COM 객체 (기본: HCharShape)'),
|
|
13
|
+
}, async ({ object: objName }) => {
|
|
14
|
+
try {
|
|
15
|
+
await bridge.ensureRunning();
|
|
16
|
+
const response = await bridge.send('inspect_com_object', { object: objName ?? 'HCharShape' }, 30000);
|
|
17
|
+
if (!response.success) {
|
|
18
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
19
|
+
}
|
|
20
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
server.tool('hwp_generate_multi_documents', '하나의 템플릿으로 여러 건의 문서를 생성합니다. 각 데이터마다 템플릿을 별도 파일로 복사 후 채우기/치환하므로 AllReplace 범위 문제가 없습니다. 같은 양식에 여러 사람/기업 데이터를 채울 때 사용하세요.', {
|
|
27
|
+
template_path: z.string().describe('템플릿 HWP/HWPX 파일 경로'),
|
|
28
|
+
data_list: z.array(z.object({
|
|
29
|
+
name: z.string().describe('출력 파일명 접미사 (예: "이준혁_(주)딥러닝코리아")'),
|
|
30
|
+
table_cells: z.record(z.string(), z.array(z.object({
|
|
31
|
+
tab: z.number().int().min(0).describe('Tab 인덱스'),
|
|
32
|
+
text: z.string().describe('채울 텍스트'),
|
|
33
|
+
}))).optional().describe('표 채우기 데이터 { "표인덱스": [{tab, text}, ...] }'),
|
|
34
|
+
replacements: z.array(z.object({
|
|
35
|
+
find: z.string().describe('찾을 텍스트'),
|
|
36
|
+
replace: z.string().describe('바꿀 텍스트'),
|
|
37
|
+
})).optional().describe('텍스트 치환 목록'),
|
|
38
|
+
verify_tables: z.array(z.number().int().min(0)).optional().describe('채우기 후 검증할 표 인덱스 목록'),
|
|
39
|
+
})).describe('각 문서별 데이터'),
|
|
40
|
+
output_dir: z.string().optional().describe('출력 디렉토리 (생략 시 템플릿과 같은 폴더)'),
|
|
41
|
+
}, async ({ template_path, data_list, output_dir }) => {
|
|
42
|
+
const resolved = path.resolve(template_path);
|
|
43
|
+
if (!fs.existsSync(resolved)) {
|
|
44
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `템플릿 파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await bridge.ensureRunning();
|
|
48
|
+
const params = {
|
|
49
|
+
template_path: resolved,
|
|
50
|
+
data_list,
|
|
51
|
+
};
|
|
52
|
+
if (output_dir)
|
|
53
|
+
params.output_dir = path.resolve(output_dir);
|
|
54
|
+
const response = await bridge.send('generate_multi_documents', params, ANALYSIS_TIMEOUT * 2);
|
|
55
|
+
if (!response.success) {
|
|
56
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
57
|
+
}
|
|
58
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.tool('hwp_smart_fill', '표 셀 채우기 + 서식 자동 감지/보존. hwp_fill_table_cells와 달리 각 셀의 글꼴/크기/자간/장평을 자동 감지하고 유지합니다. 공공기관 문서처럼 서식이 중요한 경우 이 도구를 사용하세요. 적용된 서식 정보도 반환합니다.', {
|
|
65
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
66
|
+
cells: z.array(z.object({
|
|
67
|
+
tab: z.number().int().min(0).describe('Tab 인덱스'),
|
|
68
|
+
text: z.string().describe('채울 텍스트'),
|
|
69
|
+
})).describe('채울 셀 목록'),
|
|
70
|
+
}, async ({ table_index, cells }) => {
|
|
71
|
+
if (!bridge.getCurrentDocument()) {
|
|
72
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.' }) }], isError: true };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await bridge.ensureRunning();
|
|
76
|
+
const response = await bridge.send('smart_fill', { table_index, cells }, ANALYSIS_TIMEOUT);
|
|
77
|
+
if (!response.success) {
|
|
78
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
79
|
+
}
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
server.tool('hwp_auto_map_reference', '참고자료(Excel/CSV)의 헤더와 표의 라벨을 자동 매칭하여 채울 데이터를 생성합니다. 매핑 결과를 확인한 후 hwp_fill_table_cells로 실제 채우기를 진행하세요. hwp_read_reference로 데이터를 읽은 뒤 이 도구로 매핑하면 편리합니다.', {
|
|
87
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
88
|
+
ref_headers: z.array(z.string()).describe('참고자료 헤더 목록 (예: ["기업명", "대표자", "전화번호"])'),
|
|
89
|
+
ref_row: z.array(z.string()).describe('참고자료 데이터 행 (헤더 순서에 맞춤)'),
|
|
90
|
+
}, async ({ table_index, ref_headers, ref_row }) => {
|
|
91
|
+
if (!bridge.getCurrentDocument()) {
|
|
92
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.' }) }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await bridge.ensureRunning();
|
|
96
|
+
const response = await bridge.send('auto_map_reference', { table_index, ref_headers, ref_row }, ANALYSIS_TIMEOUT);
|
|
97
|
+
if (!response.success) {
|
|
98
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
server.tool('hwp_table_insert_from_csv', 'CSV 또는 Excel 파일을 읽어서 표로 자동 생성합니다. 현재 커서 위치에 헤더+데이터가 포함된 표가 삽입됩니다.', {
|
|
107
|
+
file_path: z.string().describe('CSV 또는 Excel 파일 경로'),
|
|
108
|
+
}, async ({ file_path }) => {
|
|
109
|
+
if (!bridge.getCurrentDocument()) {
|
|
110
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
111
|
+
}
|
|
112
|
+
const resolved = path.resolve(file_path);
|
|
113
|
+
if (!fs.existsSync(resolved)) {
|
|
114
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await bridge.ensureRunning();
|
|
118
|
+
const r = await bridge.send('table_insert_from_csv', { file_path: resolved }, ANALYSIS_TIMEOUT);
|
|
119
|
+
if (!r.success)
|
|
120
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
121
|
+
bridge.setCachedAnalysis(null);
|
|
122
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
server.tool('hwp_document_merge', '현재 열린 문서에 다른 HWP 문서의 내용을 합칩니다. 여러 문서를 하나로 합칠 때 사용하세요.', {
|
|
129
|
+
file_path: z.string().describe('합칠 HWP 파일 경로'),
|
|
130
|
+
}, async ({ file_path }) => {
|
|
131
|
+
if (!bridge.getCurrentDocument()) {
|
|
132
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
133
|
+
}
|
|
134
|
+
const resolved = path.resolve(file_path);
|
|
135
|
+
if (!fs.existsSync(resolved)) {
|
|
136
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await bridge.ensureRunning();
|
|
140
|
+
const response = await bridge.send('document_merge', { file_path: resolved }, ANALYSIS_TIMEOUT);
|
|
141
|
+
if (!response.success) {
|
|
142
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
143
|
+
}
|
|
144
|
+
bridge.setCachedAnalysis(null);
|
|
145
|
+
return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
server.tool('hwp_document_summary', '문서를 분석하고 빈 필드/셀을 강조하며 작성 완성도(%)를 계산합니다. 문서 상태를 한눈에 파악하고 다음 작업을 결정할 때 사용하세요.', {
|
|
152
|
+
file_path: z.string().describe('HWP/HWPX 파일 경로'),
|
|
153
|
+
}, async ({ file_path }) => {
|
|
154
|
+
const resolved = path.resolve(file_path);
|
|
155
|
+
if (!fs.existsSync(resolved)) {
|
|
156
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
157
|
+
}
|
|
158
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
159
|
+
if (!HWP_EXTENSIONS.has(ext)) {
|
|
160
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'HWP 또는 HWPX 파일만 지원합니다.' }) }], isError: true };
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
await bridge.ensureRunning();
|
|
164
|
+
const response = await bridge.send('analyze_document', { file_path: resolved }, ANALYSIS_TIMEOUT);
|
|
165
|
+
if (!response.success) {
|
|
166
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
167
|
+
}
|
|
168
|
+
const analysis = response.data;
|
|
169
|
+
bridge.setCachedAnalysis(analysis);
|
|
170
|
+
bridge.setCurrentDocument(resolved);
|
|
171
|
+
const fields = analysis.fields || [];
|
|
172
|
+
const emptyFields = fields.filter(f => !f.value || f.value.trim() === '');
|
|
173
|
+
const tables = analysis.tables || [];
|
|
174
|
+
let totalCells = 0;
|
|
175
|
+
let emptyCells = 0;
|
|
176
|
+
for (const table of tables) {
|
|
177
|
+
for (const row of table.data) {
|
|
178
|
+
for (const cell of row) {
|
|
179
|
+
totalCells++;
|
|
180
|
+
if (!cell || cell.trim() === '') {
|
|
181
|
+
emptyCells++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const totalItems = fields.length + totalCells;
|
|
187
|
+
const filledItems = (fields.length - emptyFields.length) + (totalCells - emptyCells);
|
|
188
|
+
const completionRate = totalItems > 0
|
|
189
|
+
? Math.round((filledItems / totalItems) * 100)
|
|
190
|
+
: 100;
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (emptyFields.length > 0) {
|
|
193
|
+
parts.push(`${emptyFields.length}개 빈 필드`);
|
|
194
|
+
}
|
|
195
|
+
if (emptyCells > 0) {
|
|
196
|
+
parts.push(`${emptyCells}개 빈 셀`);
|
|
197
|
+
}
|
|
198
|
+
let recommendation;
|
|
199
|
+
const nextActions = [];
|
|
200
|
+
if (parts.length > 0) {
|
|
201
|
+
recommendation = `${parts.join('과 ')}이 있습니다.`;
|
|
202
|
+
if (emptyFields.length > 0) {
|
|
203
|
+
recommendation += ' hwp_fill_fields로 필드를 채울 수 있습니다.';
|
|
204
|
+
nextActions.push({ tool: 'hwp_fill_fields', reason: `${emptyFields.length}개 빈 필드 채우기` });
|
|
205
|
+
}
|
|
206
|
+
if (emptyCells > 0) {
|
|
207
|
+
recommendation += ' hwp_fill_table_cells로 표 셀을 채울 수 있습니다.';
|
|
208
|
+
nextActions.push({ tool: 'hwp_fill_table_cells', reason: `${emptyCells}개 빈 표 셀 채우기` });
|
|
209
|
+
}
|
|
210
|
+
nextActions.push({ tool: 'hwp_save_document', reason: '변경사항 저장' });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
recommendation = '문서가 완전히 작성되었습니다.';
|
|
214
|
+
}
|
|
215
|
+
const summary = {
|
|
216
|
+
file_name: analysis.file_name,
|
|
217
|
+
file_format: analysis.file_format,
|
|
218
|
+
pages: analysis.pages,
|
|
219
|
+
table_count: tables.length,
|
|
220
|
+
field_count: fields.length,
|
|
221
|
+
empty_fields: emptyFields.map(f => ({ name: f.name })),
|
|
222
|
+
empty_cell_count: emptyCells,
|
|
223
|
+
total_cell_count: totalCells,
|
|
224
|
+
completion_rate: `${completionRate}%`,
|
|
225
|
+
text_preview: analysis.text_preview,
|
|
226
|
+
recommendation,
|
|
227
|
+
next_actions: nextActions,
|
|
228
|
+
};
|
|
229
|
+
return { content: [{ type: 'text', text: JSON.stringify(summary) }] };
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// ── 보안/개인정보 도구 ──
|
|
236
|
+
server.tool('hwp_privacy_scan', '문서 텍스트에서 개인정보(주민번호, 전화번호, 이메일, 계좌번호 등)를 자동 감지합니다. 공공기관 문서 제출 전 개인정보 포함 여부를 확인할 때 사용하세요.', {
|
|
237
|
+
file_path: z.string().optional().describe('HWP 파일 경로 (생략 시 현재 문서의 텍스트 스캔)'),
|
|
238
|
+
}, async ({ file_path }) => {
|
|
239
|
+
try {
|
|
240
|
+
await bridge.ensureRunning();
|
|
241
|
+
// 문서 텍스트 추출
|
|
242
|
+
let text;
|
|
243
|
+
if (file_path) {
|
|
244
|
+
const resolved = path.resolve(file_path);
|
|
245
|
+
const resp = await bridge.send('analyze_document', { file_path: resolved }, ANALYSIS_TIMEOUT);
|
|
246
|
+
if (!resp.success) {
|
|
247
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: resp.error }) }], isError: true };
|
|
248
|
+
}
|
|
249
|
+
text = resp.data.full_text || '';
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const current = bridge.getCurrentDocument();
|
|
253
|
+
if (!current) {
|
|
254
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
255
|
+
}
|
|
256
|
+
const resp = await bridge.send('analyze_document', { file_path: current }, ANALYSIS_TIMEOUT);
|
|
257
|
+
if (!resp.success) {
|
|
258
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: resp.error }) }], isError: true };
|
|
259
|
+
}
|
|
260
|
+
text = resp.data.full_text || '';
|
|
261
|
+
}
|
|
262
|
+
// Python에서 개인정보 스캔
|
|
263
|
+
const scanResp = await bridge.send('privacy_scan', { text }, ANALYSIS_TIMEOUT);
|
|
264
|
+
if (!scanResp.success) {
|
|
265
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: scanResp.error }) }], isError: true };
|
|
266
|
+
}
|
|
267
|
+
return { content: [{ type: 'text', text: JSON.stringify(scanResp.data) }] };
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// ── 차별화 복합 도구 ──
|
|
274
|
+
server.tool('hwp_smart_analyze', '문서를 열고, 구조 분석 + 문서 타입 추론 + 서식 프로파일 + 완성도 + 추천 작업을 한번에 수행합니다. 문서를 처음 다룰 때 이 도구 하나면 충분합니다. analyze_document + document_summary + get_table_format_summary를 통합한 원스톱 분석 도구입니다.', {
|
|
275
|
+
file_path: z.string().describe('HWP/HWPX 파일 경로'),
|
|
276
|
+
}, async ({ file_path }) => {
|
|
277
|
+
const resolved = path.resolve(file_path);
|
|
278
|
+
if (!fs.existsSync(resolved)) {
|
|
279
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
await bridge.ensureRunning();
|
|
283
|
+
// 1. 문서 분석 (smart_analyze는 복합 도구이므로 90초 타임아웃)
|
|
284
|
+
const analysisResp = await bridge.send('analyze_document', { file_path: resolved }, 90000);
|
|
285
|
+
if (!analysisResp.success) {
|
|
286
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: analysisResp.error }) }], isError: true };
|
|
287
|
+
}
|
|
288
|
+
const analysis = analysisResp.data;
|
|
289
|
+
bridge.setCachedAnalysis(analysis);
|
|
290
|
+
bridge.setCurrentDocument(resolved);
|
|
291
|
+
// 2. 완성도 계산
|
|
292
|
+
const fields = analysis.fields || [];
|
|
293
|
+
const emptyFields = fields.filter(f => !f.value || f.value.trim() === '');
|
|
294
|
+
const tables = analysis.tables || [];
|
|
295
|
+
let totalCells = 0, emptyCells = 0;
|
|
296
|
+
for (const table of tables) {
|
|
297
|
+
for (const row of table.data) {
|
|
298
|
+
for (const cell of row) {
|
|
299
|
+
totalCells++;
|
|
300
|
+
if (!cell || cell.trim() === '')
|
|
301
|
+
emptyCells++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const totalItems = fields.length + totalCells;
|
|
306
|
+
const filledItems = (fields.length - emptyFields.length) + (totalCells - emptyCells);
|
|
307
|
+
const completionRate = totalItems > 0 ? Math.round((filledItems / totalItems) * 100) : 100;
|
|
308
|
+
// 3. 문서 타입 추론
|
|
309
|
+
const fullText = (analysis.full_text || '').toLowerCase();
|
|
310
|
+
let documentType = '일반 문서';
|
|
311
|
+
const typePatterns = [
|
|
312
|
+
['사업계획서/신청서', ['사업계획', '신청서', '지원사업', '보조금', '참여기업']],
|
|
313
|
+
['공문서 (기안문/시행문)', ['수신자', '발신명의', '시행일자', '기안자', '결재']],
|
|
314
|
+
['보고서', ['보고서', '보고일', '현황', '문제점', '개선방안', '기대효과']],
|
|
315
|
+
['계약서', ['계약서', '갑', '을', '계약금', '계약기간']],
|
|
316
|
+
['이력서', ['이력서', '학력', '경력', '자격증', '자기소개']],
|
|
317
|
+
['회의록', ['회의록', '참석자', '안건', '결정사항', '향후계획']],
|
|
318
|
+
['견적서/인보이스', ['견적서', '인보이스', '품목', '단가', '합계']],
|
|
319
|
+
];
|
|
320
|
+
let maxScore = 0;
|
|
321
|
+
for (const [type, keywords] of typePatterns) {
|
|
322
|
+
const score = keywords.filter(k => fullText.includes(k)).length;
|
|
323
|
+
if (score > maxScore) {
|
|
324
|
+
maxScore = score;
|
|
325
|
+
documentType = type;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// 4. 서식 프로파일 (첫 번째 데이터 표의 서식 샘플)
|
|
329
|
+
let formatProfile = null;
|
|
330
|
+
const dataTables = tables.filter(t => t.data.length > 0);
|
|
331
|
+
const targetTable = dataTables.length > 0 ? dataTables[0] : (tables.length > 0 ? tables[0] : null);
|
|
332
|
+
if (targetTable) {
|
|
333
|
+
try {
|
|
334
|
+
const fmtResp = await bridge.send('get_table_format_summary', {
|
|
335
|
+
table_index: targetTable.index,
|
|
336
|
+
}, ANALYSIS_TIMEOUT);
|
|
337
|
+
if (fmtResp.success) {
|
|
338
|
+
formatProfile = fmtResp.data;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch { /* 서식 조회 실패해도 계속 */ }
|
|
342
|
+
}
|
|
343
|
+
// 5. 추천 작업
|
|
344
|
+
const recommendations = [];
|
|
345
|
+
if (emptyCells > 0)
|
|
346
|
+
recommendations.push(`hwp_fill_table_cells 또는 hwp_smart_fill로 ${emptyCells}개 빈 셀 채우기`);
|
|
347
|
+
if (emptyFields.length > 0)
|
|
348
|
+
recommendations.push(`hwp_fill_fields로 ${emptyFields.length}개 빈 필드 채우기`);
|
|
349
|
+
if (completionRate >= 90)
|
|
350
|
+
recommendations.push('hwp_save_document로 저장');
|
|
351
|
+
if (documentType.includes('사업계획서'))
|
|
352
|
+
recommendations.push('fill_public_document 프롬프트로 공문서 표준 작성');
|
|
353
|
+
return {
|
|
354
|
+
content: [{
|
|
355
|
+
type: 'text',
|
|
356
|
+
text: JSON.stringify({
|
|
357
|
+
file_name: analysis.file_name,
|
|
358
|
+
file_format: analysis.file_format,
|
|
359
|
+
document_type: documentType,
|
|
360
|
+
pages: analysis.pages,
|
|
361
|
+
table_count: tables.length,
|
|
362
|
+
field_count: fields.length,
|
|
363
|
+
completion_rate: `${completionRate}%`,
|
|
364
|
+
empty_cells: emptyCells,
|
|
365
|
+
empty_fields: emptyFields.length,
|
|
366
|
+
text_preview: analysis.text_preview,
|
|
367
|
+
format_profile: formatProfile,
|
|
368
|
+
recommendations,
|
|
369
|
+
}),
|
|
370
|
+
}],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
server.tool('hwp_auto_fill_from_reference', '엑셀/CSV → 자동 매핑 → 서식 보존 채우기를 일괄 수행하는 원스톱 도구. hwp_smart_fill + hwp_read_reference + hwp_auto_map_reference를 통합합니다. "이 엑셀 데이터로 신청서를 채워줘" 같은 요청에 사용하세요.', {
|
|
378
|
+
file_path: z.string().describe('참고자료 파일 경로 (xlsx, csv, json)'),
|
|
379
|
+
table_index: z.number().int().min(0).describe('채울 표 인덱스'),
|
|
380
|
+
row_index: z.number().int().min(0).optional().describe('참고자료에서 사용할 행 번호 (0부터, 생략 시 첫 번째 행)'),
|
|
381
|
+
}, async ({ file_path: refPath, table_index, row_index }) => {
|
|
382
|
+
if (!bridge.getCurrentDocument()) {
|
|
383
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다. hwp_open_document 또는 hwp_smart_analyze로 문서를 먼저 열어주세요.' }) }], isError: true };
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await bridge.ensureRunning();
|
|
387
|
+
const resolvedRef = path.resolve(refPath);
|
|
388
|
+
// 1. 참고자료 읽기
|
|
389
|
+
const refResp = await bridge.send('read_reference', { file_path: resolvedRef }, ANALYSIS_TIMEOUT);
|
|
390
|
+
if (!refResp.success) {
|
|
391
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `참고자료 읽기 실패: ${refResp.error}` }) }], isError: true };
|
|
392
|
+
}
|
|
393
|
+
const refData = refResp.data;
|
|
394
|
+
// 헤더와 데이터 행 추출
|
|
395
|
+
let headers = [];
|
|
396
|
+
let dataRow = [];
|
|
397
|
+
const format = refData.format;
|
|
398
|
+
const ri = row_index ?? 0;
|
|
399
|
+
if (format === 'csv') {
|
|
400
|
+
headers = refData.headers || [];
|
|
401
|
+
const data = refData.data || [];
|
|
402
|
+
dataRow = data[ri] || [];
|
|
403
|
+
}
|
|
404
|
+
else if (format === 'excel') {
|
|
405
|
+
const sheets = refData.sheets || [];
|
|
406
|
+
if (sheets.length > 0) {
|
|
407
|
+
headers = sheets[0].headers || [];
|
|
408
|
+
dataRow = sheets[0].data?.[ri] || [];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (format === 'json') {
|
|
412
|
+
const jsonData = refData.data;
|
|
413
|
+
if (Array.isArray(jsonData) && jsonData.length > 0) {
|
|
414
|
+
const obj = jsonData[ri] || jsonData[0];
|
|
415
|
+
headers = Object.keys(obj);
|
|
416
|
+
dataRow = Object.values(obj).map(v => String(v ?? ''));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `자동 매핑은 csv, xlsx, json만 지원합니다. (현재: ${format})` }) }], isError: true };
|
|
421
|
+
}
|
|
422
|
+
if (headers.length === 0) {
|
|
423
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '참고자료에서 헤더를 찾을 수 없습니다.' }) }], isError: true };
|
|
424
|
+
}
|
|
425
|
+
// 2. 자동 매핑
|
|
426
|
+
const mapResp = await bridge.send('auto_map_reference', {
|
|
427
|
+
table_index, ref_headers: headers, ref_row: dataRow,
|
|
428
|
+
}, ANALYSIS_TIMEOUT);
|
|
429
|
+
if (!mapResp.success) {
|
|
430
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `매핑 실패: ${mapResp.error}` }) }], isError: true };
|
|
431
|
+
}
|
|
432
|
+
const mapData = mapResp.data;
|
|
433
|
+
if (mapData.mappings.length === 0) {
|
|
434
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
435
|
+
status: 'no_mapping',
|
|
436
|
+
message: '자동 매핑된 항목이 없습니다. 표의 라벨과 참고자료 헤더가 일치하지 않습니다.',
|
|
437
|
+
unmapped: mapData.unmapped,
|
|
438
|
+
ref_headers: headers,
|
|
439
|
+
}) }] };
|
|
440
|
+
}
|
|
441
|
+
// 3. 서식 보존 채우기 (smart_fill)
|
|
442
|
+
const cells = mapData.mappings.map(m => ({ tab: m.tab, text: m.text }));
|
|
443
|
+
const fillResp = await bridge.send('smart_fill', { table_index, cells }, ANALYSIS_TIMEOUT);
|
|
444
|
+
if (!fillResp.success) {
|
|
445
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `채우기 실패: ${fillResp.error}` }) }], isError: true };
|
|
446
|
+
}
|
|
447
|
+
bridge.setCachedAnalysis(null);
|
|
448
|
+
return {
|
|
449
|
+
content: [{
|
|
450
|
+
type: 'text',
|
|
451
|
+
text: JSON.stringify({
|
|
452
|
+
status: 'ok',
|
|
453
|
+
reference_file: path.basename(resolvedRef),
|
|
454
|
+
mapped: mapData.mappings.map(m => ({ header: m.header, label: m.matched_label, value: m.text })),
|
|
455
|
+
unmapped: mapData.unmapped,
|
|
456
|
+
fill_result: fillResp.data,
|
|
457
|
+
}),
|
|
458
|
+
}],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
// ── Phase C: 복합 기능 ──
|
|
466
|
+
server.tool('hwp_table_to_json', '표 데이터를 JSON 형식으로 추출합니다.', {
|
|
467
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
468
|
+
}, async ({ table_index }) => {
|
|
469
|
+
if (!bridge.getCurrentDocument())
|
|
470
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
471
|
+
try {
|
|
472
|
+
await bridge.ensureRunning();
|
|
473
|
+
const r = await bridge.send('table_to_json', { table_index }, ANALYSIS_TIMEOUT);
|
|
474
|
+
if (!r.success)
|
|
475
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
476
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
server.tool('hwp_batch_convert', '폴더 내 모든 HWP 파일을 지정 형식으로 일괄 변환합니다.', {
|
|
483
|
+
input_dir: z.string().describe('HWP 파일이 있는 디렉토리'),
|
|
484
|
+
output_format: z.enum(['PDF', 'HTML', 'HWPX']).describe('변환할 형식'),
|
|
485
|
+
output_dir: z.string().optional().describe('출력 디렉토리 (생략 시 input_dir)'),
|
|
486
|
+
}, async ({ input_dir, output_format, output_dir }) => {
|
|
487
|
+
try {
|
|
488
|
+
await bridge.ensureRunning();
|
|
489
|
+
const params = { input_dir: path.resolve(input_dir), output_format };
|
|
490
|
+
if (output_dir)
|
|
491
|
+
params.output_dir = path.resolve(output_dir);
|
|
492
|
+
const r = await bridge.send('batch_convert', params, ANALYSIS_TIMEOUT * 5);
|
|
493
|
+
if (!r.success)
|
|
494
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
495
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
server.tool('hwp_compare_documents', '두 HWP 문서의 텍스트를 비교하여 차이점을 반환합니다.', {
|
|
502
|
+
file_path_1: z.string().describe('첫 번째 문서 경로'),
|
|
503
|
+
file_path_2: z.string().describe('두 번째 문서 경로'),
|
|
504
|
+
}, async ({ file_path_1, file_path_2 }) => {
|
|
505
|
+
try {
|
|
506
|
+
await bridge.ensureRunning();
|
|
507
|
+
const r = await bridge.send('compare_documents', {
|
|
508
|
+
file_path_1: path.resolve(file_path_1), file_path_2: path.resolve(file_path_2),
|
|
509
|
+
}, ANALYSIS_TIMEOUT * 2);
|
|
510
|
+
if (!r.success)
|
|
511
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
512
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
// ── 목차 자동 생성 ──
|
|
519
|
+
server.tool('hwp_generate_toc', '현재 문서의 제목 패턴(Ⅰ., 1., 가. 등)을 자동 감지하여 목차를 생성하고 현재 커서 위치에 삽입합니다.', {
|
|
520
|
+
dot_leader: z.boolean().optional().describe('점선 리더 사용 여부 (기본 true)'),
|
|
521
|
+
}, async ({ dot_leader }) => {
|
|
522
|
+
if (!bridge.getCurrentDocument())
|
|
523
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
524
|
+
try {
|
|
525
|
+
await bridge.ensureRunning();
|
|
526
|
+
const params = {};
|
|
527
|
+
if (dot_leader !== undefined)
|
|
528
|
+
params.dot_leader = dot_leader;
|
|
529
|
+
const r = await bridge.send('generate_toc', params, ANALYSIS_TIMEOUT);
|
|
530
|
+
if (!r.success)
|
|
531
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
532
|
+
bridge.setCachedAnalysis(null);
|
|
533
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// ── 간트차트 추진일정 표 ──
|
|
540
|
+
server.tool('hwp_create_gantt_chart', '추진일정 간트차트 표를 자동 생성합니다. 작업 목록과 기간을 입력하면 ■ 표시가 있는 일정표를 만듭니다.', {
|
|
541
|
+
tasks: z.array(z.object({
|
|
542
|
+
name: z.string().describe('작업명'),
|
|
543
|
+
desc: z.string().optional().describe('수행내용'),
|
|
544
|
+
start: z.number().int().min(1).describe('시작 월 (1부터)'),
|
|
545
|
+
end: z.number().int().min(1).describe('종료 월'),
|
|
546
|
+
weight: z.string().optional().describe('비중(%)'),
|
|
547
|
+
})).describe('작업 목록'),
|
|
548
|
+
months: z.number().int().min(1).max(24).describe('총 기간 (월 수)'),
|
|
549
|
+
month_label: z.string().optional().describe('월 라벨 형식 (기본 "M+N")'),
|
|
550
|
+
}, async ({ tasks, months, month_label }) => {
|
|
551
|
+
if (!bridge.getCurrentDocument())
|
|
552
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
553
|
+
try {
|
|
554
|
+
await bridge.ensureRunning();
|
|
555
|
+
const params = { tasks, months };
|
|
556
|
+
if (month_label)
|
|
557
|
+
params.month_label = month_label;
|
|
558
|
+
const r = await bridge.send('create_gantt_chart', params, ANALYSIS_TIMEOUT);
|
|
559
|
+
if (!r.success)
|
|
560
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
561
|
+
bridge.setCachedAnalysis(null);
|
|
562
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
server.tool('hwp_word_count', '현재 문서의 글자수, 단어수, 문단수, 페이지수를 반환합니다.', {}, async () => {
|
|
569
|
+
if (!bridge.getCurrentDocument())
|
|
570
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
571
|
+
try {
|
|
572
|
+
await bridge.ensureRunning();
|
|
573
|
+
const r = await bridge.send('word_count', {}, ANALYSIS_TIMEOUT);
|
|
574
|
+
if (!r.success)
|
|
575
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
576
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
// ── Phase D: HWPX XML 엔진 도구 ──
|
|
583
|
+
server.tool('hwp_template_list', '사용 가능한 22종 문서 템플릿 목록을 반환합니다. 공문서, 기업, 학술, 개인 카테고리별 템플릿을 확인할 수 있습니다. 한글 프로그램 없이 동작합니다.', {
|
|
584
|
+
category: z.string().optional().describe('필터링할 카테고리 (공문서/기업/학술/개인, 생략 시 전체)'),
|
|
585
|
+
}, async ({ category }) => {
|
|
586
|
+
try {
|
|
587
|
+
const { TEMPLATES } = await import('../hwpx-engine.js');
|
|
588
|
+
let list = TEMPLATES;
|
|
589
|
+
if (category) {
|
|
590
|
+
list = TEMPLATES.filter(t => t.category === category);
|
|
591
|
+
}
|
|
592
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
593
|
+
templates: list.map(t => ({ id: t.id, name: t.name, category: t.category, fields: t.fields })),
|
|
594
|
+
total: list.length,
|
|
595
|
+
categories: [...new Set(TEMPLATES.map(t => t.category))],
|
|
596
|
+
}) }] };
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
server.tool('hwp_document_create', '빈 HWPX 문서를 생성합니다. 한글 프로그램 없이 동작합니다. 생성된 파일은 한글에서 열 수 있습니다.', {
|
|
603
|
+
output_path: z.string().describe('생성할 HWPX 파일 경로'),
|
|
604
|
+
title: z.string().optional().describe('문서 제목 (선택)'),
|
|
605
|
+
}, async ({ output_path, title }) => {
|
|
606
|
+
try {
|
|
607
|
+
const { createBlankHwpx } = await import('../hwpx-engine.js');
|
|
608
|
+
const resolved = path.resolve(output_path);
|
|
609
|
+
await createBlankHwpx(resolved, title);
|
|
610
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
611
|
+
status: 'ok', path: resolved, title: title || '(빈 문서)',
|
|
612
|
+
note: '한글 프로그램에서 열어서 확인하세요.',
|
|
613
|
+
}) }] };
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
server.tool('hwp_template_generate', '템플릿 기반으로 HWPX 문서를 생성합니다. 변수(기업명, 대표자 등)를 치환하여 완성된 문서를 만듭니다. 한글 프로그램 없이 동작합니다.', {
|
|
620
|
+
template_id: z.string().describe('템플릿 ID (hwp_template_list로 확인)'),
|
|
621
|
+
variables: z.record(z.string(), z.string()).describe('채울 변수 { "기업명": "플랜아이", "대표자": "이명기" }'),
|
|
622
|
+
output_path: z.string().describe('생성할 HWPX 파일 경로'),
|
|
623
|
+
}, async ({ template_id, variables, output_path }) => {
|
|
624
|
+
try {
|
|
625
|
+
const { generateFromTemplate } = await import('../hwpx-engine.js');
|
|
626
|
+
const resolved = path.resolve(output_path);
|
|
627
|
+
const result = await generateFromTemplate(template_id, variables, resolved);
|
|
628
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
629
|
+
status: 'ok', path: resolved, template: template_id,
|
|
630
|
+
filled_fields: result.filledFields,
|
|
631
|
+
empty_fields: result.emptyFields,
|
|
632
|
+
}) }] };
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
server.tool('hwp_xml_edit_text', 'HWPX 파일의 텍스트를 직접 찾아 바꿉니다. 한글 프로그램 없이 동작합니다. HWPX(ZIP+XML) 내부의 텍스트를 수정합니다.', {
|
|
639
|
+
file_path: z.string().describe('수정할 HWPX 파일 경로'),
|
|
640
|
+
find: z.string().describe('찾을 텍스트'),
|
|
641
|
+
replace: z.string().describe('바꿀 텍스트'),
|
|
642
|
+
output_path: z.string().optional().describe('저장 경로 (생략 시 원본 덮어쓰기)'),
|
|
643
|
+
}, async ({ file_path, find, replace, output_path }) => {
|
|
644
|
+
try {
|
|
645
|
+
const { readHwpxXml, writeHwpxXml, replaceTextInSection } = await import('../hwpx-engine.js');
|
|
646
|
+
const resolved = path.resolve(file_path);
|
|
647
|
+
const outResolved = output_path ? path.resolve(output_path) : resolved;
|
|
648
|
+
if (!fs.existsSync(resolved)) {
|
|
649
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
|
|
650
|
+
}
|
|
651
|
+
const doc = await readHwpxXml(resolved, 'Contents/section0.xml');
|
|
652
|
+
const count = replaceTextInSection(doc, find, replace);
|
|
653
|
+
await writeHwpxXml(resolved, outResolved, 'Contents/section0.xml', doc);
|
|
654
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
655
|
+
status: 'ok', path: outResolved, find, replace,
|
|
656
|
+
replacements: count,
|
|
657
|
+
note: 'linesegarray가 자동 삭제되었습니다 (CLAUDE.md 규칙)',
|
|
658
|
+
}) }] };
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}
|