Update Pier_v1 project
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
f25e36c050
commit
ad73ddec2b
2476 files changed
+3423
-4902
No files matched your search
@@ -0,0 +1,113 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { PassThrough, Readable } from 'node:stream';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DRAWING_API_PATH, drawingLauncherPlugin } from '../랜딩페이지/drawing-launcher';
|
||||
|
||||
class Worker extends EventEmitter {
|
||||
stdout = new PassThrough(); stderr = new PassThrough(); kill = vi.fn();
|
||||
send(value: object) { this.stdout.write(JSON.stringify(value) + '\n'); }
|
||||
}
|
||||
const base = path.resolve('.verify');
|
||||
const roots: string[] = [];
|
||||
function fixture(options: { missing?: boolean; fail?: boolean; platform?: NodeJS.Platform } = {}) {
|
||||
mkdirSync(base, { recursive: true });
|
||||
const root = mkdtempSync(path.join(base, 'drawing-api-')); roots.push(root);
|
||||
const folder = path.join(root, 'DWG & 도면'); mkdirSync(folder);
|
||||
const workers: Worker[] = [];
|
||||
const start = vi.fn((_file: string, args: string[]) => {
|
||||
const worker = new Worker(); workers.push(worker);
|
||||
queueMicrotask(() => {
|
||||
if (options.fail) worker.emit('error', new Error('worker unavailable'));
|
||||
else if (args[2] === 'choose') { worker.send({ type: 'folder', folder }); worker.emit('close', 0); }
|
||||
});
|
||||
return worker as unknown as ChildProcess;
|
||||
});
|
||||
const plugin = drawingLauncherPlugin(root, {
|
||||
platform: options.platform ?? 'win32', start,
|
||||
exists: file => options.missing ? false : file.endsWith('python.exe') || file.endsWith('drawing-worker.py') || existsSync(file),
|
||||
});
|
||||
let handle!: (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
||||
(plugin.configureServer as Function)({ middlewares: { use(fn: typeof handle) { handle = fn; } } });
|
||||
const request = (action: string, body: object = {}, overrides: { method?: string; origin?: string; action?: string; address?: string; host?: string; raw?: string } = {}) => new Promise<{ status: number; body: any; bytes?: Buffer }>(resolve => {
|
||||
const req = Readable.from([overrides.raw ?? JSON.stringify(body)]) as unknown as IncomingMessage;
|
||||
Object.assign(req, { url: `${DRAWING_API_PATH}/${action}`, method: overrides.method ?? 'POST', headers: {
|
||||
host: overrides.host ?? '127.0.0.1:5173', origin: overrides.origin ?? 'http://127.0.0.1:5173', 'x-pierzainer-action': overrides.action ?? 'drawing-extractor',
|
||||
}, socket: { remoteAddress: overrides.address ?? '127.0.0.1' } });
|
||||
let status = 0, json = true, sent = false;
|
||||
const res = { get headersSent() { return sent; }, writeHead(code: number, headers: Record<string, string>) { status = code; json = headers['Content-Type'].includes('application/json'); sent = true; },
|
||||
end(text: string | Buffer) { resolve(json ? { status, body: JSON.parse(String(text)) } : { status, body: null, bytes: Buffer.from(text) }); } } as unknown as ServerResponse;
|
||||
handle(req, res, () => resolve({ status: 404, body: {} }));
|
||||
});
|
||||
const finish = (outside = false) => {
|
||||
const args = start.mock.calls[start.mock.calls.length - 1][1];
|
||||
const output = args[args.indexOf('--output')+1];
|
||||
const file = path.join(outside ? root : output, '결과.xlsx'); writeFileSync(file, 'test-workbook');
|
||||
const worker = workers[workers.length-1]; worker.send({ type: 'log', message: '도면 판독 완료' });
|
||||
worker.send({ type: 'result', result: { status: 'needs_review', output_dir: output, review_count: 2, failed_file_count: 0,
|
||||
workbooks: [{ name: '테스트 교량', xlsx_path: file, review_count: 2 }] } });
|
||||
worker.emit('close', 0);
|
||||
return file;
|
||||
};
|
||||
return { root, folder, request, start, workers, finish };
|
||||
}
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
if (!root.startsWith(base + path.sep)) throw new Error('Unsafe test fixture path');
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
describe('in-app drawing extraction API', () => {
|
||||
it('only accepts local same-origin POST requests with the application header', async () => {
|
||||
const f = fixture();
|
||||
expect((await f.request('start', {}, { method: 'GET' })).status).toBe(405);
|
||||
for (const headers of [{ origin: 'https://other.example' }, { action: '' }, { address: '192.168.0.2' }, { host: 'other.example' }])
|
||||
expect((await f.request('start', {}, headers)).status).toBe(403);
|
||||
expect(f.start).not.toHaveBeenCalled();
|
||||
});
|
||||
it('returns folder selection without starting the standalone GUI', async () => {
|
||||
const f = fixture(); expect((await f.request('choose-folder')).body.folder).toBe(f.folder);
|
||||
expect(f.start.mock.calls[0][1][2]).toBe('choose');
|
||||
expect(f.start.mock.calls[0][1].join(' ')).not.toContain('desktop_app.py');
|
||||
expect((await f.request('status')).body.job).toBeNull();
|
||||
});
|
||||
it('tracks progress, preserves review warnings, and serves only its generated workbook', async () => {
|
||||
const f = fixture(); const response = await f.request('start', { folder: f.folder });
|
||||
expect(response.status).toBe(202); const jobId = response.body.job.id;
|
||||
expect(f.start.mock.calls[0][1][3]).toBe(f.folder);
|
||||
expect((await f.request('workbook', { jobId, index: 0 })).status).toBe(400);
|
||||
const file = f.finish();
|
||||
const state = (await f.request('status', { jobId })).body.job;
|
||||
expect(state.status).toBe('needs_review'); expect(state.logs).toContain('도면 판독 완료');
|
||||
expect(state.result.reviewCount).toBe(2);
|
||||
const download = await f.request('workbook', { jobId, index: 0, path: 'unrelated.txt' });
|
||||
expect(download.status).toBe(200); expect(download.bytes).toEqual(readFileSync(file));
|
||||
expect((await f.request('workbook', { jobId, index: -1 })).status).toBe(400);
|
||||
expect((await f.request('workbook', { jobId, index: 20 })).status).toBe(404);
|
||||
});
|
||||
it('does not run two extraction jobs concurrently', async () => {
|
||||
const f = fixture(); const states = await Promise.all([f.request('start', { folder: f.folder }), f.request('start', { folder: f.folder })]);
|
||||
expect(states.map(s => s.status).sort()).toEqual([202, 409]); expect(f.start).toHaveBeenCalledTimes(1); f.finish();
|
||||
});
|
||||
it('refuses files outside the controlled output even if a worker lists them', async () => {
|
||||
const f = fixture(); const { body } = await f.request('start', { folder: f.folder }); f.finish(true);
|
||||
expect((await f.request('workbook', { jobId: body.job.id, index: 0 })).status).toBe(403);
|
||||
});
|
||||
it('reports worker failures so the popup can retry', async () => {
|
||||
const f = fixture({ fail: true }); const { body } = await f.request('start', { folder: f.folder });
|
||||
const job = (await f.request('status', { jobId: body.job.id })).body.job;
|
||||
expect(job.status).toBe('error'); expect(job.error).toContain('worker unavailable');
|
||||
});
|
||||
it('validates folders, JSON, and request size before starting a worker', async () => {
|
||||
const f = fixture();
|
||||
expect((await f.request('start', { folder: 'missing-folder' })).status).toBe(400);
|
||||
expect((await f.request('start', {}, { raw: 'bad-json' })).status).toBe(400);
|
||||
expect((await f.request('start', {}, { raw: 'a'.repeat(8193) })).status).toBe(413);
|
||||
expect(f.start).not.toHaveBeenCalled();
|
||||
expect((await fixture({ missing: true }).request('status')).status).toBe(503);
|
||||
expect((await fixture({ platform: 'linux' }).request('status')).status).toBe(501);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -5,9 +5,9 @@ import { parseWorkbook } from '../src/core/parseWorkbook';
|
||||
import type { PierInput } from '../src/core/types';
|
||||
|
||||
/** Spec workbook (RAG/Pier ui_xxx.xlsx, sheet intent.PR, all tables on one sheet). Used by single-sheet tests. */
|
||||
const SPEC_PATH = fileURLToPath(new URL('../public/samples/P1.xlsx', import.meta.url));
|
||||
const SPEC_PATH = fileURLToPath(new URL('../도면추출/samples/P1.xlsx', import.meta.url));
|
||||
/** User input (RAG/운곡교P1입력.xlsx, three sheets 교각 · IP · 받침). This is the P1 input used by the app and tests. */
|
||||
const INPUT_PATH = fileURLToPath(new URL('../public/samples/운곡교P1입력.xlsx', import.meta.url));
|
||||
const INPUT_PATH = fileURLToPath(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url));
|
||||
|
||||
export function loadWorkbook(path: string = SPEC_PATH): XLSX.WorkBook {
|
||||
return XLSX.read(readFileSync(path), { type: 'buffer' });
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
/**
|
||||
* Regression against spec.md §12 (P1, 운곡교, T형, 말뚝기초).
|
||||
* Coordinates and EL: ±1 mm (0.001 m). Angles: ±0.01°.
|
||||
*/
|
||||
const TOL = 0.001;
|
||||
const model = buildPier(loadP1());
|
||||
|
||||
describe('P1 regression — spec.md §12', () => {
|
||||
it('axis length and azimuth', () => {
|
||||
expect(model.lengthIp).toBeCloseTo(12.166, 3);
|
||||
expect(model.azimuthDeg).toBeCloseTo(350.37, 2);
|
||||
});
|
||||
|
||||
it('BP1, BP2 coordinates (국토좌표 E, N)', () => {
|
||||
expect(model.bp1.e).toBeCloseTo(259866.681, 2);
|
||||
expect(model.bp1.n).toBeCloseTo(179132.173, 2);
|
||||
expect(Math.abs(model.bp2.e - 259864.674)).toBeLessThan(TOL);
|
||||
expect(Math.abs(model.bp2.n - 179144.004)).toBeLessThan(TOL);
|
||||
});
|
||||
|
||||
it('IP2 reference recalculation and d_IP2', () => {
|
||||
expect(Math.abs(model.dIp2 - 1.134)).toBeLessThan(TOL);
|
||||
});
|
||||
|
||||
it('column centre coordinates', () => {
|
||||
expect(Math.abs(model.pivot.e - 259865.677)).toBeLessThan(TOL);
|
||||
expect(Math.abs(model.pivot.n - 179138.089)).toBeLessThan(TOL);
|
||||
});
|
||||
|
||||
it('bearing positions: front row across-axis −0.698, back row +0.600; front s from 0.943 to 10.965', () => {
|
||||
const front = model.bearings.filter((b) => b.row === '전열');
|
||||
const back = model.bearings.filter((b) => b.row === '후열');
|
||||
expect(front[0].n).toBeCloseTo(-0.698, 3);
|
||||
expect(back[0].n).toBeCloseTo(0.6, 3);
|
||||
expect(front[0].s).toBeCloseTo(0.943, 3);
|
||||
expect(front[4].s).toBeCloseTo(10.965, 3);
|
||||
expect(back[0].s).toBeCloseTo(1.029, 3);
|
||||
expect(back[4].s).toBeCloseTo(11.05, 3);
|
||||
});
|
||||
|
||||
it('step boundaries at the midpoints of front bearings: 2.196 / 4.701 / 7.207 / 9.712', () => {
|
||||
const bounds = model.segments.slice(1).map((s) => s.s0);
|
||||
[2.196, 4.701, 7.207, 9.712].forEach((b, i) => expect(Math.abs(bounds[i] - b)).toBeLessThan(TOL));
|
||||
});
|
||||
|
||||
it('front segment tops: 43.411 / 43.483 / 43.555 / 43.626 / 43.698', () => {
|
||||
const tops = model.segments.map((s) => s.top);
|
||||
[43.411, 43.483, 43.555, 43.626, 43.698].forEach((t, i) => expect(Math.abs(tops[i] - t)).toBeLessThan(TOL));
|
||||
});
|
||||
|
||||
it('bearing block heights: h1 = 150, h2 = 200 mm; concrete 100 / 150 mm', () => {
|
||||
expect(model.h1).toBeCloseTo(0.15, 6);
|
||||
expect(model.h2).toBeCloseTo(0.2, 6);
|
||||
expect(model.derived.blockHeightFront).toBeCloseTo(0.1, 6);
|
||||
expect(model.derived.blockHeightBack).toBeCloseTo(0.15, 6);
|
||||
});
|
||||
|
||||
it('no lateral step (front and back tops agree)', () => {
|
||||
expect(model.derived.frontBackDiffSpread).toBeLessThan(0.0051);
|
||||
});
|
||||
|
||||
it('coping bottom: 42.411 at the ends, 40.911 at the Level zone', () => {
|
||||
expect(Math.abs(model.endBottom - 42.411)).toBeLessThan(TOL);
|
||||
expect(Math.abs(model.levelBottom - 40.911)).toBeLessThan(TOL);
|
||||
});
|
||||
|
||||
it('column height 12.703 m and footing top 28.208 m, soil cover 0.723 m', () => {
|
||||
expect(Math.abs(model.derived.columnHeight - 12.703)).toBeLessThan(TOL);
|
||||
expect(Math.abs(model.footings[0].top - 28.208)).toBeLessThan(TOL);
|
||||
expect(Math.abs(model.derived.soilCover - 0.723)).toBeLessThan(TOL);
|
||||
});
|
||||
|
||||
it('piles: 4 rows × 5 = 20, spacing 2250 mm, tip at 2.208 m', () => {
|
||||
expect(model.piles).toHaveLength(20);
|
||||
expect(model.derived.pileCount).toBe(20);
|
||||
expect(Math.abs((model.derived.pileTipEl ?? NaN) - 2.208)).toBeLessThan(TOL);
|
||||
const sValues = [...new Set(model.piles.map((p) => p.s.toFixed(4)))];
|
||||
const nValues = [...new Set(model.piles.map((p) => p.n.toFixed(4)))];
|
||||
expect(sValues).toHaveLength(5);
|
||||
expect(nValues).toHaveLength(4);
|
||||
expect(Number(nValues[1]) - Number(nValues[0])).toBeCloseTo(2.25, 4);
|
||||
expect(Number(sValues[1]) - Number(sValues[0])).toBeCloseTo(2.25, 4);
|
||||
});
|
||||
|
||||
it('blinding concrete 10600 × 8350 × 100 under the footing', () => {
|
||||
const b = model.blindings[0];
|
||||
expect(b.length).toBeCloseTo(10.6, 4);
|
||||
expect(b.width).toBeCloseTo(8.35, 4);
|
||||
expect(b.thickness).toBeCloseTo(0.1, 4);
|
||||
});
|
||||
|
||||
it('footing 10400 × 8150 × 2000 mm', () => {
|
||||
const f = model.footings[0];
|
||||
expect(f.length).toBeCloseTo(10.4, 4);
|
||||
expect(f.width).toBeCloseTo(8.15, 4);
|
||||
expect(f.top - f.bottom).toBeCloseTo(2.0, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pile layout text (spec §12, §6.8 display)', () => {
|
||||
it('reads 4@2250 × 3@2250 = 20본', () => {
|
||||
expect(model.derived.pileLayout).toBe('4@2250 × 3@2250 = 20본');
|
||||
});
|
||||
});
|
||||
@@ -4,9 +4,9 @@ import { fileURLToPath } from 'node:url';
|
||||
import { parseWorkbook } from '../src/core/parseWorkbook';
|
||||
import { loadWorkbook } from './helpers';
|
||||
|
||||
// The standing input is RAG/운곡교P1입력.xlsx. The app serves a copy in public/samples; the two must stay identical.
|
||||
// The standing input is RAG/운곡교P1입력.xlsx. The app uses the copy in 도면추출/samples.
|
||||
const RAG_INPUT = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const APP_COPY = fileURLToPath(new URL('../public/samples/운곡교P1입력.xlsx', import.meta.url));
|
||||
const APP_COPY = fileURLToPath(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url));
|
||||
|
||||
describe('표준 입력 파일 (운곡교P1입력.xlsx)', () => {
|
||||
it('the app sample is byte-identical to RAG/운곡교P1입력.xlsx', () => {
|
||||
|
||||
Reference in new issue
Block a user