claude-code-hwp-mcp 0.2.2 → 0.5.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 +141 -3
- package/dist/hwp-bridge.d.ts +1 -0
- package/dist/hwp-bridge.js +35 -16
- package/dist/hwpx-engine.d.ts +20 -1
- package/dist/hwpx-engine.js +110 -3
- package/dist/index.js +2 -2
- package/dist/tools/analysis-tools.js +202 -1
- package/dist/tools/editing-tools.js +173 -12
- package/package.json +3 -3
- package/python/hwp_analyzer.py +15 -12
- package/python/hwp_editor.py +379 -115
- package/python/hwp_service.py +470 -70
- package/python/requirements.txt +1 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Claude Code와 Claude Desktop에서 한글(HWP) 문서를 AI로 자동 편집하는 MCP 서버입니다.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
94개 도구로 문서 열기, 표 채우기, 텍스트 편집, 서식 설정, 페이지 레이아웃, PDF 시각 검증까지 모두 자동화할 수 있습니다.
|
|
6
6
|
|
|
7
7
|
> Windows 전용 | 한글 2014 이상 | Python 3.8+ | Claude Code + Claude Desktop 지원
|
|
8
8
|
|
|
@@ -299,7 +299,7 @@ Python 경로, pyhwpx 설치 여부, 한글 프로그램 설치 및 실행 상
|
|
|
299
299
|
|
|
300
300
|
---
|
|
301
301
|
|
|
302
|
-
## 기능 목록 (
|
|
302
|
+
## 기능 목록 (94개)
|
|
303
303
|
|
|
304
304
|
### 환경/문서 관리 (6개)
|
|
305
305
|
|
|
@@ -379,16 +379,30 @@ Python 경로, pyhwpx 설치 여부, 한글 프로그램 설치 및 실행 상
|
|
|
379
379
|
| hwp_set_cell_color | 셀 배경색 설정 |
|
|
380
380
|
| hwp_set_table_border | 표 테두리 스타일 설정 |
|
|
381
381
|
|
|
382
|
-
###
|
|
382
|
+
### 페이지/레이아웃 (9개)
|
|
383
383
|
|
|
384
384
|
| 도구 | 설명 |
|
|
385
385
|
|------|------|
|
|
386
|
+
| hwp_set_page_setup | 여백, 용지 크기, 방향(가로/세로) 설정 |
|
|
387
|
+
| hwp_set_header_footer | 머리글/바닥글 삽입 |
|
|
388
|
+
| hwp_set_column | 다단 설정 (2단/3단, 구분선) |
|
|
389
|
+
| hwp_verify_layout | PDF→PNG 시각 검증 (PyMuPDF) |
|
|
386
390
|
| hwp_insert_picture | 이미지 삽입 |
|
|
387
391
|
| hwp_set_background_picture | 배경 이미지 설정 |
|
|
388
392
|
| hwp_insert_line | 선(줄) 삽입 |
|
|
389
393
|
| hwp_break_section | 섹션 나누기 |
|
|
390
394
|
| hwp_break_column | 다단 나누기 |
|
|
391
395
|
|
|
396
|
+
### 서식/그리기 (5개)
|
|
397
|
+
|
|
398
|
+
| 도구 | 설명 |
|
|
399
|
+
|------|------|
|
|
400
|
+
| hwp_apply_style | 문단 스타일 적용 ("제목1", "본문" 등) |
|
|
401
|
+
| hwp_set_cell_property | 셀 여백/수직정렬/텍스트방향/보호 |
|
|
402
|
+
| hwp_insert_textbox | 글상자 생성 (위치/크기 지정) |
|
|
403
|
+
| hwp_draw_line | 선 그리기 (두께/색상/스타일) |
|
|
404
|
+
| hwp_insert_caption | 표/그림 캡션 삽입 |
|
|
405
|
+
|
|
392
406
|
### 스마트/복합 도구 (16개)
|
|
393
407
|
|
|
394
408
|
| 도구 | 설명 |
|
|
@@ -459,6 +473,130 @@ Python Bridge (hwp_service.py + pyhwpx)
|
|
|
459
473
|
|
|
460
474
|
---
|
|
461
475
|
|
|
476
|
+
## HWP vs HWPX 파일 형식 차이
|
|
477
|
+
|
|
478
|
+
| 구분 | HWP (바이너리) | HWPX (XML 기반) |
|
|
479
|
+
|------|--------------|-----------------|
|
|
480
|
+
| 내부 구조 | OLE2 바이너리 | ZIP + XML |
|
|
481
|
+
| 텍스트 검색 | COM API (제한적) | XML 직접 검색 (안정적) |
|
|
482
|
+
| 찾기/바꾸기 | COM API (제한적) | XML 직접 치환 (안정적) |
|
|
483
|
+
| 표 생성/편집 | COM API | COM API |
|
|
484
|
+
| 문서 열기/저장 | COM API | COM API |
|
|
485
|
+
|
|
486
|
+
**HWPX 파일 사용을 권장합니다.** HWPX 파일의 텍스트 검색/치환은 XML을 직접 조작하므로 COM API보다 안정적입니다. 한글 프로그램에서 "다른 이름으로 저장" > "HWPX" 형식으로 변환할 수 있습니다.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## 알려진 제한사항
|
|
491
|
+
|
|
492
|
+
### HWP 파일의 COM 텍스트 검색
|
|
493
|
+
|
|
494
|
+
HWP 바이너리 파일에서 `hwp_text_search`가 0건을 반환할 수 있습니다. 이는 한글 COM API(`HAction.Execute("FindReplace")`)의 반환값이 프로그래밍적으로 불안정한 설계 한계입니다.
|
|
495
|
+
|
|
496
|
+
대안:
|
|
497
|
+
- `hwp_get_document_text`로 전체 텍스트를 가져와서 직접 검색
|
|
498
|
+
- 가능하면 HWPX 형식으로 변환하여 사용 (XML 검색은 안정적)
|
|
499
|
+
|
|
500
|
+
### HWPX 파일 잠금
|
|
501
|
+
|
|
502
|
+
한글에서 HWPX 파일을 열어둔 상태에서 XML 직접 편집을 시도하면 파일 잠금(EBUSY) 에러가 발생합니다. 이 경우 자동으로 COM 경로로 폴백합니다.
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## 추천 워크플로우
|
|
507
|
+
|
|
508
|
+
### 양식 채우기
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
1. hwp_open_document → 파일 열기
|
|
512
|
+
2. hwp_smart_analyze → 문서 구조 파악
|
|
513
|
+
3. hwp_smart_fill 또는 hwp_auto_fill_from_reference → 자동 채우기
|
|
514
|
+
4. hwp_privacy_scan → 개인정보 확인
|
|
515
|
+
5. hwp_save_document → 저장
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 텍스트 치환
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
1. hwp_open_document → 파일 열기
|
|
522
|
+
2. hwp_find_replace 또는 hwp_find_replace_multi → 치환
|
|
523
|
+
3. hwp_save_document → 저장
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 문서 분석
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
1. hwp_open_document → 파일 열기
|
|
530
|
+
2. hwp_analyze_document → 전체 구조 분석
|
|
531
|
+
3. hwp_get_tables → 표 데이터 확인
|
|
532
|
+
4. hwp_word_count → 글자수 통계
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## 변경 이력
|
|
538
|
+
|
|
539
|
+
### v0.5.0 (2026-03-25) — 94개 도구, 표 서식 대폭 강화
|
|
540
|
+
|
|
541
|
+
**신규 도구 9개:**
|
|
542
|
+
- `hwp_set_page_setup` — 여백, 용지 크기, 방향(가로/세로) 설정
|
|
543
|
+
- `hwp_set_header_footer` — 머리글/바닥글 삽입 (CreateAction 방식, 대화상자 없음)
|
|
544
|
+
- `hwp_set_column` — 다단 설정 (2단/3단, 구분선)
|
|
545
|
+
- `hwp_verify_layout` — PDF→PNG 시각 검증 (PyMuPDF 필요)
|
|
546
|
+
- `hwp_apply_style` — 문단 스타일 적용 ("제목1", "본문" 등)
|
|
547
|
+
- `hwp_set_cell_property` — 셀 여백/수직정렬/텍스트방향/보호
|
|
548
|
+
- `hwp_insert_textbox` — 글상자 생성 (위치/크기 지정)
|
|
549
|
+
- `hwp_draw_line` — 선 그리기 (두께/색상/스타일)
|
|
550
|
+
- `hwp_insert_caption` — 표/그림 캡션 삽입
|
|
551
|
+
|
|
552
|
+
**텍스트 서식 속성 25+ 추가 (insert_text):**
|
|
553
|
+
- 밑줄: underline_type(7종), underline_color
|
|
554
|
+
- 취소선: strikeout_type(4종), strikeout_color
|
|
555
|
+
- 효과: superscript, subscript, outline, shadow, emboss, engrave, small_caps
|
|
556
|
+
- 글꼴: font_name_latin(라틴 전용), shadow_color, use_kerning
|
|
557
|
+
- 배경: bg_color
|
|
558
|
+
|
|
559
|
+
**문단 서식 속성 11개 추가 (set_paragraph_style):**
|
|
560
|
+
- page_break_before, keep_with_next, widow_orphan
|
|
561
|
+
- line_wrap, snap_to_grid, auto_space_eAsian_eng/num
|
|
562
|
+
- break_latin_word, heading_type, keep_lines_together, condense
|
|
563
|
+
|
|
564
|
+
**표 기능 핵심 개선:**
|
|
565
|
+
- 셀 배경색: pyhwpx `cell_fill()` 내장 메서드 사용 (안정적)
|
|
566
|
+
- 셀 텍스트 정렬: `TableCellAlignCenterCenter` 액션 사용 (삽입 후 적용)
|
|
567
|
+
- 셀 병합: `TableCellBlockExtend` 방식으로 정확한 블록 선택
|
|
568
|
+
- 표 생성: col_widths(mm), row_heights(mm), alignment(left/center/right), header_style(Bold)
|
|
569
|
+
- 테두리: color(#RRGGBB), edges(방향별 적용) 지원
|
|
570
|
+
|
|
571
|
+
**버그 수정:**
|
|
572
|
+
- insert_text 자동 줄바꿈 — 각 호출이 독립 문단으로 생성
|
|
573
|
+
- set_header_footer: `CreateAction("HeaderFooter")` 방식 (대화상자 타임아웃 해결)
|
|
574
|
+
- apply_style: `Execute("Style")` + `SetItem("StyleName")` (대화상자 방지)
|
|
575
|
+
- draw_line: 대화형 InsertLine fallback 제거
|
|
576
|
+
- insert_footnote/endnote: try/except + CloseEx 추가
|
|
577
|
+
- get_cursor_context: page → total_pages + current_page(KeyIndicator)
|
|
578
|
+
|
|
579
|
+
### v0.3.0 — HWPX XML 라우팅 + 10건 버그 수정
|
|
580
|
+
|
|
581
|
+
**버그 수정 (10건):**
|
|
582
|
+
- SelectAll 문서 파괴 수정 (table_create_from_data, gantt_chart 등)
|
|
583
|
+
- find_replace 전후 텍스트 비교 검증으로 개선
|
|
584
|
+
- text_search 선택 영역 기반 판단으로 개선
|
|
585
|
+
- find_and_append 커서 유실 수정 (Cancel → MoveRight)
|
|
586
|
+
- 버퍼 오버플로우 시 개별 요청만 거부
|
|
587
|
+
- XHwpMessageBoxMode 복원 (close_document)
|
|
588
|
+
- blank_template.hwpx 프로그래밍적 생성 (파일 의존성 제거)
|
|
589
|
+
|
|
590
|
+
**HWPX XML 라우팅:**
|
|
591
|
+
- HWPX 파일의 텍스트 검색/치환을 Node.js XML 엔진으로 직접 처리
|
|
592
|
+
- XML 실패 시(파일 잠금 등) COM 경로로 자동 폴백
|
|
593
|
+
|
|
594
|
+
**환경 진단 개선:**
|
|
595
|
+
- Microsoft Store Python 자동 감지 + 경고
|
|
596
|
+
- 한글 프로세스 실행 여부 체크 (tasklist)
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
462
600
|
## 지원 한글 버전
|
|
463
601
|
|
|
464
602
|
한글 2014, 2018, 2020, 2022, 2024 모두 지원합니다.
|
package/dist/hwp-bridge.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export declare class HwpBridge {
|
|
|
63
63
|
getState(): BridgeState;
|
|
64
64
|
setCurrentDocument(filePath: string | null): void;
|
|
65
65
|
getCurrentDocument(): string | null;
|
|
66
|
+
getCurrentDocumentFormat(): string | null;
|
|
66
67
|
getCachedAnalysis(): unknown | null;
|
|
67
68
|
setCachedAnalysis(data: unknown): void;
|
|
68
69
|
}
|
package/dist/hwp-bridge.js
CHANGED
|
@@ -81,11 +81,22 @@ export class HwpBridge {
|
|
|
81
81
|
});
|
|
82
82
|
this.process.stdout?.on('data', (chunk) => {
|
|
83
83
|
this.buffer += chunk.toString('utf-8');
|
|
84
|
-
//
|
|
84
|
+
// BUG-6 fix: 버퍼 초과 시 가장 오래된 대기 요청만 거부 (전체가 아닌 개별)
|
|
85
85
|
if (this.buffer.length > this.MAX_BUFFER_SIZE) {
|
|
86
|
-
console.error(`[HWP MCP Bridge] Buffer exceeded ${this.MAX_BUFFER_SIZE / 1024 / 1024}MB
|
|
87
|
-
|
|
88
|
-
this.
|
|
86
|
+
console.error(`[HWP MCP Bridge] Buffer exceeded ${this.MAX_BUFFER_SIZE / 1024 / 1024}MB — truncating oldest pending`);
|
|
87
|
+
// 버퍼를 비우되, 마지막 줄바꿈 이후 부분은 보존 (진행 중인 응답)
|
|
88
|
+
const lastNewline = this.buffer.lastIndexOf('\n');
|
|
89
|
+
this.buffer = lastNewline >= 0 ? this.buffer.slice(lastNewline + 1) : '';
|
|
90
|
+
// 가장 오래된 요청 하나만 거부
|
|
91
|
+
const oldestId = this.pending.keys().next().value;
|
|
92
|
+
if (oldestId) {
|
|
93
|
+
const oldest = this.pending.get(oldestId);
|
|
94
|
+
if (oldest) {
|
|
95
|
+
clearTimeout(oldest.timer);
|
|
96
|
+
oldest.reject(new Error('응답 크기가 너무 큽니다.'));
|
|
97
|
+
this.pending.delete(oldestId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
89
100
|
return;
|
|
90
101
|
}
|
|
91
102
|
this.processBuffer();
|
|
@@ -278,23 +289,28 @@ export class HwpBridge {
|
|
|
278
289
|
};
|
|
279
290
|
return result;
|
|
280
291
|
}
|
|
281
|
-
// 3) 한글(HWP) COM
|
|
292
|
+
// 3) 한글(HWP) 설치 체크 — COM Dispatch 대신 파일 존재 + pyhwpx import 확인
|
|
293
|
+
// (COM Dispatch는 빈 한글 문서를 열어버리는 부작용이 있으므로 제거)
|
|
282
294
|
try {
|
|
283
|
-
await execFileAsync(pythonExe, ['-c',
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
result.hwp = {
|
|
290
|
-
found: false,
|
|
291
|
-
guide: '한글(HWP) 프로그램이 설치되지 않았습니다.\n→ 한컴오피스 한글 설치 필요 (한글 2014 이상)\n→ 설치 후 한글을 한번 실행하여 초기 설정 완료',
|
|
292
|
-
};
|
|
295
|
+
const { stdout } = await execFileAsync(pythonExe, ['-c',
|
|
296
|
+
'import os; paths = [r"C:\\Program Files\\Hancom", r"C:\\Program Files (x86)\\Hancom"]; ' +
|
|
297
|
+
'found = any(os.path.isdir(p) for p in paths); ' +
|
|
298
|
+
'print("installed" if found else "not_found")'
|
|
299
|
+
], { timeout: 5000 });
|
|
300
|
+
if (stdout.trim() === 'installed') {
|
|
301
|
+
result.hwp = { found: true };
|
|
293
302
|
}
|
|
294
303
|
else {
|
|
295
|
-
|
|
304
|
+
// 폴더가 없어도 pyhwpx가 COM을 찾을 수 있으므로 pyhwpx import 성공이면 OK
|
|
305
|
+
result.hwp = { found: true, guide: '한컴 설치 폴더를 찾을 수 없지만, pyhwpx가 설치되어 있으므로 동작할 수 있습니다.' };
|
|
296
306
|
}
|
|
297
307
|
}
|
|
308
|
+
catch {
|
|
309
|
+
result.hwp = {
|
|
310
|
+
found: false,
|
|
311
|
+
guide: '한글(HWP) 프로그램이 설치되지 않았습니다.\n→ 한컴오피스 한글 설치 필요 (한글 2014 이상)\n→ 설치 후 한글을 한번 실행하여 초기 설정 완료',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
298
314
|
// 4) 한글 프로세스 실행 여부 체크
|
|
299
315
|
try {
|
|
300
316
|
const { stdout } = await execFileAsync('tasklist', ['/FI', 'IMAGENAME eq Hwp.exe', '/NH'], { timeout: 5000 });
|
|
@@ -329,6 +345,9 @@ export class HwpBridge {
|
|
|
329
345
|
getCurrentDocument() {
|
|
330
346
|
return this.currentDocumentPath;
|
|
331
347
|
}
|
|
348
|
+
getCurrentDocumentFormat() {
|
|
349
|
+
return this.currentDocumentFormat;
|
|
350
|
+
}
|
|
332
351
|
getCachedAnalysis() {
|
|
333
352
|
return this.lastAnalysis;
|
|
334
353
|
}
|
package/dist/hwpx-engine.d.ts
CHANGED
|
@@ -24,9 +24,28 @@ export declare function extractTextFromSection(doc: Document): string[];
|
|
|
24
24
|
* CLAUDE.md 규칙: 수정 후 linesegarray 삭제 필수.
|
|
25
25
|
*/
|
|
26
26
|
export declare function replaceTextInSection(doc: Document, find: string, replace: string): number;
|
|
27
|
+
/**
|
|
28
|
+
* HWPX section XML에서 텍스트 검색. COM FindReplace 우회용.
|
|
29
|
+
*/
|
|
30
|
+
export declare function searchTextInSection(doc: Document, searchText: string): {
|
|
31
|
+
total: number;
|
|
32
|
+
results: Array<{
|
|
33
|
+
index: number;
|
|
34
|
+
paragraph: number;
|
|
35
|
+
context: string;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* HWPX section XML에서 N번째 텍스트만 치환.
|
|
40
|
+
*/
|
|
41
|
+
export declare function replaceTextNthInSection(doc: Document, find: string, replace: string, nth: number): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* HWPX section XML에서 텍스트를 찾아 그 뒤에 추가.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findAndAppendInSection(doc: Document, find: string, appendText: string): boolean;
|
|
27
46
|
/**
|
|
28
47
|
* 빈 HWPX 파일 생성.
|
|
29
|
-
* blank_template.hwpx
|
|
48
|
+
* BUG-9 fix: blank_template.hwpx가 없으면 프로그래밍적으로 생성.
|
|
30
49
|
*/
|
|
31
50
|
export declare function createBlankHwpx(outputPath: string, title?: string): Promise<void>;
|
|
32
51
|
/**
|
package/dist/hwpx-engine.js
CHANGED
|
@@ -114,6 +114,77 @@ export function replaceTextInSection(doc, find, replace) {
|
|
|
114
114
|
}
|
|
115
115
|
return count;
|
|
116
116
|
}
|
|
117
|
+
// ── HWPX XML 검색 (COM 우회) ──
|
|
118
|
+
/**
|
|
119
|
+
* HWPX section XML에서 텍스트 검색. COM FindReplace 우회용.
|
|
120
|
+
*/
|
|
121
|
+
export function searchTextInSection(doc, searchText) {
|
|
122
|
+
const results = [];
|
|
123
|
+
const paragraphs = doc.getElementsByTagNameNS(NS_HP, 'p');
|
|
124
|
+
let matchCount = 0;
|
|
125
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
126
|
+
const runs = paragraphs[i].getElementsByTagNameNS(NS_HP, 'run');
|
|
127
|
+
let paraText = '';
|
|
128
|
+
for (let j = 0; j < runs.length; j++) {
|
|
129
|
+
const tNodes = runs[j].getElementsByTagNameNS(NS_HP, 't');
|
|
130
|
+
for (let k = 0; k < tNodes.length; k++) {
|
|
131
|
+
paraText += tNodes[k].textContent || '';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let pos = 0;
|
|
135
|
+
while ((pos = paraText.indexOf(searchText, pos)) !== -1) {
|
|
136
|
+
matchCount++;
|
|
137
|
+
const start = Math.max(0, pos - 20);
|
|
138
|
+
const end = Math.min(paraText.length, pos + searchText.length + 20);
|
|
139
|
+
results.push({
|
|
140
|
+
index: matchCount,
|
|
141
|
+
paragraph: i,
|
|
142
|
+
context: paraText.slice(start, end),
|
|
143
|
+
});
|
|
144
|
+
pos += searchText.length;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { total: matchCount, results };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* HWPX section XML에서 N번째 텍스트만 치환.
|
|
151
|
+
*/
|
|
152
|
+
export function replaceTextNthInSection(doc, find, replace, nth) {
|
|
153
|
+
const tNodes = doc.getElementsByTagNameNS(NS_HP, 't');
|
|
154
|
+
let matchCount = 0;
|
|
155
|
+
for (let i = 0; i < tNodes.length; i++) {
|
|
156
|
+
const t = tNodes[i];
|
|
157
|
+
const text = t.textContent || '';
|
|
158
|
+
let pos = 0;
|
|
159
|
+
while ((pos = text.indexOf(find, pos)) !== -1) {
|
|
160
|
+
matchCount++;
|
|
161
|
+
if (matchCount === nth) {
|
|
162
|
+
t.textContent = text.slice(0, pos) + replace + text.slice(pos + find.length);
|
|
163
|
+
removeLinesegarray(doc);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
pos += find.length;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* HWPX section XML에서 텍스트를 찾아 그 뒤에 추가.
|
|
173
|
+
*/
|
|
174
|
+
export function findAndAppendInSection(doc, find, appendText) {
|
|
175
|
+
const tNodes = doc.getElementsByTagNameNS(NS_HP, 't');
|
|
176
|
+
for (let i = 0; i < tNodes.length; i++) {
|
|
177
|
+
const t = tNodes[i];
|
|
178
|
+
const text = t.textContent || '';
|
|
179
|
+
const pos = text.indexOf(find);
|
|
180
|
+
if (pos !== -1) {
|
|
181
|
+
t.textContent = text.slice(0, pos + find.length) + appendText + text.slice(pos + find.length);
|
|
182
|
+
removeLinesegarray(doc);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
117
188
|
/**
|
|
118
189
|
* linesegarray 요소 삭제 (CLAUDE.md 규칙 8).
|
|
119
190
|
*/
|
|
@@ -128,19 +199,55 @@ function removeLinesegarray(doc) {
|
|
|
128
199
|
}
|
|
129
200
|
}
|
|
130
201
|
// ── 빈 HWPX 생성 ──
|
|
202
|
+
/**
|
|
203
|
+
* BUG-9 fix: blank_template.hwpx 파일 의존 제거.
|
|
204
|
+
* 최소 유효 HWPX를 프로그래밍적으로 생성.
|
|
205
|
+
*/
|
|
206
|
+
async function createMinimalHwpx(outputPath, title) {
|
|
207
|
+
const zip = new JSZip();
|
|
208
|
+
// mimetype
|
|
209
|
+
zip.file('mimetype', 'application/hwp+zip');
|
|
210
|
+
// META-INF/manifest.xml
|
|
211
|
+
zip.file('META-INF/manifest.xml', `<?xml version="1.0" encoding="UTF-8"?>
|
|
212
|
+
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
|
213
|
+
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/hwp+zip"/>
|
|
214
|
+
<manifest:file-entry manifest:full-path="Contents/section0.xml" manifest:media-type="text/xml"/>
|
|
215
|
+
<manifest:file-entry manifest:full-path="Contents/content.hpf" manifest:media-type="text/xml"/>
|
|
216
|
+
</manifest:manifest>`);
|
|
217
|
+
// Contents/content.hpf
|
|
218
|
+
zip.file('Contents/content.hpf', `<?xml version="1.0" encoding="UTF-8"?>
|
|
219
|
+
<hp:HWPMLPackageFormat xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph">
|
|
220
|
+
<hp:BodyText>
|
|
221
|
+
<hp:SectionRef hp:IDRef="0"/>
|
|
222
|
+
</hp:BodyText>
|
|
223
|
+
</hp:HWPMLPackageFormat>`);
|
|
224
|
+
// Contents/section0.xml
|
|
225
|
+
const titleText = title || '';
|
|
226
|
+
zip.file('Contents/section0.xml', `<?xml version="1.0" encoding="UTF-8"?>
|
|
227
|
+
<hp:sec xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph">
|
|
228
|
+
<hp:p>
|
|
229
|
+
<hp:run>
|
|
230
|
+
<hp:t>${titleText}</hp:t>
|
|
231
|
+
</hp:run>
|
|
232
|
+
</hp:p>
|
|
233
|
+
</hp:sec>`);
|
|
234
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
235
|
+
fs.writeFileSync(outputPath, buffer);
|
|
236
|
+
}
|
|
131
237
|
function getTemplatePath() {
|
|
132
|
-
// ESM에서 __dirname 대안
|
|
133
238
|
const thisFile = new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
|
134
239
|
return path.join(path.dirname(thisFile), '../../blank_template.hwpx');
|
|
135
240
|
}
|
|
136
241
|
/**
|
|
137
242
|
* 빈 HWPX 파일 생성.
|
|
138
|
-
* blank_template.hwpx
|
|
243
|
+
* BUG-9 fix: blank_template.hwpx가 없으면 프로그래밍적으로 생성.
|
|
139
244
|
*/
|
|
140
245
|
export async function createBlankHwpx(outputPath, title) {
|
|
141
246
|
const templatePath = getTemplatePath();
|
|
142
247
|
if (!fs.existsSync(templatePath)) {
|
|
143
|
-
|
|
248
|
+
// 템플릿 파일 없음 → 프로그래밍적 생성
|
|
249
|
+
await createMinimalHwpx(outputPath, title);
|
|
250
|
+
return;
|
|
144
251
|
}
|
|
145
252
|
fs.copyFileSync(templatePath, outputPath);
|
|
146
253
|
if (title) {
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,8 @@ const resolvedToolset = validToolsets.includes(toolset)
|
|
|
19
19
|
: 'standard';
|
|
20
20
|
const bridge = new HwpBridge();
|
|
21
21
|
const server = new McpServer({
|
|
22
|
-
name: '
|
|
23
|
-
version: '0.
|
|
22
|
+
name: 'hwp-studio',
|
|
23
|
+
version: '0.4.1',
|
|
24
24
|
});
|
|
25
25
|
setupServer(server, bridge, resolvedToolset);
|
|
26
26
|
const transport = new StdioServerTransport();
|