@clazic/kordoc 2.4.18 → 2.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/dist/{auto-detect-2YGFYQCN.js → auto-detect-CBYICI6B.js} +4 -4
- package/dist/chunk-AEWWERJ5.js +35 -0
- package/dist/chunk-AEWWERJ5.js.map +1 -0
- package/dist/{chunk-7NOZFYH6.js → chunk-CLK4PNZ7.js} +7 -8
- package/dist/chunk-CLK4PNZ7.js.map +1 -0
- package/dist/chunk-CPTOBJJD.js +125 -0
- package/dist/chunk-CPTOBJJD.js.map +1 -0
- package/dist/{chunk-W2KDIKDF.js → chunk-IJGNPAK2.js} +2 -2
- package/dist/{chunk-W2KDIKDF.js.map → chunk-IJGNPAK2.js.map} +1 -1
- package/dist/{chunk-T7EBS5XP.js → chunk-NKUNXGWI.js} +10 -22
- package/dist/chunk-NKUNXGWI.js.map +1 -0
- package/dist/chunk-THBLCND6.js +33 -0
- package/dist/chunk-THBLCND6.js.map +1 -0
- package/dist/{chunk-34WIGIQC.js → chunk-Y4WFKJ5P.js} +1 -1
- package/dist/chunk-Y4WFKJ5P.js.map +1 -0
- package/dist/cli.js +41 -13
- package/dist/cli.js.map +1 -1
- package/dist/doctor-SJ7NYSXC.js +126 -0
- package/dist/doctor-SJ7NYSXC.js.map +1 -0
- package/dist/index.cjs +2606 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -7
- package/dist/index.d.ts +6 -7
- package/dist/index.js +2619 -197
- package/dist/index.js.map +1 -1
- package/dist/install-commands-P2KTEXQ4.js +11 -0
- package/dist/mcp.js +8 -6
- package/dist/mcp.js.map +1 -1
- package/dist/pm-7KGLH6MX.js +9 -0
- package/dist/{resolve-673XFZQ6.js → resolve-XWYJYKKH.js} +15 -36
- package/dist/resolve-XWYJYKKH.js.map +1 -0
- package/dist/setup/doctor.cjs +308 -0
- package/dist/setup/doctor.js +288 -0
- package/dist/{utils-DHOODYKU.js → utils-RBXHHCLI.js} +2 -2
- package/dist/utils-RBXHHCLI.js.map +1 -0
- package/dist/{watch-YGIU7RN7.js → watch-FRLS6FKE.js} +8 -6
- package/dist/{watch-YGIU7RN7.js.map → watch-FRLS6FKE.js.map} +1 -1
- package/package.json +7 -4
- package/scripts/postinstall.cjs +27 -0
- package/dist/chunk-34WIGIQC.js.map +0 -1
- package/dist/chunk-7FMKAV4P.js +0 -56
- package/dist/chunk-7FMKAV4P.js.map +0 -1
- package/dist/chunk-7NOZFYH6.js.map +0 -1
- package/dist/chunk-T7EBS5XP.js.map +0 -1
- package/dist/resolve-673XFZQ6.js.map +0 -1
- package/dist/tesseract-provider-MNMZPSGF.js +0 -11
- /package/dist/{auto-detect-2YGFYQCN.js.map → auto-detect-CBYICI6B.js.map} +0 -0
- /package/dist/{tesseract-provider-MNMZPSGF.js.map → install-commands-P2KTEXQ4.js.map} +0 -0
- /package/dist/{utils-DHOODYKU.js.map → pm-7KGLH6MX.js.map} +0 -0
package/dist/mcp.js
CHANGED
|
@@ -10,17 +10,19 @@ import {
|
|
|
10
10
|
markdownToHwpx,
|
|
11
11
|
markdownToXlsx,
|
|
12
12
|
parse
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
13
|
+
} from "./chunk-NKUNXGWI.js";
|
|
14
|
+
import "./chunk-CPTOBJJD.js";
|
|
15
|
+
import "./chunk-THBLCND6.js";
|
|
16
|
+
import "./chunk-AEWWERJ5.js";
|
|
15
17
|
import {
|
|
16
18
|
KordocError,
|
|
17
19
|
VERSION,
|
|
18
20
|
sanitizeError,
|
|
19
21
|
toArrayBuffer
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-IJGNPAK2.js";
|
|
21
23
|
import "./chunk-MOL7MDBG.js";
|
|
22
|
-
import "./chunk-
|
|
23
|
-
import "./chunk-
|
|
24
|
+
import "./chunk-Y4WFKJ5P.js";
|
|
25
|
+
import "./chunk-YW5G6BCJ.js";
|
|
24
26
|
import {
|
|
25
27
|
createLoggerFromEnv,
|
|
26
28
|
generateRunId
|
|
@@ -75,7 +77,7 @@ server.tool(
|
|
|
75
77
|
{
|
|
76
78
|
file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX)"),
|
|
77
79
|
image_dir: z.string().optional().describe("\uC774\uBBF8\uC9C0 \uC800\uC7A5 \uD3F4\uB354 \uACBD\uB85C (\uAE30\uBCF8: \uD30C\uC77C\uBA85\uACFC \uAC19\uC740 \uC774\uB984\uC758 \uD3F4\uB354)"),
|
|
78
|
-
ocr: z.enum(["auto", "gemini", "claude", "codex", "ollama", "
|
|
80
|
+
ocr: z.enum(["auto", "gemini", "claude", "codex", "ollama", "off"]).optional().describe("OCR \uBAA8\uB4DC (\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF\uC6A9): auto, gemini, claude, codex, ollama, off")
|
|
79
81
|
},
|
|
80
82
|
async ({ file_path, image_dir, ocr }) => {
|
|
81
83
|
mcpLogger.log({ level: "info", stage: "detect", event: "start", message: "MCP parse_document \uC2DC\uC791", meta: { file_path } });
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/** kordoc MCP 서버 — Claude/Cursor에서 문서 파싱 도구로 사용 */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { z } from \"zod\"\nimport { readFileSync, realpathSync, openSync, readSync, closeSync, statSync, mkdirSync, writeFileSync, readdirSync, existsSync } from \"fs\"\nimport { resolve, isAbsolute, extname, basename } from \"path\"\nimport { parse, detectFormat, blocksToMarkdown, compare, extractFormFields, markdownToHwpx, markdownToXlsx } from \"./index.js\"\nimport type { ExtractedImage } from \"./types.js\"\nimport { VERSION, toArrayBuffer, sanitizeError, KordocError } from \"./utils.js\"\nimport { createLoggerFromEnv, generateRunId } from \"./logging/logger.js\"\nimport { extractHwp5MetadataOnly } from \"./hwp5/parser.js\"\nimport { extractHwpxMetadataOnly } from \"./hwpx/parser.js\"\nimport { extractPdfMetadataOnly } from \"./pdf/parser.js\"\n\n/** 허용 파일 확장자 */\nconst ALLOWED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\n/** 최대 파일 크기 (500MB) */\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/** 경로 정규화 및 보안 검증 */\nfunction safePath(filePath: string): string {\n if (!filePath) throw new KordocError(\"파일 경로가 비어있습니다\")\n const resolved = resolve(filePath)\n const real = realpathSync(resolved)\n if (!isAbsolute(real)) throw new KordocError(\"절대 경로만 허용됩니다\")\n const ext = extname(real).toLowerCase()\n if (!ALLOWED_EXTENSIONS.has(ext)) throw new KordocError(`지원하지 않는 확장자입니다: ${ext} (허용: ${[...ALLOWED_EXTENSIONS].join(\", \")})`)\n return real\n}\n\n/** 최대 파일 크기 — metadata 전용 (50MB, 전체 파싱보다 보수적) */\nconst MAX_METADATA_FILE_SIZE = 50 * 1024 * 1024\n\n/** 파일 읽기 + 크기 검증 공통 로직 */\nfunction readValidatedFile(filePath: string, maxSize = MAX_FILE_SIZE): { buffer: ArrayBuffer; resolved: string } {\n const resolved = safePath(filePath)\n const fileSize = statSync(resolved).size\n if (fileSize > maxSize) {\n throw new KordocError(`파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${maxSize / 1024 / 1024}MB)`)\n }\n const raw = readFileSync(resolved)\n return { buffer: toArrayBuffer(raw), resolved }\n}\n\n/** 파일 헤더(16바이트)만 읽어 포맷 감지 — 전체 파일 로드 불필요 */\nfunction detectFormatFromHeader(resolved: string): ReturnType<typeof detectFormat> {\n const fd = openSync(resolved, \"r\")\n try {\n const headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n return detectFormat(toArrayBuffer(headerBuf))\n } finally {\n closeSync(fd)\n }\n}\n\nconst server = new McpServer({\n name: \"kordoc\",\n version: VERSION,\n})\n\nconst mcpLogger = createLoggerFromEnv().withRun(generateRunId(\"mcp\")).child({ component: \"mcp.ts\" })\n\n// ─── 도구: parse_document ────────────────────────────\n\nserver.tool(\n \"parse_document\",\n \"한국 문서 파일(HWP, HWPX, PDF, XLSX, DOCX)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF, XLSX, DOCX)\"),\n image_dir: z.string().optional().describe(\"이미지 저장 폴더 경로 (기본: 파일명과 같은 이름의 폴더)\"),\n ocr: z.enum([\"auto\", \"gemini\", \"claude\", \"codex\", \"ollama\", \"tesseract\", \"off\"]).optional().describe(\"OCR 모드 (이미지 기반 PDF용): auto, gemini, claude, codex, ollama, tesseract, off\"),\n },\n async ({ file_path, image_dir, ocr }) => {\n mcpLogger.log({ level: \"info\", stage: \"detect\", event: \"start\", message: \"MCP parse_document 시작\", meta: { file_path } })\n try {\n const { buffer, resolved: resolvedFilePath } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { ocrMode: ocr as import(\"./types.js\").OcrMode | undefined })\n\n if (!result.success) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 실패\", meta: { file_path, error: result.error, code: result.code } })\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n result.metadata?.title ? `제목: ${result.metadata.title}` : null,\n result.metadata?.author ? `작성자: ${result.metadata.author}` : null,\n result.isImageBased ? \"이미지 기반 PDF (텍스트 추출 불가)\" : null,\n ].filter(Boolean).join(\" | \")\n\n // outline/warnings 부가 정보 추가\n const parts: string[] = [`[${meta}]`]\n\n if (result.outline && result.outline.length > 0) {\n const outlineText = result.outline.map(o => `${\" \".repeat(o.level - 1)}- ${o.text}`).join(\"\\n\")\n parts.push(`\\n📑 문서 구조:\\n${outlineText}`)\n }\n\n if (result.warnings && result.warnings.length > 0) {\n const warnText = result.warnings.map(w => `- [p${w.page || \"?\"}] ${w.message}`).join(\"\\n\")\n parts.push(`\\n⚠️ 경고:\\n${warnText}`)\n }\n\n // 이미지 저장\n const savedImages: string[] = []\n if (result.images?.length) {\n const defaultDir = resolve(resolvedFilePath, \"..\", basename(resolvedFilePath).replace(/\\.[^.]+$/, \"\"))\n const imgDir = image_dir ? resolve(image_dir) : defaultDir\n mkdirSync(imgDir, { recursive: true })\n for (const img of result.images) {\n const imgPath = resolve(imgDir, img.filename)\n writeFileSync(imgPath, img.data)\n savedImages.push(imgPath)\n }\n parts.push(`\\n💾 저장된 이미지 (${savedImages.length}개):\\n${savedImages.map(p => ` ${p}`).join(\"\\n\")}`)\n }\n\n parts.push(`\\n\\n${result.markdown}`)\n mcpLogger.log({ level: \"info\", stage: \"finalize\", event: \"done\", message: \"MCP parse_document 완료\", meta: { file_path, fileType: result.fileType } })\n\n return {\n content: [{ type: \"text\", text: parts.join(\"\") }],\n }\n } catch (err) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 예외\", error: { message: sanitizeError(err), name: err instanceof Error ? err.name : \"Error\", stack: err instanceof Error ? err.stack : undefined } })\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: convert_document ──────────────────────────\n\nserver.tool(\n \"convert_document\",\n \"마크다운 텍스트를 HWPX 또는 XLSX 파일로 변환하여 저장합니다.\",\n {\n markdown: z.string().min(1).describe(\"변환할 마크다운 텍스트\"),\n output_path: z.string().min(1).describe(\"저장할 파일의 절대 경로 (.hwpx 또는 .xlsx)\"),\n format: z.enum([\"hwpx\", \"xlsx\"]).optional().default(\"hwpx\").describe(\"출력 포맷 (기본: hwpx)\"),\n image_dir: z.string().optional().describe(\"이미지 폴더 경로 (기본: output_path 파일명 폴더)\"),\n template_path: z.string().optional().describe(\"HWPX 템플릿 파일 경로 (hwpx 전용)\"),\n },\n async ({ markdown, output_path, format, image_dir, template_path }) => {\n try {\n const resolvedOutput = resolve(output_path)\n const outputExt = extname(resolvedOutput).toLowerCase()\n if (outputExt !== \".hwpx\" && outputExt !== \".xlsx\") {\n return {\n content: [{ type: \"text\", text: `오류: 출력 파일 확장자는 .hwpx 또는 .xlsx만 허용됩니다: ${outputExt || \"(없음)\"}` }],\n isError: true,\n }\n }\n const warnings: string[] = []\n\n // 이미지 폴더에서 이미지 로드\n const stem = basename(resolvedOutput).replace(/\\.[^.]+$/, \"\")\n const defaultImgDir = resolve(resolvedOutput, \"..\", stem)\n const imgDir = image_dir ? resolve(image_dir) : defaultImgDir\n const images: ExtractedImage[] = []\n\n if (existsSync(imgDir)) {\n const mimeMap: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\",\n gif: \"image/gif\", bmp: \"image/bmp\",\n }\n for (const entry of readdirSync(imgDir, { withFileTypes: true })) {\n if (!entry.isFile()) continue\n const fname = entry.name\n const ext = extname(fname).slice(1).toLowerCase()\n if (!mimeMap[ext]) continue\n const data = readFileSync(resolve(imgDir, fname))\n images.push({ filename: fname, data: new Uint8Array(data), mimeType: mimeMap[ext] })\n }\n }\n\n let buf: ArrayBuffer\n if (format === \"xlsx\") {\n if (template_path) warnings.push(\"[warn] --template은 hwpx 전용입니다. 무시됩니다.\")\n buf = await markdownToXlsx(markdown, { warnings, images: images.length ? images : undefined })\n } else {\n let templateArrayBuffer: ArrayBuffer | undefined\n if (template_path) {\n const tmpl = readFileSync(safePath(template_path))\n templateArrayBuffer = tmpl.buffer.slice(tmpl.byteOffset, tmpl.byteOffset + tmpl.byteLength)\n }\n buf = await markdownToHwpx(markdown, {\n warnings,\n images: images.length ? images : undefined,\n templateArrayBuffer,\n })\n }\n\n writeFileSync(resolvedOutput, Buffer.from(buf))\n\n const parts = [\n `✅ 변환 완료: ${resolvedOutput}`,\n `포맷: ${format.toUpperCase()}, 크기: ${(buf.byteLength / 1024).toFixed(1)}KB`,\n ]\n if (images.length) parts.push(`포함된 이미지: ${images.length}개 (${imgDir})`)\n if (warnings.length) parts.push(`경고:\\n${warnings.map(w => ` ${w}`).join(\"\\n\")}`)\n\n return { content: [{ type: \"text\", text: parts.join(\"\\n\") }] }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: detect_format ─────────────────────────────\n\nserver.tool(\n \"detect_format\",\n \"파일의 포맷을 매직 바이트로 감지합니다 (hwpx, hwp, pdf, unknown).\",\n {\n file_path: z.string().min(1).describe(\"감지할 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n return {\n content: [{ type: \"text\", text: `${file_path}: ${format}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_metadata ────────────────────────────\n\nserver.tool(\n \"parse_metadata\",\n \"문서의 메타데이터(제목, 작성자, 날짜 등)만 빠르게 추출합니다. 전체 파싱 없이 헤더/매니페스트만 읽습니다.\",\n {\n file_path: z.string().min(1).describe(\"메타데이터를 추출할 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n // metadata 전용 크기 제한 (50MB)\n const { buffer } = readValidatedFile(file_path, MAX_METADATA_FILE_SIZE)\n\n let metadata\n switch (format) {\n case \"hwp\":\n metadata = extractHwp5MetadataOnly(Buffer.from(buffer))\n break\n case \"hwpx\":\n metadata = await extractHwpxMetadataOnly(buffer)\n break\n case \"pdf\":\n metadata = await extractPdfMetadataOnly(buffer)\n break\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ format, ...metadata }, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_pages ──────────────────────────────\n\nserver.tool(\n \"parse_pages\",\n \"문서의 특정 페이지/섹션 범위만 파싱합니다. PDF는 정확한 페이지, HWP/HWPX는 섹션 단위 근사치입니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n pages: z.string().min(1).describe(\"페이지 범위 (예: '1-3', '1,3,5-7')\"),\n },\n async ({ file_path, pages }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { pages })\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n `범위: ${pages}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n ].filter(Boolean).join(\" | \")\n\n return {\n content: [{ type: \"text\", text: `[${meta}]\\n\\n${result.markdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_table ──────────────────────────────\n\nserver.tool(\n \"parse_table\",\n \"문서에서 N번째 테이블만 추출합니다 (0-based index). 테이블이 없거나 인덱스 범위를 초과하면 오류를 반환합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n table_index: z.number().int().min(0).describe(\"추출할 테이블 인덱스 (0부터 시작)\"),\n },\n async ({ file_path, table_index }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const tableBlocks = result.blocks.filter(b => b.type === \"table\" && b.table)\n if (tableBlocks.length === 0) {\n return {\n content: [{ type: \"text\", text: `문서에 테이블이 없습니다.` }],\n isError: true,\n }\n }\n\n if (table_index >= tableBlocks.length) {\n return {\n content: [{ type: \"text\", text: `테이블 인덱스 초과: ${table_index} (총 ${tableBlocks.length}개 테이블)` }],\n isError: true,\n }\n }\n\n const tableBlock = tableBlocks[table_index]\n const tableMarkdown = blocksToMarkdown([tableBlock])\n\n return {\n content: [{ type: \"text\", text: `[테이블 #${table_index} / 총 ${tableBlocks.length}개]\\n\\n${tableMarkdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: compare_documents ─────────────────────────\n\nserver.tool(\n \"compare_documents\",\n \"두 한국 문서 파일을 비교하여 추가/삭제/변경된 블록을 표시합니다. 신구대조표 생성에 활용됩니다. 크로스 포맷(HWP↔HWPX) 비교 가능.\",\n {\n file_path_a: z.string().min(1).describe(\"비교 원본 문서의 절대 경로\"),\n file_path_b: z.string().min(1).describe(\"비교 대상 문서의 절대 경로\"),\n },\n async ({ file_path_a, file_path_b }) => {\n try {\n const { buffer: bufA } = readValidatedFile(file_path_a)\n const { buffer: bufB } = readValidatedFile(file_path_b)\n\n const result = await compare(bufA, bufB)\n const { stats, diffs } = result\n\n const lines: string[] = [\n `## 문서 비교 결과`,\n `추가: ${stats.added} | 삭제: ${stats.removed} | 변경: ${stats.modified} | 동일: ${stats.unchanged}`,\n \"\",\n ]\n\n for (const d of diffs) {\n const prefix = d.type === \"added\" ? \"+\" : d.type === \"removed\" ? \"-\" : d.type === \"modified\" ? \"~\" : \" \"\n const text = d.after?.text || d.before?.text || (d.after?.table ? \"[테이블]\" : d.before?.table ? \"[테이블]\" : \"\")\n const sim = d.similarity !== undefined ? ` (${(d.similarity * 100).toFixed(0)}%)` : \"\"\n lines.push(`${prefix} ${text.substring(0, 200)}${sim}`)\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_form ───────────────────────────────\n\nserver.tool(\n \"parse_form\",\n \"한국 서식 문서에서 레이블-값 쌍을 구조화된 JSON으로 추출합니다. 양식/서식 문서에 최적화.\",\n {\n file_path: z.string().min(1).describe(\"서식 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패: ${result.error}` }],\n isError: true,\n }\n }\n\n const form = extractFormFields(result.blocks)\n return {\n content: [{ type: \"text\", text: JSON.stringify(form, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 서버 시작 ───────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\nmain().catch((err) => { console.error(err); process.exit(1) })\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/** kordoc MCP 서버 — Claude/Cursor에서 문서 파싱 도구로 사용 */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { z } from \"zod\"\nimport { readFileSync, realpathSync, openSync, readSync, closeSync, statSync, mkdirSync, writeFileSync, readdirSync, existsSync } from \"fs\"\nimport { resolve, isAbsolute, extname, basename } from \"path\"\nimport { parse, detectFormat, blocksToMarkdown, compare, extractFormFields, markdownToHwpx, markdownToXlsx } from \"./index.js\"\nimport type { ExtractedImage } from \"./types.js\"\nimport { VERSION, toArrayBuffer, sanitizeError, KordocError } from \"./utils.js\"\nimport { createLoggerFromEnv, generateRunId } from \"./logging/logger.js\"\nimport { extractHwp5MetadataOnly } from \"./hwp5/parser.js\"\nimport { extractHwpxMetadataOnly } from \"./hwpx/parser.js\"\nimport { extractPdfMetadataOnly } from \"./pdf/parser.js\"\n\n/** 허용 파일 확장자 */\nconst ALLOWED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\n/** 최대 파일 크기 (500MB) */\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/** 경로 정규화 및 보안 검증 */\nfunction safePath(filePath: string): string {\n if (!filePath) throw new KordocError(\"파일 경로가 비어있습니다\")\n const resolved = resolve(filePath)\n const real = realpathSync(resolved)\n if (!isAbsolute(real)) throw new KordocError(\"절대 경로만 허용됩니다\")\n const ext = extname(real).toLowerCase()\n if (!ALLOWED_EXTENSIONS.has(ext)) throw new KordocError(`지원하지 않는 확장자입니다: ${ext} (허용: ${[...ALLOWED_EXTENSIONS].join(\", \")})`)\n return real\n}\n\n/** 최대 파일 크기 — metadata 전용 (50MB, 전체 파싱보다 보수적) */\nconst MAX_METADATA_FILE_SIZE = 50 * 1024 * 1024\n\n/** 파일 읽기 + 크기 검증 공통 로직 */\nfunction readValidatedFile(filePath: string, maxSize = MAX_FILE_SIZE): { buffer: ArrayBuffer; resolved: string } {\n const resolved = safePath(filePath)\n const fileSize = statSync(resolved).size\n if (fileSize > maxSize) {\n throw new KordocError(`파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${maxSize / 1024 / 1024}MB)`)\n }\n const raw = readFileSync(resolved)\n return { buffer: toArrayBuffer(raw), resolved }\n}\n\n/** 파일 헤더(16바이트)만 읽어 포맷 감지 — 전체 파일 로드 불필요 */\nfunction detectFormatFromHeader(resolved: string): ReturnType<typeof detectFormat> {\n const fd = openSync(resolved, \"r\")\n try {\n const headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n return detectFormat(toArrayBuffer(headerBuf))\n } finally {\n closeSync(fd)\n }\n}\n\nconst server = new McpServer({\n name: \"kordoc\",\n version: VERSION,\n})\n\nconst mcpLogger = createLoggerFromEnv().withRun(generateRunId(\"mcp\")).child({ component: \"mcp.ts\" })\n\n// ─── 도구: parse_document ────────────────────────────\n\nserver.tool(\n \"parse_document\",\n \"한국 문서 파일(HWP, HWPX, PDF, XLSX, DOCX)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF, XLSX, DOCX)\"),\n image_dir: z.string().optional().describe(\"이미지 저장 폴더 경로 (기본: 파일명과 같은 이름의 폴더)\"),\n ocr: z.enum([\"auto\", \"gemini\", \"claude\", \"codex\", \"ollama\", \"off\"]).optional().describe(\"OCR 모드 (이미지 기반 PDF용): auto, gemini, claude, codex, ollama, off\"),\n },\n async ({ file_path, image_dir, ocr }) => {\n mcpLogger.log({ level: \"info\", stage: \"detect\", event: \"start\", message: \"MCP parse_document 시작\", meta: { file_path } })\n try {\n const { buffer, resolved: resolvedFilePath } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { ocrMode: ocr as import(\"./types.js\").OcrMode | undefined })\n\n if (!result.success) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 실패\", meta: { file_path, error: result.error, code: result.code } })\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n result.metadata?.title ? `제목: ${result.metadata.title}` : null,\n result.metadata?.author ? `작성자: ${result.metadata.author}` : null,\n result.isImageBased ? \"이미지 기반 PDF (텍스트 추출 불가)\" : null,\n ].filter(Boolean).join(\" | \")\n\n // outline/warnings 부가 정보 추가\n const parts: string[] = [`[${meta}]`]\n\n if (result.outline && result.outline.length > 0) {\n const outlineText = result.outline.map(o => `${\" \".repeat(o.level - 1)}- ${o.text}`).join(\"\\n\")\n parts.push(`\\n📑 문서 구조:\\n${outlineText}`)\n }\n\n if (result.warnings && result.warnings.length > 0) {\n const warnText = result.warnings.map(w => `- [p${w.page || \"?\"}] ${w.message}`).join(\"\\n\")\n parts.push(`\\n⚠️ 경고:\\n${warnText}`)\n }\n\n // 이미지 저장\n const savedImages: string[] = []\n if (result.images?.length) {\n const defaultDir = resolve(resolvedFilePath, \"..\", basename(resolvedFilePath).replace(/\\.[^.]+$/, \"\"))\n const imgDir = image_dir ? resolve(image_dir) : defaultDir\n mkdirSync(imgDir, { recursive: true })\n for (const img of result.images) {\n const imgPath = resolve(imgDir, img.filename)\n writeFileSync(imgPath, img.data)\n savedImages.push(imgPath)\n }\n parts.push(`\\n💾 저장된 이미지 (${savedImages.length}개):\\n${savedImages.map(p => ` ${p}`).join(\"\\n\")}`)\n }\n\n parts.push(`\\n\\n${result.markdown}`)\n mcpLogger.log({ level: \"info\", stage: \"finalize\", event: \"done\", message: \"MCP parse_document 완료\", meta: { file_path, fileType: result.fileType } })\n\n return {\n content: [{ type: \"text\", text: parts.join(\"\") }],\n }\n } catch (err) {\n mcpLogger.log({ level: \"error\", stage: \"finalize\", event: \"error\", message: \"MCP parse_document 예외\", error: { message: sanitizeError(err), name: err instanceof Error ? err.name : \"Error\", stack: err instanceof Error ? err.stack : undefined } })\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: convert_document ──────────────────────────\n\nserver.tool(\n \"convert_document\",\n \"마크다운 텍스트를 HWPX 또는 XLSX 파일로 변환하여 저장합니다.\",\n {\n markdown: z.string().min(1).describe(\"변환할 마크다운 텍스트\"),\n output_path: z.string().min(1).describe(\"저장할 파일의 절대 경로 (.hwpx 또는 .xlsx)\"),\n format: z.enum([\"hwpx\", \"xlsx\"]).optional().default(\"hwpx\").describe(\"출력 포맷 (기본: hwpx)\"),\n image_dir: z.string().optional().describe(\"이미지 폴더 경로 (기본: output_path 파일명 폴더)\"),\n template_path: z.string().optional().describe(\"HWPX 템플릿 파일 경로 (hwpx 전용)\"),\n },\n async ({ markdown, output_path, format, image_dir, template_path }) => {\n try {\n const resolvedOutput = resolve(output_path)\n const outputExt = extname(resolvedOutput).toLowerCase()\n if (outputExt !== \".hwpx\" && outputExt !== \".xlsx\") {\n return {\n content: [{ type: \"text\", text: `오류: 출력 파일 확장자는 .hwpx 또는 .xlsx만 허용됩니다: ${outputExt || \"(없음)\"}` }],\n isError: true,\n }\n }\n const warnings: string[] = []\n\n // 이미지 폴더에서 이미지 로드\n const stem = basename(resolvedOutput).replace(/\\.[^.]+$/, \"\")\n const defaultImgDir = resolve(resolvedOutput, \"..\", stem)\n const imgDir = image_dir ? resolve(image_dir) : defaultImgDir\n const images: ExtractedImage[] = []\n\n if (existsSync(imgDir)) {\n const mimeMap: Record<string, string> = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\",\n gif: \"image/gif\", bmp: \"image/bmp\",\n }\n for (const entry of readdirSync(imgDir, { withFileTypes: true })) {\n if (!entry.isFile()) continue\n const fname = entry.name\n const ext = extname(fname).slice(1).toLowerCase()\n if (!mimeMap[ext]) continue\n const data = readFileSync(resolve(imgDir, fname))\n images.push({ filename: fname, data: new Uint8Array(data), mimeType: mimeMap[ext] })\n }\n }\n\n let buf: ArrayBuffer\n if (format === \"xlsx\") {\n if (template_path) warnings.push(\"[warn] --template은 hwpx 전용입니다. 무시됩니다.\")\n buf = await markdownToXlsx(markdown, { warnings, images: images.length ? images : undefined })\n } else {\n let templateArrayBuffer: ArrayBuffer | undefined\n if (template_path) {\n const tmpl = readFileSync(safePath(template_path))\n templateArrayBuffer = tmpl.buffer.slice(tmpl.byteOffset, tmpl.byteOffset + tmpl.byteLength)\n }\n buf = await markdownToHwpx(markdown, {\n warnings,\n images: images.length ? images : undefined,\n templateArrayBuffer,\n })\n }\n\n writeFileSync(resolvedOutput, Buffer.from(buf))\n\n const parts = [\n `✅ 변환 완료: ${resolvedOutput}`,\n `포맷: ${format.toUpperCase()}, 크기: ${(buf.byteLength / 1024).toFixed(1)}KB`,\n ]\n if (images.length) parts.push(`포함된 이미지: ${images.length}개 (${imgDir})`)\n if (warnings.length) parts.push(`경고:\\n${warnings.map(w => ` ${w}`).join(\"\\n\")}`)\n\n return { content: [{ type: \"text\", text: parts.join(\"\\n\") }] }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: detect_format ─────────────────────────────\n\nserver.tool(\n \"detect_format\",\n \"파일의 포맷을 매직 바이트로 감지합니다 (hwpx, hwp, pdf, unknown).\",\n {\n file_path: z.string().min(1).describe(\"감지할 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n return {\n content: [{ type: \"text\", text: `${file_path}: ${format}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_metadata ────────────────────────────\n\nserver.tool(\n \"parse_metadata\",\n \"문서의 메타데이터(제목, 작성자, 날짜 등)만 빠르게 추출합니다. 전체 파싱 없이 헤더/매니페스트만 읽습니다.\",\n {\n file_path: z.string().min(1).describe(\"메타데이터를 추출할 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n // metadata 전용 크기 제한 (50MB)\n const { buffer } = readValidatedFile(file_path, MAX_METADATA_FILE_SIZE)\n\n let metadata\n switch (format) {\n case \"hwp\":\n metadata = extractHwp5MetadataOnly(Buffer.from(buffer))\n break\n case \"hwpx\":\n metadata = await extractHwpxMetadataOnly(buffer)\n break\n case \"pdf\":\n metadata = await extractPdfMetadataOnly(buffer)\n break\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ format, ...metadata }, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_pages ──────────────────────────────\n\nserver.tool(\n \"parse_pages\",\n \"문서의 특정 페이지/섹션 범위만 파싱합니다. PDF는 정확한 페이지, HWP/HWPX는 섹션 단위 근사치입니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n pages: z.string().min(1).describe(\"페이지 범위 (예: '1-3', '1,3,5-7')\"),\n },\n async ({ file_path, pages }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { pages })\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n `범위: ${pages}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n ].filter(Boolean).join(\" | \")\n\n return {\n content: [{ type: \"text\", text: `[${meta}]\\n\\n${result.markdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_table ──────────────────────────────\n\nserver.tool(\n \"parse_table\",\n \"문서에서 N번째 테이블만 추출합니다 (0-based index). 테이블이 없거나 인덱스 범위를 초과하면 오류를 반환합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n table_index: z.number().int().min(0).describe(\"추출할 테이블 인덱스 (0부터 시작)\"),\n },\n async ({ file_path, table_index }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const tableBlocks = result.blocks.filter(b => b.type === \"table\" && b.table)\n if (tableBlocks.length === 0) {\n return {\n content: [{ type: \"text\", text: `문서에 테이블이 없습니다.` }],\n isError: true,\n }\n }\n\n if (table_index >= tableBlocks.length) {\n return {\n content: [{ type: \"text\", text: `테이블 인덱스 초과: ${table_index} (총 ${tableBlocks.length}개 테이블)` }],\n isError: true,\n }\n }\n\n const tableBlock = tableBlocks[table_index]\n const tableMarkdown = blocksToMarkdown([tableBlock])\n\n return {\n content: [{ type: \"text\", text: `[테이블 #${table_index} / 총 ${tableBlocks.length}개]\\n\\n${tableMarkdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: compare_documents ─────────────────────────\n\nserver.tool(\n \"compare_documents\",\n \"두 한국 문서 파일을 비교하여 추가/삭제/변경된 블록을 표시합니다. 신구대조표 생성에 활용됩니다. 크로스 포맷(HWP↔HWPX) 비교 가능.\",\n {\n file_path_a: z.string().min(1).describe(\"비교 원본 문서의 절대 경로\"),\n file_path_b: z.string().min(1).describe(\"비교 대상 문서의 절대 경로\"),\n },\n async ({ file_path_a, file_path_b }) => {\n try {\n const { buffer: bufA } = readValidatedFile(file_path_a)\n const { buffer: bufB } = readValidatedFile(file_path_b)\n\n const result = await compare(bufA, bufB)\n const { stats, diffs } = result\n\n const lines: string[] = [\n `## 문서 비교 결과`,\n `추가: ${stats.added} | 삭제: ${stats.removed} | 변경: ${stats.modified} | 동일: ${stats.unchanged}`,\n \"\",\n ]\n\n for (const d of diffs) {\n const prefix = d.type === \"added\" ? \"+\" : d.type === \"removed\" ? \"-\" : d.type === \"modified\" ? \"~\" : \" \"\n const text = d.after?.text || d.before?.text || (d.after?.table ? \"[테이블]\" : d.before?.table ? \"[테이블]\" : \"\")\n const sim = d.similarity !== undefined ? ` (${(d.similarity * 100).toFixed(0)}%)` : \"\"\n lines.push(`${prefix} ${text.substring(0, 200)}${sim}`)\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_form ───────────────────────────────\n\nserver.tool(\n \"parse_form\",\n \"한국 서식 문서에서 레이블-값 쌍을 구조화된 JSON으로 추출합니다. 양식/서식 문서에 최적화.\",\n {\n file_path: z.string().min(1).describe(\"서식 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패: ${result.error}` }],\n isError: true,\n }\n }\n\n const form = extractFormFields(result.blocks)\n return {\n content: [{ type: \"text\", text: JSON.stringify(form, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 서버 시작 ───────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\nmain().catch((err) => { console.error(err); process.exit(1) })\n"],"mappings":"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","names":[]}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createCliOcrProvider
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
createLoggerFromEnv
|
|
7
|
-
} from "./chunk-I6YC6ZGK.js";
|
|
4
|
+
} from "./chunk-Y4WFKJ5P.js";
|
|
8
5
|
import {
|
|
9
6
|
detectAvailableOcr,
|
|
10
|
-
getTesseractFallbackMessage,
|
|
11
7
|
validateOcrMode
|
|
12
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CLK4PNZ7.js";
|
|
9
|
+
import {
|
|
10
|
+
createLoggerFromEnv
|
|
11
|
+
} from "./chunk-I6YC6ZGK.js";
|
|
13
12
|
import "./chunk-ZWE3DS7E.js";
|
|
14
13
|
|
|
15
14
|
// src/ocr/resolve.ts
|
|
@@ -22,15 +21,6 @@ async function resolveOcrProvider(mode, warnings, concurrency, batchSize) {
|
|
|
22
21
|
}
|
|
23
22
|
if (mode !== "auto") {
|
|
24
23
|
validateOcrMode(mode);
|
|
25
|
-
if (mode === "tesseract") {
|
|
26
|
-
const { createTesseractProvider, createTesseractPoolProvider } = await import("./tesseract-provider-MNMZPSGF.js");
|
|
27
|
-
if (concurrency && concurrency > 1) {
|
|
28
|
-
logger.log({ level: "info", event: "done", message: "Tesseract pool provider \uC120\uD0DD", meta: { concurrency } });
|
|
29
|
-
return createTesseractPoolProvider(concurrency);
|
|
30
|
-
}
|
|
31
|
-
logger.log({ level: "info", event: "done", message: "Tesseract single provider \uC120\uD0DD" });
|
|
32
|
-
return createTesseractProvider();
|
|
33
|
-
}
|
|
34
24
|
if (mode === "gemini" || mode === "claude" || mode === "codex") {
|
|
35
25
|
const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import("./batch-provider-5BFJRKAZ.js");
|
|
36
26
|
const effectiveBatch = batchSize ?? DEFAULT_BATCH_SIZES[mode];
|
|
@@ -46,27 +36,16 @@ async function resolveOcrProvider(mode, warnings, concurrency, batchSize) {
|
|
|
46
36
|
}
|
|
47
37
|
const detected = detectAvailableOcr();
|
|
48
38
|
logger.log({ level: "info", event: "progress", message: "OCR auto \uAC10\uC9C0 \uACB0\uACFC", meta: { detected } });
|
|
49
|
-
if (detected
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
code: "OCR_CLI_FALLBACK"
|
|
54
|
-
});
|
|
55
|
-
} else {
|
|
56
|
-
warnings?.push({
|
|
57
|
-
message: `OCR: '${detected}' \uC0AC\uC6A9 \uC911 (codex CLI\uAC00 \uC5C6\uC5B4 fallback). \uB354 \uB098\uC740 \uD488\uC9C8\uC744 \uC704\uD574 codex CLI \uC124\uCE58\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.`,
|
|
58
|
-
code: "OCR_CLI_FALLBACK"
|
|
59
|
-
});
|
|
60
|
-
}
|
|
39
|
+
if (!detected) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"\uC0AC\uC6A9 \uAC00\uB2A5\uD55C OCR CLI\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC74C \uC911 \uD558\uB098\uB97C \uC124\uCE58\uD558\uC138\uC694:\n Codex CLI: npm install -g @openai/codex\n Claude CLI: npm install -g @anthropic-ai/claude-code\n Gemini CLI: https://ai.google.dev/gemini-api/docs/cli"
|
|
42
|
+
);
|
|
61
43
|
}
|
|
62
|
-
if (detected
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
logger.log({ level: "info", event: "done", message: "AUTO: Tesseract single provider \uC120\uD0DD" });
|
|
69
|
-
return createTesseractProvider();
|
|
44
|
+
if (detected !== "codex") {
|
|
45
|
+
warnings?.push({
|
|
46
|
+
message: `OCR: '${detected}' \uC0AC\uC6A9 \uC911 (codex CLI\uAC00 \uC5C6\uC5B4 fallback). \uB354 \uB098\uC740 \uD488\uC9C8\uC744 \uC704\uD574 codex CLI \uC124\uCE58\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.`,
|
|
47
|
+
code: "OCR_CLI_FALLBACK"
|
|
48
|
+
});
|
|
70
49
|
}
|
|
71
50
|
if (detected === "gemini" || detected === "codex" || detected === "claude") {
|
|
72
51
|
const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import("./batch-provider-5BFJRKAZ.js");
|
|
@@ -84,4 +63,4 @@ async function resolveOcrProvider(mode, warnings, concurrency, batchSize) {
|
|
|
84
63
|
export {
|
|
85
64
|
resolveOcrProvider
|
|
86
65
|
};
|
|
87
|
-
//# sourceMappingURL=resolve-
|
|
66
|
+
//# sourceMappingURL=resolve-XWYJYKKH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ocr/resolve.ts"],"sourcesContent":["/**\n * OCR 프로바이더 팩토리\n *\n * ocrMode에 따라 적절한 OcrProvider를 생성하여 반환.\n * - \"auto\": 설치된 CLI 자동 탐색 (codex → gemini → claude → ollama)\n * - 특정 CLI: 해당 CLI 사용 (미설치 시 에러)\n * - \"off\": 에러 throw\n */\n\nimport type { OcrMode, OcrProvider, ParseWarning, BatchOcrProvider } from \"../types.js\"\nimport { detectAvailableOcr, validateOcrMode } from \"./auto-detect.js\"\nimport { createCliOcrProvider } from \"./cli-provider.js\"\nimport { createLoggerFromEnv } from \"../logging/logger.js\"\n\n/**\n * ocrMode에 따라 OcrProvider를 생성.\n *\n * @param mode - OCR 모드\n * @param warnings - 경고 수집 배열 (fallback 발생 시 경고 추가)\n * @param concurrency - 병렬 처리 수 (기본: 1=순차)\n * @returns OcrProvider 함수\n * @throws mode=\"off\"이거나 지정 CLI 미설치 시 Error\n */\nexport async function resolveOcrProvider(\n mode: OcrMode,\n warnings?: ParseWarning[],\n concurrency?: number,\n batchSize?: number\n): Promise<OcrProvider | BatchOcrProvider> {\n const logger = createLoggerFromEnv().child({ component: \"ocr/resolve.ts\", stage: \"ocr\" })\n logger.log({ level: \"debug\", event: \"start\", message: \"OCR provider resolve 시작\", meta: { mode, concurrency, batchSize } })\n\n if (mode === \"off\") {\n logger.log({ level: \"warn\", event: \"error\", message: \"OCR 비활성화 모드 요청\" })\n throw new Error(\"OCR이 비활성화되어 있습니다 (--ocr off).\")\n }\n\n // ── 수동 지정 모드 ──────────────────────────────────\n if (mode !== \"auto\") {\n validateOcrMode(mode)\n\n // gemini/claude/codex: 배치 크기 > 1이면 배치 프로바이더 사용\n if (mode === \"gemini\" || mode === \"claude\" || mode === \"codex\") {\n const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import(\"./batch-provider.js\")\n const effectiveBatch = batchSize ?? DEFAULT_BATCH_SIZES[mode]\n if (effectiveBatch > 1) {\n logger.log({ level: \"info\", event: \"done\", message: \"Batch CLI provider 선택\", meta: { mode, batchSize: effectiveBatch } })\n return createBatchCliProvider(mode, effectiveBatch)\n }\n logger.log({ level: \"info\", event: \"done\", message: \"CLI provider 선택\", meta: { mode } })\n return createCliOcrProvider(mode)\n }\n\n logger.log({ level: \"info\", event: \"done\", message: \"CLI provider 선택\", meta: { mode } })\n return createCliOcrProvider(mode)\n }\n\n // ── 자동 탐색 모드 ───────────────────────────────────\n const detected = detectAvailableOcr()\n logger.log({ level: \"info\", event: \"progress\", message: \"OCR auto 감지 결과\", meta: { detected } })\n\n if (!detected) {\n throw new Error(\n \"사용 가능한 OCR CLI가 없습니다. 다음 중 하나를 설치하세요:\\n\" +\n \" Codex CLI: npm install -g @openai/codex\\n\" +\n \" Claude CLI: npm install -g @anthropic-ai/claude-code\\n\" +\n \" Gemini CLI: https://ai.google.dev/gemini-api/docs/cli\"\n )\n }\n\n if (detected !== \"codex\") {\n warnings?.push({\n message: `OCR: '${detected}' 사용 중 (codex CLI가 없어 fallback). 더 나은 품질을 위해 codex CLI 설치를 권장합니다.`,\n code: \"OCR_CLI_FALLBACK\",\n })\n }\n\n // gemini/claude/codex: 배치 크기 > 1이면 배치 프로바이더 사용\n if (detected === \"gemini\" || detected === \"codex\" || detected === \"claude\") {\n const { createBatchCliProvider, DEFAULT_BATCH_SIZES } = await import(\"./batch-provider.js\")\n const effectiveBatch = batchSize ?? DEFAULT_BATCH_SIZES[detected]\n if (effectiveBatch > 1) {\n logger.log({ level: \"info\", event: \"done\", message: \"AUTO: Batch CLI provider 선택\", meta: { mode: detected, batchSize: effectiveBatch } })\n return createBatchCliProvider(detected, effectiveBatch)\n }\n logger.log({ level: \"info\", event: \"done\", message: \"AUTO: CLI provider 선택\", meta: { mode: detected } })\n return createCliOcrProvider(detected)\n }\n\n logger.log({ level: \"info\", event: \"done\", message: \"AUTO: CLI provider 선택\", meta: { mode: detected } })\n return createCliOcrProvider(detected)\n}\n"],"mappings":";;;;;;;;;;;;;;AAuBA,eAAsB,mBACpB,MACA,UACA,aACA,WACyC;AACzC,QAAM,SAAS,oBAAoB,EAAE,MAAM,EAAE,WAAW,kBAAkB,OAAO,MAAM,CAAC;AACxF,SAAO,IAAI,EAAE,OAAO,SAAS,OAAO,SAAS,SAAS,qCAA2B,MAAM,EAAE,MAAM,aAAa,UAAU,EAAE,CAAC;AAEzH,MAAI,SAAS,OAAO;AAClB,WAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,SAAS,SAAS,yDAAiB,CAAC;AACvE,UAAM,IAAI,MAAM,sFAA+B;AAAA,EACjD;AAGA,MAAI,SAAS,QAAQ;AACnB,oBAAgB,IAAI;AAGpB,QAAI,SAAS,YAAY,SAAS,YAAY,SAAS,SAAS;AAC9D,YAAM,EAAE,wBAAwB,oBAAoB,IAAI,MAAM,OAAO,8BAAqB;AAC1F,YAAM,iBAAiB,aAAa,oBAAoB,IAAI;AAC5D,UAAI,iBAAiB,GAAG;AACtB,eAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,mCAAyB,MAAM,EAAE,MAAM,WAAW,eAAe,EAAE,CAAC;AACxH,eAAO,uBAAuB,MAAM,cAAc;AAAA,MACpD;AACA,aAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,6BAAmB,MAAM,EAAE,KAAK,EAAE,CAAC;AACvF,aAAO,qBAAqB,IAAI;AAAA,IAClC;AAEA,WAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,6BAAmB,MAAM,EAAE,KAAK,EAAE,CAAC;AACvF,WAAO,qBAAqB,IAAI;AAAA,EAClC;AAGA,QAAM,WAAW,mBAAmB;AACpC,SAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,YAAY,SAAS,sCAAkB,MAAM,EAAE,SAAS,EAAE,CAAC;AAE9F,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,IAIF;AAAA,EACF;AAEA,MAAI,aAAa,SAAS;AACxB,cAAU,KAAK;AAAA,MACb,SAAS,SAAS,QAAQ;AAAA,MAC1B,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAGA,MAAI,aAAa,YAAY,aAAa,WAAW,aAAa,UAAU;AAC1E,UAAM,EAAE,wBAAwB,oBAAoB,IAAI,MAAM,OAAO,8BAAqB;AAC1F,UAAM,iBAAiB,aAAa,oBAAoB,QAAQ;AAChE,QAAI,iBAAiB,GAAG;AACtB,aAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,yCAA+B,MAAM,EAAE,MAAM,UAAU,WAAW,eAAe,EAAE,CAAC;AACxI,aAAO,uBAAuB,UAAU,cAAc;AAAA,IACxD;AACA,WAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,mCAAyB,MAAM,EAAE,MAAM,SAAS,EAAE,CAAC;AACvG,WAAO,qBAAqB,QAAQ;AAAA,EACtC;AAEA,SAAO,IAAI,EAAE,OAAO,QAAQ,OAAO,QAAQ,SAAS,mCAAyB,MAAM,EAAE,MAAM,SAAS,EAAE,CAAC;AACvG,SAAO,qBAAqB,QAAQ;AACtC;","names":[]}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/setup/doctor.ts
|
|
21
|
+
var doctor_exports = {};
|
|
22
|
+
__export(doctor_exports, {
|
|
23
|
+
runDoctor: () => runDoctor
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(doctor_exports);
|
|
26
|
+
|
|
27
|
+
// src/setup/detect.ts
|
|
28
|
+
var import_node_child_process = require("child_process");
|
|
29
|
+
var import_node_fs = require("fs");
|
|
30
|
+
var import_node_path = require("path");
|
|
31
|
+
var isWin = process.platform === "win32";
|
|
32
|
+
function which(cmd) {
|
|
33
|
+
const finder = isWin ? "where" : "which";
|
|
34
|
+
const r = (0, import_node_child_process.spawnSync)(finder, [cmd], { encoding: "utf8", shell: isWin });
|
|
35
|
+
if (r.status !== 0) return void 0;
|
|
36
|
+
return r.stdout.split(/\r?\n/).find(Boolean)?.trim();
|
|
37
|
+
}
|
|
38
|
+
function resolveCmd(cmd, envKey) {
|
|
39
|
+
const override = process.env[envKey];
|
|
40
|
+
if (override) return override;
|
|
41
|
+
const found = which(cmd);
|
|
42
|
+
if (found) return found;
|
|
43
|
+
if (isWin) return findWinCandidate(cmd);
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
function findWinCandidate(cmd) {
|
|
47
|
+
const exe = cmd.endsWith(".exe") ? cmd : `${cmd}.exe`;
|
|
48
|
+
const userProfile = process.env["USERPROFILE"] ?? "C:\\Users\\Default";
|
|
49
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? (0, import_node_path.join)(userProfile, "AppData", "Local");
|
|
50
|
+
const programFiles = process.env["ProgramFiles"] ?? "C:\\Program Files";
|
|
51
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
52
|
+
const isPopplerExe = ["pdftoppm.exe", "pdfinfo.exe", "pdftotext.exe"].includes(exe);
|
|
53
|
+
const isSoffice = exe === "soffice.exe";
|
|
54
|
+
if (isPopplerExe) return findPopplerWin(exe, { userProfile, localAppData, programFiles, programFilesX86 });
|
|
55
|
+
if (isSoffice) return findSofficeWin({ programFiles, programFilesX86, localAppData });
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
function findPopplerWin(exe, p) {
|
|
59
|
+
const scoopPath = (0, import_node_path.join)(p.userProfile, "scoop", "apps", "poppler", "current", "bin", exe);
|
|
60
|
+
if ((0, import_node_fs.existsSync)(scoopPath)) return scoopPath;
|
|
61
|
+
const wingetBase = (0, import_node_path.join)(p.localAppData, "Microsoft", "WinGet", "Packages");
|
|
62
|
+
const wingetResult = globFirst(wingetBase, ["oschwartz10612.Poppler_*"], ["poppler-*", "Library", "bin", exe]);
|
|
63
|
+
if (wingetResult) return wingetResult;
|
|
64
|
+
const chocoBase = (0, import_node_path.join)("C:", "ProgramData", "chocolatey", "lib", "poppler", "tools");
|
|
65
|
+
const chocoResult = globFirst(chocoBase, ["poppler-*"], ["bin", exe]);
|
|
66
|
+
if (chocoResult) return chocoResult;
|
|
67
|
+
for (const base of [
|
|
68
|
+
(0, import_node_path.join)("C:", "poppler", "bin", exe),
|
|
69
|
+
(0, import_node_path.join)("C:", "tools", "poppler", "bin", exe),
|
|
70
|
+
(0, import_node_path.join)(p.programFiles, "poppler", "bin", exe),
|
|
71
|
+
(0, import_node_path.join)(p.programFilesX86, "poppler", "bin", exe),
|
|
72
|
+
(0, import_node_path.join)(p.localAppData, "Programs", "poppler", "bin", exe)
|
|
73
|
+
]) {
|
|
74
|
+
if ((0, import_node_fs.existsSync)(base)) return base;
|
|
75
|
+
}
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
function findSofficeWin(p) {
|
|
79
|
+
for (const candidate of [
|
|
80
|
+
(0, import_node_path.join)(p.programFiles, "LibreOffice", "program", "soffice.exe"),
|
|
81
|
+
(0, import_node_path.join)(p.programFilesX86, "LibreOffice", "program", "soffice.exe"),
|
|
82
|
+
(0, import_node_path.join)(p.localAppData, "Programs", "LibreOffice", "program", "soffice.exe"),
|
|
83
|
+
// winget 설치는 Program Files와 동일
|
|
84
|
+
(0, import_node_path.join)(p.programFiles, "LibreOffice 24", "program", "soffice.exe"),
|
|
85
|
+
(0, import_node_path.join)(p.programFiles, "LibreOffice 25", "program", "soffice.exe")
|
|
86
|
+
]) {
|
|
87
|
+
if ((0, import_node_fs.existsSync)(candidate)) return candidate;
|
|
88
|
+
}
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
function globFirst(base, wildcardSegments, rest) {
|
|
92
|
+
if (!(0, import_node_fs.existsSync)(base)) return void 0;
|
|
93
|
+
let current = base;
|
|
94
|
+
for (const seg of wildcardSegments) {
|
|
95
|
+
if (!seg.includes("*")) {
|
|
96
|
+
current = (0, import_node_path.join)(current, seg);
|
|
97
|
+
if (!(0, import_node_fs.existsSync)(current)) return void 0;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const { readdirSync } = require("fs");
|
|
102
|
+
const entries = readdirSync(current);
|
|
103
|
+
const pattern = seg.replace(/\*/g, ".*");
|
|
104
|
+
const re = new RegExp(`^${pattern}$`, "i");
|
|
105
|
+
const match = entries.find((e) => re.test(e));
|
|
106
|
+
if (!match) return void 0;
|
|
107
|
+
current = (0, import_node_path.join)(current, match);
|
|
108
|
+
} catch {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const final = (0, import_node_path.join)(current, ...rest);
|
|
113
|
+
return (0, import_node_fs.existsSync)(final) ? final : void 0;
|
|
114
|
+
}
|
|
115
|
+
function probe(cmd, envKey, versionArgs = ["--version"]) {
|
|
116
|
+
const resolvedPath = resolveCmd(cmd, envKey);
|
|
117
|
+
if (!resolvedPath) return { name: cmd, found: false };
|
|
118
|
+
const v = (0, import_node_child_process.spawnSync)(resolvedPath, versionArgs, { encoding: "utf8", shell: isWin });
|
|
119
|
+
const versionLine = (v.stdout || v.stderr).split(/\r?\n/)[0]?.trim();
|
|
120
|
+
return { name: cmd, found: true, path: resolvedPath, version: versionLine };
|
|
121
|
+
}
|
|
122
|
+
function probeOcrToolchain() {
|
|
123
|
+
return {
|
|
124
|
+
// poppler는 --version이 비표준이므로 -v 사용
|
|
125
|
+
pdftoppm: probe("pdftoppm", "KORDOC_PDFTOPPM_PATH", ["-v"]),
|
|
126
|
+
pdfinfo: probe("pdfinfo", "KORDOC_PDFINFO_PATH", ["-v"]),
|
|
127
|
+
pdftotext: probe("pdftotext", "KORDOC_PDFTOTEXT_PATH", ["-v"]),
|
|
128
|
+
soffice: probe("soffice", "KORDOC_SOFFICE_PATH", ["--version"])
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function isWSL() {
|
|
132
|
+
if (process.platform !== "linux") return false;
|
|
133
|
+
try {
|
|
134
|
+
const { readFileSync } = require("fs");
|
|
135
|
+
const version = readFileSync("/proc/version", "utf8");
|
|
136
|
+
return /microsoft|wsl/i.test(version);
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/setup/pm.ts
|
|
143
|
+
var import_node_child_process2 = require("child_process");
|
|
144
|
+
var isWin2 = process.platform === "win32";
|
|
145
|
+
function which2(cmd) {
|
|
146
|
+
const finder = isWin2 ? "where" : "which";
|
|
147
|
+
const r = (0, import_node_child_process2.spawnSync)(finder, [cmd], { encoding: "utf8", shell: isWin2 });
|
|
148
|
+
return r.status === 0;
|
|
149
|
+
}
|
|
150
|
+
function detectPackageManager() {
|
|
151
|
+
if (process.platform === "darwin") {
|
|
152
|
+
return which2("brew") ? "brew" : "unknown";
|
|
153
|
+
}
|
|
154
|
+
if (isWin2) {
|
|
155
|
+
if (which2("winget")) return "winget";
|
|
156
|
+
if (which2("scoop")) return "scoop";
|
|
157
|
+
if (which2("choco")) return "choco";
|
|
158
|
+
return "unknown";
|
|
159
|
+
}
|
|
160
|
+
if (which2("apt-get") || which2("apt")) return "apt";
|
|
161
|
+
if (which2("dnf")) return "dnf";
|
|
162
|
+
if (which2("yum")) return "yum";
|
|
163
|
+
if (which2("pacman")) return "pacman";
|
|
164
|
+
if (which2("zypper")) return "zypper";
|
|
165
|
+
if (which2("apk")) return "apk";
|
|
166
|
+
return "unknown";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/setup/install-commands.ts
|
|
170
|
+
var POPPLER_RECIPES = {
|
|
171
|
+
brew: { pm: "brew", cmd: "brew install poppler", needsSudo: false },
|
|
172
|
+
winget: { pm: "winget", cmd: "winget install --id oschwartz10612.Poppler", needsSudo: false, notes: "\uC2E4\uC874 \uD655\uC778\uB428 (winget-pkgs 25.07.0-0)" },
|
|
173
|
+
scoop: { pm: "scoop", cmd: "scoop install poppler", needsSudo: false, notes: "main \uBC84\uD0B7 \uD3EC\uD568, PATH \uC790\uB3D9 \uB4F1\uB85D" },
|
|
174
|
+
choco: { pm: "choco", cmd: "choco install poppler -y", needsSudo: true, notes: "\uAD00\uB9AC\uC790 PowerShell \uD544\uC694" },
|
|
175
|
+
apt: { pm: "apt", cmd: "sudo apt-get install -y poppler-utils", needsSudo: true },
|
|
176
|
+
dnf: { pm: "dnf", cmd: "sudo dnf install -y poppler-utils", needsSudo: true },
|
|
177
|
+
yum: { pm: "yum", cmd: "sudo yum install -y poppler-utils", needsSudo: true },
|
|
178
|
+
pacman: { pm: "pacman", cmd: "sudo pacman -S --noconfirm poppler", needsSudo: true },
|
|
179
|
+
zypper: { pm: "zypper", cmd: "sudo zypper install -y poppler-tools", needsSudo: true },
|
|
180
|
+
apk: { pm: "apk", cmd: "apk add --no-cache poppler-utils", needsSudo: false, notes: "Alpine/Docker \uCE5C\uD654" },
|
|
181
|
+
unknown: null
|
|
182
|
+
};
|
|
183
|
+
var LIBREOFFICE_RECIPES = {
|
|
184
|
+
brew: { pm: "brew", cmd: "brew install --cask libreoffice", needsSudo: false },
|
|
185
|
+
winget: { pm: "winget", cmd: "winget install --id TheDocumentFoundation.LibreOffice", needsSudo: false, notes: "\uC2E4\uC874 \uD655\uC778\uB428" },
|
|
186
|
+
scoop: { pm: "scoop", cmd: "scoop bucket add extras && scoop install libreoffice", needsSudo: false },
|
|
187
|
+
choco: { pm: "choco", cmd: "choco install libreoffice-fresh -y", needsSudo: true, notes: "\uAD00\uB9AC\uC790 PowerShell \uD544\uC694" },
|
|
188
|
+
apt: { pm: "apt", cmd: "sudo apt-get install -y libreoffice", needsSudo: true },
|
|
189
|
+
dnf: { pm: "dnf", cmd: "sudo dnf install -y libreoffice", needsSudo: true },
|
|
190
|
+
yum: { pm: "yum", cmd: "sudo yum install -y libreoffice", needsSudo: true },
|
|
191
|
+
pacman: { pm: "pacman", cmd: "sudo pacman -S --noconfirm libreoffice-still", needsSudo: true },
|
|
192
|
+
zypper: { pm: "zypper", cmd: "sudo zypper install -y libreoffice", needsSudo: true },
|
|
193
|
+
apk: { pm: "apk", cmd: "apk add --no-cache libreoffice", needsSudo: false, notes: "Alpine \uBBF8\uC9C0\uC6D0 \uAC00\uB2A5 \u2014 node:20-slim(Debian) \uAD8C\uC7A5" },
|
|
194
|
+
unknown: null
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/setup/doctor.ts
|
|
198
|
+
var hr = "\u2500".repeat(45);
|
|
199
|
+
function platform() {
|
|
200
|
+
const arch = process.arch;
|
|
201
|
+
if (isWSL()) return `linux/WSL (arch: ${arch})`;
|
|
202
|
+
return `${process.platform} (arch: ${arch})`;
|
|
203
|
+
}
|
|
204
|
+
function icon(found) {
|
|
205
|
+
return found ? "\u2705" : "\u274C";
|
|
206
|
+
}
|
|
207
|
+
function pad(s, n) {
|
|
208
|
+
return s.padEnd(n);
|
|
209
|
+
}
|
|
210
|
+
function formatTool(name, status) {
|
|
211
|
+
const ico = icon(status.found);
|
|
212
|
+
if (!status.found) return ` ${pad(name, 10)} ${ico} \uCC3E\uC744 \uC218 \uC5C6\uC74C`;
|
|
213
|
+
const ver = status.version ? ` (${status.version})` : "";
|
|
214
|
+
return ` ${pad(name, 10)} ${ico} ${status.path}${ver}`;
|
|
215
|
+
}
|
|
216
|
+
function runDoctor(opts = {}) {
|
|
217
|
+
const toolchain = probeOcrToolchain();
|
|
218
|
+
const pm = detectPackageManager();
|
|
219
|
+
const popplerOk = toolchain.pdftoppm.found && toolchain.pdfinfo.found;
|
|
220
|
+
const sofficeOk = toolchain.soffice.found;
|
|
221
|
+
const missing = [];
|
|
222
|
+
if (!popplerOk) missing.push("poppler");
|
|
223
|
+
if (!sofficeOk) missing.push("libreoffice");
|
|
224
|
+
const out = opts.postinstall ? process.stderr : process.stdout;
|
|
225
|
+
out.write(`${hr}
|
|
226
|
+
`);
|
|
227
|
+
out.write(`kordoc OCR \uC758\uC874\uC131 \uC810\uAC80
|
|
228
|
+
`);
|
|
229
|
+
out.write(`${hr}
|
|
230
|
+
`);
|
|
231
|
+
out.write(`\uD50C\uB7AB\uD3FC: ${platform()}
|
|
232
|
+
`);
|
|
233
|
+
out.write(`\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm === "unknown" ? "\uAC10\uC9C0 \uC2E4\uD328" : pm}
|
|
234
|
+
`);
|
|
235
|
+
out.write(`Node.js: ${process.version}
|
|
236
|
+
`);
|
|
237
|
+
out.write(`
|
|
238
|
+
`);
|
|
239
|
+
out.write(`[OCR \uD30C\uC774\uD504\uB77C\uC778 \uC758\uC874\uC131]
|
|
240
|
+
`);
|
|
241
|
+
out.write(`${formatTool("pdftoppm", toolchain.pdftoppm)}
|
|
242
|
+
`);
|
|
243
|
+
out.write(`${formatTool("pdfinfo", toolchain.pdfinfo)}
|
|
244
|
+
`);
|
|
245
|
+
out.write(`${formatTool("pdftotext", toolchain.pdftotext)}
|
|
246
|
+
`);
|
|
247
|
+
out.write(`${formatTool("soffice", toolchain.soffice)}
|
|
248
|
+
`);
|
|
249
|
+
if (missing.length > 0) {
|
|
250
|
+
out.write(`
|
|
251
|
+
\uAD8C\uC7A5 \uC124\uCE58 \uBA85\uB839:
|
|
252
|
+
`);
|
|
253
|
+
if (!popplerOk) {
|
|
254
|
+
const recipe = POPPLER_RECIPES[pm];
|
|
255
|
+
if (recipe) {
|
|
256
|
+
out.write(` # poppler (pdftoppm/pdfinfo)
|
|
257
|
+
`);
|
|
258
|
+
out.write(` ${recipe.cmd}
|
|
259
|
+
`);
|
|
260
|
+
if (recipe.notes) out.write(` # ${recipe.notes}
|
|
261
|
+
`);
|
|
262
|
+
} else {
|
|
263
|
+
out.write(` poppler: OS\uC5D0 \uB9DE\uB294 \uBC29\uBC95\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694 (README \uCC38\uACE0)
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (!sofficeOk) {
|
|
268
|
+
const recipe = LIBREOFFICE_RECIPES[pm];
|
|
269
|
+
if (recipe) {
|
|
270
|
+
out.write(`
|
|
271
|
+
# LibreOffice (soffice)
|
|
272
|
+
`);
|
|
273
|
+
out.write(` ${recipe.cmd}
|
|
274
|
+
`);
|
|
275
|
+
if (recipe.notes) out.write(` # ${recipe.notes}
|
|
276
|
+
`);
|
|
277
|
+
} else {
|
|
278
|
+
out.write(` LibreOffice: OS\uC5D0 \uB9DE\uB294 \uBC29\uBC95\uC73C\uB85C \uC124\uCE58\uD558\uC138\uC694 (README \uCC38\uACE0)
|
|
279
|
+
`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (process.platform === "win32") {
|
|
283
|
+
out.write(`
|
|
284
|
+
\u26A0\uFE0F \uC124\uCE58 \uD6C4 \uC0C8 \uD130\uBBF8\uB110\uC744 \uC5F4\uAC70\uB098 PATH\uB97C \uAC31\uC2E0\uD574\uC57C \uD569\uB2C8\uB2E4.
|
|
285
|
+
`);
|
|
286
|
+
out.write(` PowerShell: $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
out.write(`
|
|
290
|
+
\uD658\uACBD\uBCC0\uC218\uB85C \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD560 \uC218\uB3C4 \uC788\uC2B5\uB2C8\uB2E4:
|
|
291
|
+
`);
|
|
292
|
+
out.write(` KORDOC_PDFTOPPM_PATH, KORDOC_PDFINFO_PATH, KORDOC_SOFFICE_PATH
|
|
293
|
+
`);
|
|
294
|
+
out.write(`
|
|
295
|
+
\uC790\uC138\uD55C \uC810\uAC80: npx kordoc doctor
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
out.write(`${hr}
|
|
299
|
+
`);
|
|
300
|
+
const summary = missing.length === 0 ? "\uBAA8\uB4E0 \uC758\uC874\uC131\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4 \u2705" : `${missing.length}\uAC1C \uACB0\uC190 \u2014 OCR \uBAA8\uB4DC \uC0AC\uC6A9 \uC2DC \uCC28\uB2E8\uB429\uB2C8\uB2E4 (${missing.join(", ")})`;
|
|
301
|
+
out.write(`\uC694\uC57D: ${summary}
|
|
302
|
+
`);
|
|
303
|
+
return { ok: missing.length === 0, missing, toolchain, pm };
|
|
304
|
+
}
|
|
305
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
306
|
+
0 && (module.exports = {
|
|
307
|
+
runDoctor
|
|
308
|
+
});
|