Files
Tunnel3D/server.js
T

137 lines
6.2 KiB
JavaScript

/**
* ============================================================================
* 프로젝트: 터널 단면 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(`====================================================`);
});