Files
Pier_v1/도면작성/dwg-server.ts
T
ypshimandClaude Haiku 5.5 d740406a8c fix(ground): WSL에서 지반 생성 Python·DWG 변환 실행 경로 해결
- 공통 resolvePython: Windows venv → .venv/bin/python → PIERZAINER_PYTHON → python3/python
- 지반 생성·수량산출 Excel·DWG 출력 서버가 같은 해석기 사용, ENOENT 안내 문구 보강
- WSL에서 번들 dwg2dxf.exe 호출 시 wslpath -w로 경로 변환, .exe 실행 권한 부여
- 실제 운곡교 DWG+Excel로 엔진/HTTP 경로 생성 확인(교각16·지형6,545점)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-09 14:03:16 +09:00

42 lines
3.6 KiB
TypeScript

import type { Plugin } from 'vite';
import type { IncomingMessage,ServerResponse } from 'node:http';
import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { mkdir,readFile,writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import { resolvePython } from '../pierzainer_ground/python-runtime.ts';
export const DWG_API='/__pierzainer/general-arrangement/dwg';
/** A narrow local export route; no project state or existing drawing-extractor endpoints are changed. */
export function generalArrangementDwgPlugin(root:string):Plugin{
let active=0;
const handler=async(req:IncomingMessage,res:ServerResponse)=>{
const error=(status:number,message:string)=>{res.statusCode=status;res.setHeader('Content-Type','application/json; charset=utf-8');res.end(JSON.stringify({error:message}));};
if(req.method!=='POST'){error(405,'DWG 출력은 POST 요청만 지원합니다.');return;}
try{if(req.headers.origin&&new URL(req.headers.origin).host!==req.headers.host){error(403,'현재 프로그램에서만 요청할 수 있습니다.');return;}}catch{error(403,'잘못된 요청 출처입니다.');return;}
if(active>=2){error(429,'다른 도면을 변환 중입니다. 완료 후 다시 실행하세요.');return;}
active++;
try{
const chunks:Buffer[]=[];let size=0;
for await(const chunk of req){const b=Buffer.from(chunk);size+=b.length;if(size>20*1024*1024)throw new Error('도면 크기가 20MB를 넘습니다.');chunks.push(b);}
const body=JSON.parse(Buffer.concat(chunks).toString('utf8')) as {dxf?:unknown};
if(typeof body.dxf!=='string'||!body.dxf.includes('SECTION')||!body.dxf.trim().endsWith('EOF'))throw new Error('유효한 DXF 도면 데이터가 필요합니다.');
const python=resolvePython(root),script=path.join(root,'도면작성','dwg_export.py');
if(path.isAbsolute(python)&&!existsSync(python))throw new Error('DWG 출력용 기존 Python 실행 환경을 찾을 수 없습니다.');
const folder=path.join(root,'도면작성','output',randomUUID());await mkdir(folder,{recursive:true});
const source=path.join(folder,'source.dxf'),output=path.join(folder,'general.dwg');await writeFile(source,body.dxf,'utf8');
const result=await new Promise<{code:number;stdout:string;stderr:string}>((resolve,reject)=>{
const child=spawn(python,['-X','utf8',script,source,output],{cwd:root,windowsHide:true,stdio:['ignore','pipe','pipe']});
let stdout='',stderr='';child.stdout.on('data',b=>stdout+=b);child.stderr.on('data',b=>stderr+=b);child.on('error',reject);
const timeout=setTimeout(()=>{child.kill();reject(new Error('DWG 변환 제한 시간(120초)을 넘었습니다.'));},120000);
child.on('exit',code=>{clearTimeout(timeout);resolve({code:code??-1,stdout,stderr});});
});
const report=JSON.parse(result.stdout.trim().split(/\r?\n/).at(-1)??'{}');
if(result.code!==0||report.status!=='validated')throw new Error(report.message||'DWG 왕복 검증을 통과하지 못했습니다.');
const bytes=await readFile(output);res.statusCode=200;res.setHeader('Content-Type','application/octet-stream');res.setHeader('Content-Disposition','attachment; filename="general.dwg"');res.setHeader('X-Pierzainer-Validation',encodeURIComponent(JSON.stringify(report)));res.end(bytes);
}catch(e){error(422,(e as Error).message);}finally{active--;}
};
return{name:'pierzainer-general-arrangement-dwg',configureServer(server){server.middlewares.use(DWG_API,(req,res)=>void handler(req,res));},configurePreviewServer(server){server.middlewares.use(DWG_API,(req,res)=>void handler(req,res));}};
}