Files
hyungi_document_server/services/kordoc/server.js
Hyungi Ahn 2dfb05e653 fix: convert kordoc service to ESM (kordoc requires ESM import)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:38:34 +09:00

140 lines
3.8 KiB
JavaScript

/**
* kordoc 마이크로서비스 — HWP/HWPX/PDF/텍스트 → Markdown 변환 API
*/
import express from 'express';
import fs from 'fs';
import path from 'path';
import { parse, detectFormat } from 'kordoc';
const app = express();
const PORT = 3100;
const PARSE_TIMEOUT_MS = 30000;
app.use(express.json({ limit: '500mb' }));
// 헬스체크
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'kordoc' });
});
// 지원 포맷 목록
const TEXT_FORMATS = new Set(['.md', '.txt', '.csv', '.json', '.xml', '.html']);
const PARSEABLE_FORMATS = new Set(['.hwp', '.hwpx', '.pdf']);
const IMAGE_FORMATS = new Set(['.jpg', '.jpeg', '.png', '.tiff', '.tif', '.bmp', '.gif']);
/**
* 문서 파싱 — 파일 경로를 받아 Markdown으로 변환
*/
app.post('/parse', async (req, res) => {
const startTime = Date.now();
try {
const { filePath } = req.body;
if (!filePath) {
return res.status(400).json({ error: 'filePath is required' });
}
// 파일 존재 확인
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: `파일을 찾을 수 없습니다: ${filePath}` });
}
const ext = path.extname(filePath).toLowerCase();
const stat = fs.statSync(filePath);
// 100MB 초과 파일 거부
if (stat.size > 100 * 1024 * 1024) {
return res.status(413).json({ error: '파일 크기 100MB 초과' });
}
// 텍스트 파일 — 직접 읽기
if (TEXT_FORMATS.has(ext)) {
const text = fs.readFileSync(filePath, 'utf-8');
return res.json({
success: true,
markdown: text,
metadata: { format: ext.slice(1), fileSize: stat.size },
format: ext.slice(1),
requires_ocr: false,
});
}
// 이미지 파일 — OCR 필요 플래그
if (IMAGE_FORMATS.has(ext)) {
return res.json({
success: true,
markdown: '',
metadata: { format: ext.slice(1), fileSize: stat.size },
format: ext.slice(1),
requires_ocr: true,
});
}
// HWP/HWPX/PDF — kordoc 파싱
if (PARSEABLE_FORMATS.has(ext)) {
const buffer = fs.readFileSync(filePath);
// 타임아웃 처리
const result = await Promise.race([
parse(buffer),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('파싱 타임아웃 (30초)')), PARSE_TIMEOUT_MS)
),
]);
if (!result.success) {
return res.status(422).json({
error: '문서 파싱 실패',
warnings: result.warnings || [],
});
}
return res.json({
success: true,
markdown: result.markdown || '',
metadata: {
...(result.metadata || {}),
format: ext.slice(1),
fileSize: stat.size,
parseTime: Date.now() - startTime,
},
format: ext.slice(1),
requires_ocr: false,
});
}
// 미지원 포맷
return res.json({
success: true,
markdown: '',
metadata: { format: ext.slice(1), fileSize: stat.size },
format: ext.slice(1),
requires_ocr: false,
unsupported: true,
});
} catch (err) {
console.error(`[ERROR] /parse: ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// 문서 비교
app.post('/compare', async (req, res) => {
try {
const { filePathA, filePathB } = req.body;
if (!filePathA || !filePathB) {
return res.status(400).json({ error: 'filePathA and filePathB are required' });
}
// TODO: Phase 2에서 kordoc compare 구현
return res.json({ diffs: [], message: 'compare는 Phase 2에서 구현 예정' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(PORT, () => {
console.log(`kordoc-service listening on port ${PORT}`);
});