// ③ 교대 벽체 카드 화면 확인 (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 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); });