/** * ============================================================================ * 프로젝트: 터널 단면 3D 파라메트릭 모델러 (Tunnel Modeler 3D) * 개발자: m12205.tdc.hanmacgroup@gmail.com * 저작권: Copyright (c) 2026 m12205.tdc.hanmacgroup@gmail.com. All rights reserved. * 설명: DWG/DXF 변환 및 정적 웹 서비스 로컬 서버 * ============================================================================ */ const http = require('http'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); const PORT = Number(process.env.PORT || 4180); const ROOT = __dirname; const PARENT_ROOT = path.resolve(__dirname, '..'); function findConverter() { const candidates = [ process.env.ODA_FILE_CONVERTER, 'C:\\Program Files\\ODA\\ODAFileConverter\\ODAFileConverter.exe', 'C:\\Program Files\\ODA\\ODAFileConverter 27.9.0\\ODAFileConverter.exe', 'C:\\Program Files\\ODAFileConverter\\ODAFileConverter.exe', 'C:\\Program Files\\Open Design Alliance\\ODAFileConverter.exe' ].filter(Boolean); for (const candidate of candidates) if (fs.existsSync(candidate)) return candidate; for (const root of [process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter(Boolean)) { for (const folder of ['ODA', 'ODA File Converter', 'Open Design Alliance']) { const parent = path.join(root, folder); if (!fs.existsSync(parent)) continue; for (const entry of fs.readdirSync(parent, { withFileTypes: true })) { if (!entry.isDirectory() || !/oda.*converter/i.test(entry.name)) continue; const candidate = path.join(parent, entry.name, 'ODAFileConverter.exe'); if (fs.existsSync(candidate)) return candidate; } } } return null; } function send(res, status, body, type = 'application/json; charset=utf-8') { res.writeHead(status, { 'Content-Type': type, 'Cache-Control': 'no-store' }); res.end(typeof body === 'string' ? body : JSON.stringify(body)); } const server = http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); if (req.method === 'GET' && url.pathname === '/api/status') { const converter = findConverter(); return send(res, 200, { converterAvailable: Boolean(converter), converterPath: converter }); } if (req.method === 'GET' && url.pathname === '/api/sample') { const sampleCandidates = [ path.join(ROOT, 'sample.dxf'), path.join(PARENT_ROOT, 'sejong_seoun1.dxf'), path.join(PARENT_ROOT, 'Tunnel3D', 'sejong_seoun1.dxf'), path.join(PARENT_ROOT, 'Tunnel3D-inspect', 'out', 'source.dxf') ]; const sample = sampleCandidates.find(p => fs.existsSync(p)); if (!sample) return send(res, 404, { error: '예제 DXF 파일을 찾지 못했습니다.' }); res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' }); return fs.createReadStream(sample).pipe(res); } if (req.method === 'POST' && url.pathname === '/api/convert') { const converter = findConverter(); if (!converter) { return send(res, 503, { error: 'ODA File Converter를 찾지 못했습니다. DXF 파일을 사용하거나 ODA File Converter를 설치해 주세요.' }); } const original = path.basename(decodeURIComponent(req.headers['x-filename'] || 'drawing.dwg')); if (!/\.dwg$/i.test(original)) { return send(res, 400, { error: 'DWG 파일만 변환할 수 있습니다.' }); } const work = fs.mkdtempSync(path.join(os.tmpdir(), 'tunnel-modeler-3d-')); const input = path.join(work, 'in'); const output = path.join(work, 'out'); fs.mkdirSync(input); fs.mkdirSync(output); const source = path.join(input, 'source.dwg'); const outFile = path.join(output, 'source.dxf'); const stream = fs.createWriteStream(source); req.pipe(stream); stream.on('error', () => { fs.rmSync(work, { recursive: true, force: true }); send(res, 500, { error: 'DWG 파일을 임시 작업 폴더에 저장하지 못했습니다.' }); }); stream.on('finish', () => { const child = spawn(converter, [input, output, 'ACAD2018', 'DXF', '0', '0', '*.DWG'], { windowsHide: true }); let stderr = ''; child.stderr.on('data', data => { stderr += data.toString(); }); child.on('error', error => { fs.rmSync(work, { recursive: true, force: true }); send(res, 500, { error: `DWG 변환기를 실행하지 못했습니다: ${error.message}` }); }); child.on('close', code => { if (!fs.existsSync(outFile)) { fs.rmSync(work, { recursive: true, force: true }); return send(res, 422, { error: `DXF 변환 결과가 없습니다 (종료 코드 ${code}). ${stderr.slice(0, 500)}` }); } const dxf = fs.readFileSync(outFile, 'utf8'); fs.rmSync(work, { recursive: true, force: true }); send(res, 200, dxf, 'text/plain; charset=utf-8'); }); }); return; } if (req.method !== 'GET') return send(res, 405, { error: '허용되지 않은 요청입니다.' }); const relative = url.pathname === '/' ? 'index.html' : decodeURIComponent(url.pathname).replace(/^\/+/, ''); const file = path.resolve(ROOT, relative); if (!file.startsWith(ROOT + path.sep) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) { return send(res, 404, '파일을 찾을 수 없습니다.', 'text/plain; charset=utf-8'); } const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.dxf': 'text/plain; charset=utf-8', '.wasm': 'application/wasm' }; res.writeHead(200, { 'Content-Type': types[path.extname(file).toLowerCase()] || 'application/octet-stream', 'Cache-Control': 'no-store' }); fs.createReadStream(file).pipe(res); }); server.listen(PORT, '127.0.0.1', () => { console.log(`====================================================`); console.log(` 터널 단면 3D 모델러 실행 중: http://127.0.0.1:${PORT}`); console.log(` ODA File Converter: ${findConverter() ? '준비됨 (DWG 직독 가능)' : '미설치 (DXF 사용 가능)'}`); console.log(`====================================================`); });