feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음. - 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용), 프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외 - 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정 - 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결 - 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등) 정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2bebe47eb
commit
fc520ec03d
47 files changed
+2848
-149
No files matched your search
@@ -0,0 +1,120 @@
|
||||
// Run with Windows Node and an isolated Chrome on CDP 9444; Vite must be on 5173.
|
||||
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict');
|
||||
const out = path.resolve(__dirname, '../.verify/takeoff-alignment-20261106/browser');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [] };
|
||||
let ws, send;
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
|
||||
let seq = 0; const pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
};
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq, timer = setTimeout(() => reject(new Error('Timeout ' + method)), 30000);
|
||||
pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expression => {
|
||||
for (let i = 0; i < 120; i++) { if (await ev(expression)) return; await delay(150); }
|
||||
throw new Error('Wait ' + expression);
|
||||
};
|
||||
const check = (name, value) => { assert.ok(value, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const click = expression => ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing target');e.click();})()`);
|
||||
const shot = async name => {
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64'));
|
||||
};
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Browser.setDownloadBehavior', {behavior:'deny'}); // Request transport is mocked below; native Excel is verified separately.
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.openSampleProject==='function'`);
|
||||
const fixture = await ev(`(async()=>{
|
||||
const XLSX=await import('/node_modules/xlsx/xlsx.mjs'),{parseWorkbook}=await import('/src/core/parseWorkbook.ts');
|
||||
const bytes=await(await fetch('/'+encodeURIComponent('도면추출')+'/samples/'+encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base=parseWorkbook(XLSX.read(bytes)).piers.find(p=>p.name==='P1');
|
||||
if(!base)throw new Error('Missing sample pier');
|
||||
const {defaultLevelWidthMm}=await import('/src/core/geometry.ts');
|
||||
const piers=['직접기초','직접기초','현장타설말뚝'].map((foundationType,i)=>{const input={...base,name:'P'+(i+1),foundationType,type:'T형',columnCount:1,groundEl:30,pileLengthMm:24000,castInPlaceShape:{clayBottomEl:16},columnDiameterMm:2000,footingLengthMm:10400,footingWidthMm:8150,footingHeightMm:2000,blindingMarginMm:100,blindingThicknessMm:100,pileKind:'강관',pileOuterMm:609.6,pileThicknessMm:12,copingH1Mm:1000,copingH2Mm:1500,levelWidthMm:defaultLevelWidthMm('T형',2000)};return{id:input.name,input,defaults:input,draft:input};});
|
||||
return{schemaVersion:1,project:{name:'병합 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'합성 표본',sheets:[]},piers,history:[]};
|
||||
})()`);
|
||||
for(const pier of fixture.piers){
|
||||
const settings={values:{r20:0,r21:2.8,r22:0,r23:0},reviewed:true};
|
||||
for(const key of ['input','defaults','draft'])pier[key]={...pier[key],quantityTakeoff:settings};
|
||||
}
|
||||
const file = path.join(out, 'fixture.pzr.json'); fs.writeFileSync(file, JSON.stringify(fixture));
|
||||
await ev('window.openSampleProject()');
|
||||
await click(`document.querySelector('#file-menu summary')`); await click(`document.getElementById('open-project')`);
|
||||
const doc = await send('DOM.getDocument');
|
||||
const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#project-file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId, files: [file] });
|
||||
await until(`document.querySelectorAll('#rebar-pier-items button').length===3`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
check('local rebar screen renders', await ev(`!document.getElementById('view-rebar').hidden&&!!document.querySelector('#rebar-table table')`));
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith('P3 ·'))`);
|
||||
await click(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith('기둥'))`);
|
||||
check('unified cast input and physical rebar preserved', await ev(`!!document.getElementById('cip-apply')&&document.getElementById('rebar-table').textContent.includes('말뚝-P1')`));
|
||||
await shot('01-cast');
|
||||
await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`);
|
||||
check('materials is the default result tab', await ev(`!document.getElementById('view-quantity').hidden&&document.querySelector('[data-takeoff-mode="materials"]').getAttribute('aria-pressed')==='true'`));
|
||||
check('local material table preserved', await ev(`document.getElementById('quantity-body').textContent.includes('말뚝-P1')`));
|
||||
await shot('02-materials');
|
||||
check('partial results explicitly export as review only', await ev(`!document.getElementById('quantity-export').disabled&&document.getElementById('quantity-export').textContent.includes('검토용')`));
|
||||
check('piece details expose count and individual lengths', await ev(`!!document.querySelector('#quantity-body details table')&&document.getElementById('quantity-body').textContent.includes('가공 본수')`));
|
||||
await click(`[...document.querySelectorAll('#quantity-pier-items button')].find(b=>b.textContent.startsWith('P1 ·'))`);
|
||||
check('current regular pier permits export', await ev(`!document.getElementById('quantity-export').disabled`));
|
||||
await click(`document.querySelector('[data-takeoff-mode="takeoff"]')`);
|
||||
check('selected pier receives automatic rebar weight', await ev(`!document.querySelector('input[data-variable="rebar"]')&&document.querySelector('.takeoff-workspace').textContent.includes('기존 철근재료표')`));
|
||||
check('selected and all Excel exports are separate', await ev(`document.querySelector('[data-takeoff-export="selected"]').textContent==='선택 교각 Excel'&&document.querySelector('[data-takeoff-export="all"]').textContent==='전체 교각 Excel'`));
|
||||
await click(`document.querySelector('button[aria-label="수량산출 재계산"]')`);
|
||||
await ev(`(()=>{
|
||||
const original=window.fetch;window.__takeoffRequests=[];
|
||||
window.fetch=(url,options)=>String(url).includes('/__pierzainer/quantity/export')?new Promise(resolve=>{window.__takeoffRequests.push(JSON.parse(options.body));window.__finishTakeoff=()=>resolve(new Response(new Uint8Array([80,75]),{status:200}));}):original(url,options);
|
||||
})()`);
|
||||
await click(`document.querySelector('[data-takeoff-export="selected"]')`);
|
||||
await until(`window.__takeoffRequests.length===1`);
|
||||
check('selected request carries one pier and model solplate mode', await ev(`window.__takeoffRequests[0].piers.length===1&&window.__takeoffRequests[0].piers[0].pier==='P1'&&window.__takeoffRequests[0].piers[0].solplateMode==='model'`));
|
||||
await ev(`(()=>{const e=document.querySelector('input[data-variable="r21"]');e.value='4';e.dispatchEvent(new Event('change'));})()`);
|
||||
check('rerender during export keeps both export buttons locked', await ev(`[...document.querySelectorAll('[data-takeoff-export]')].every(b=>b.disabled)`));
|
||||
await ev(`window.__finishTakeoff()`);
|
||||
await until(`!document.querySelector('[data-takeoff-export="selected"]').disabled`);
|
||||
check('async export preserves edited input and displays snapshot warning', await ev(`document.querySelector('input[data-variable="r21"]').value==='4'&&document.querySelector('.takeoff-workspace [role="status"]').textContent.includes('입력은 보존')`));
|
||||
const requests=await ev(`window.__takeoffRequests.length`);
|
||||
await click(`document.querySelector('[data-takeoff-export="all"]')`);
|
||||
await delay(200);
|
||||
check('all export validates other incomplete piers before requesting server', await ev(`window.__takeoffRequests.length===${requests}&&document.querySelector('.takeoff-workspace [role="status"]').textContent.includes('P3')`));
|
||||
await ev(`(()=>{const e=document.querySelector('select[aria-label="수량산출 대상 교각"]');e.value='P2';e.dispatchEvent(new Event('change'));})()`);
|
||||
check('another pier retains automatic weight despite material filter', await ev(`document.querySelector('select[aria-label="수량산출 대상 교각"]').value==='P2'&&!document.querySelector('input[data-variable="rebar"]')&&document.querySelector('.takeoff-workspace').textContent.includes('기존 철근재료표')`));
|
||||
await click(`document.getElementById('nav-drawing')`);
|
||||
await until(`!!document.querySelector('#view-drawing svg')`);
|
||||
check('origin drawing screen renders finite geometry', await ev(`!document.getElementById('view-drawing').hidden&&!/NaN|Infinity/.test(document.querySelector('#view-drawing svg').outerHTML)`));
|
||||
await shot('03-drawing');
|
||||
await click(`document.getElementById('ga-materials')`);
|
||||
check('drawing shortcut opens materials rather than takeoff', await ev(`!document.getElementById('view-quantity').hidden&&document.querySelector('[data-takeoff-mode="materials"]').getAttribute('aria-pressed')==='true'`));
|
||||
await click(`document.getElementById('nav-plan')`);
|
||||
check('plan remains reachable after drawing', await ev(`!document.getElementById('view-plan').hidden`));
|
||||
await ev(`(()=>{const e=document.getElementById('edit-copingLengthMm');e.value=Number(e.value)+100;e.dispatchEvent(new Event('change'));})()`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`document.getElementById('rebar-materials')`);
|
||||
check('dirty geometry blocks Excel and print with reason', await ev(`document.getElementById('quantity-export').disabled&&document.getElementById('quantity-print').disabled&&document.getElementById('quantity-body').textContent.includes('재생성 필요')`));
|
||||
for (const width of [1280, 1600, 1920]) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await delay(150);
|
||||
check(`one-row header preserved at ${width}`, await ev(`document.querySelector('.top').getBoundingClientRect().height<=65&&document.documentElement.scrollWidth<=innerWidth`));
|
||||
}
|
||||
check('no runtime exceptions', report.errors.length === 0);
|
||||
report.result = 'PASS';
|
||||
})().catch(e => { report.result = 'FAIL'; report.failure = e.stack; process.exitCode = 1; }).finally(() => {
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report)); ws?.close();
|
||||
});
|
||||
Reference in new issue
Block a user