Files
Pier_v1/scripts/verify-abut-wall.cjs
ypshimandClaude Haiku 5.5 a17c9ebbb4 feat(abut-rebar): ③ 교대 벽체 카드
- 보기 탭(교대축 직각 단면 / 계단 단면) · 삽도 · 구간 메모 · 범례
- 입력: 기본간격 3종, 피복, 첫 철근 최소 여유, W7 배수(편집). 자동 값: 연직 격자, W7 간격, skew 고정, l_d (fck 교대 벽체 등)
- 철근표: 선택 · 구분 · 번호 · 규격 · 간격 · 실간격(묶음 수정 W3 / W5 / W9) · 형상 · 길이 · 근거(규칙/가정) · 개수 · 위치
- 부재별 경고 수 · 알림(RB-AB), 행 선택 함수를 저판 카드와 공용
- scripts/verify-abut-wall.cjs: 헤드리스 Chrome 화면 확인 스크립트

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

91 lines
5.4 KiB
JavaScript

// ③ 교대 벽체 카드 화면 확인 (2026-11-04): 헤드리스 Chrome(CDP)으로 입력 파일을 불러와 A1 → ③ → 벽체를 열고 스크린샷을 남긴다.
// 사용 (WSL Node ≥ 22, 개발 서버 실행 중):
// chrome-headless-shell --headless --remote-debugging-port=9444 & (~/.cache/ms-playwright/chromium_headless_shell-*/)
// node scripts/verify-abut-wall.cjs <input.xlsx> <out dir>
const fs = require('fs');
const path = require('path');
const APP_URL = process.env.APP_URL || 'http://127.0.0.1:5173/';
const CDP = process.env.CDP || 'http://127.0.0.1:9444';
const [INPUT_FILE, OUT_DIR] = process.argv.slice(2);
if (!INPUT_FILE || !OUT_DIR) throw new Error('INPUT_FILE and OUT_DIR are required');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
const targets = await (await fetch(`${CDP}/json`)).json();
const page = targets.find((t) => t.type === 'page');
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((res, rej) => ((ws.onopen = res), (ws.onerror = rej)));
let seq = 0;
const waiting = new Map();
const consoleEvents = [];
ws.onmessage = (ev) => {
const m = JSON.parse(ev.data);
if (m.id && waiting.has(m.id)) {
waiting.get(m.id)(m);
waiting.delete(m.id);
} else if (m.method === 'Runtime.exceptionThrown') consoleEvents.push(`exception: ${m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text}`);
else if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') consoleEvents.push(`console.error: ${m.params.args.map((a) => a.value ?? a.description ?? '').join(' ')}`);
};
const send = (method, params = {}) => new Promise((res) => { const id = ++seq; waiting.set(id, res); ws.send(JSON.stringify({ id, method, params })); });
const evalJs = async (expression) => {
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
if (r.result.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 400));
return r.result.result.value;
};
const shot = async (name, full = false) => {
const params = { format: 'png' };
if (full) {
const m = await send('Page.getLayoutMetrics');
params.clip = { x: 0, y: 0, width: 1600, height: Math.min(m.result.cssContentSize.height, 6000), scale: 1 };
params.captureBeyondViewport = true;
}
const r = await send('Page.captureScreenshot', params);
fs.writeFileSync(path.join(OUT_DIR, name), Buffer.from(r.result.data, 'base64'));
console.log(`SHOT ${name}`);
};
const click = (js) => evalJs(`(() => { const e = ${js}; if (!e) throw new Error('없음: ${js.replace(/'/g, '')}'); e.click(); return true; })()`);
await send('Page.enable');
await send('Runtime.enable');
await send('DOM.enable');
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
await send('Page.navigate', { url: APP_URL });
await sleep(4000);
await click(`document.getElementById('open-import')`);
await sleep(500);
const doc = await send('DOM.getDocument', { depth: -1 });
const q = await send('DOM.querySelector', { nodeId: doc.result.root.nodeId, selector: '#file' });
await send('DOM.setFileInputFiles', { nodeId: q.result.nodeId, files: [INPUT_FILE] });
await sleep(3000);
await click(`document.getElementById('dlg-apply')`);
await sleep(2500);
await click(`document.getElementById('nav-rebar')`);
await sleep(1500);
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find((b) => b.textContent.includes('교대'))`);
await sleep(2000);
await click(`[...document.querySelectorAll('#rebar-tabs button')].find((b) => b.textContent.startsWith('벽체'))`);
await sleep(2000);
console.log('STATUS', await evalJs(`document.getElementById('step3-status').textContent`));
console.log('TABS', await evalJs(`[...document.querySelectorAll('#rebar-tabs button')].map((b) => b.textContent).join(' | ')`));
console.log('NOTE', await evalJs(`document.getElementById('rebar-note').textContent`));
console.log('ROWS', await evalJs(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].filter((r) => r.cells.length > 2).map((r) => [r.cells[2].textContent, r.cells[7].textContent, r.cells[8].textContent].join(' ')).join(' | ')`));
console.log('ISSUES', await evalJs(`document.getElementById('rebar-issues').innerText.replace(/\\n+/g, ' / ')`));
console.log('BARS3D', await evalJs(`document.querySelectorAll('canvas').length`));
await shot('wall-1-section.png', true);
await click(`[...document.querySelectorAll('.abut-optt')].find((b) => b.textContent.startsWith('교대축 단면'))`);
await sleep(1200);
await shot('wall-2-elevation.png', true);
// W9 행 선택 → 선택만 보기.
await click(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].find((r) => r.cells[2] && r.cells[2].textContent === 'W9')`);
await sleep(800);
await click(`[...document.querySelectorAll('#rebar-table .rebar-select button')].find((b) => b.textContent === '선택만 보기')`);
await sleep(1500);
console.log('SELECTED', await evalJs(`document.querySelector('#rebar-table .rebar-select span').textContent`));
await shot('wall-3-w9-only.png');
console.log(`CONSOLE_EVENTS ${consoleEvents.length}`);
for (const e of consoleEvents) console.log(` ${e}`);
ws.close();
}
main().catch((e) => { console.error('FAILED', e); process.exit(1); });