/* 배관 유출 「추가 흙막이(단)」 칸(PLAN 54-27) 단위검증 헬퍼 — 실제 시설 폼(`createFacilityOptionsForm`)을 * 그 자리에서 트랜스파일해 Node 로 돌린다. 단 수 읽기 · 쓰기(`design_structure_pipe_calc`)만 흉내 — * 쓰면 그 수가 곧 지형이 세운 단 수(한도 2). 결과는 표준출력에 JSON 한 벌. 짝 = `test_54_27_soil_tier_field.py`. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); // ── 화면 그릇 흉내 — 태그 · 클래스 · data · 이벤트만 ───────────────────────── class FakeNode { constructor(tag) { this.tagName = tag.toUpperCase(); this.children = []; this.dataset = {}; this.style = {}; this.parentElement = null; this.hidden = false; this.disabled = false; this._classes = new Set(); this._listeners = {}; this._text = ""; this.value = ""; const self = this; this.classList = { add: (...n) => n.forEach((c) => self._classes.add(c)), remove: (...n) => n.forEach((c) => self._classes.delete(c)), toggle: (c, f) => (f ?? !self._classes.has(c)) ? self._classes.add(c) : self._classes.delete(c), contains: (c) => self._classes.has(c), }; } set className(v) { this._classes = new Set(String(v).split(/\s+/).filter(Boolean)); } set textContent(v) { this._text = String(v); this.children = []; } get textContent() { return this._text + this.children.map((c) => c.textContent ?? "").join(""); } get childElementCount() { return this.children.length; } append(...kids) { kids.forEach((k) => { const node = typeof k === "string" ? { textContent: k } : k; if (node.parentElement) node.parentElement.children.splice(node.parentElement.children.indexOf(node), 1); node.parentElement = this; this.children.push(node); }); } replaceChildren(...kids) { this.children.forEach((k) => (k.parentElement = null)); this.children = []; this.append(...kids); } add(kid) { this.append(kid); } setAttribute(n, v) { this[n] = v; } addEventListener(type, fn) { (this._listeners[type] ??= []).push(fn); } dispatchEvent(event) { (this._listeners[event.type] ?? []).forEach((fn) => fn(event)); } closest() { return null; } matches(sel) { return sel.split(",").some((s) => { s = s.trim(); return s.startsWith(".") ? this._classes.has(s.slice(1)) : this.tagName === s.toUpperCase(); }); } querySelectorAll(sel) { const found = []; const walk = (n) => (n.children ?? []).forEach((k) => { if (k.matches?.(sel)) found.push(k); walk(k); }); walk(this); return found; } querySelector(sel) { return this.querySelectorAll(sel)[0] ?? null; } } globalThis.document = { createElement: (t) => new FakeNode(t), createComment: () => new FakeNode("#comment"), }; globalThis.Event = function (type) { this.type = type; }; globalThis.queueMicrotask = (fn) => fn(); globalThis.Option = function (text, value) { const o = new FakeNode("option"); o.textContent = text; o.value = value; return o; }; // 단 수 자리 흉내 — 쓰면 지형 한도(2)까지만 섬 const LIMIT = 2; let stored = 0; const writes = []; const PIPE_CALC = { guardHeightHint: () => "", pipeDrawn: () => null, pipeLengthM: () => null, pipeExtraCount: () => Math.min(stored, LIMIT), pipeRevetChoice: () => null, setPipeRevetChoice: () => Promise.resolve(), setPipeExtraCount: (chainage, count) => { writes.push([chainage, count]); stored = count; return Promise.resolve(); }, }; const STUBS = { "@config/config_frontend": { FORD_MANNING_N: 0.03, FORD_SLOPE: 0.02 }, "./ui_template_checkbox.css": {}, "../A00_Common/design/design_structure_pipe_calc": PIPE_CALC, // 관 보호공 [연동] · [경사] 단추(54-30) — 여기서는 자리만 · 단 칸(90-1 공용)은 실제 파일 "./B05_Profile_UI_Panel_Fields": { toggleButton: () => new FakeNode("button") }, "../../B05_Profile/B05_Profile_UI_Structures_Summary_Row": { buildSummaryRow: () => { const root = new FakeNode("div"); const label = new FakeNode("span"); label.className = "b05-structure__summary-label"; root.append(label); return { root, set: () => {}, bind: () => {} }; }, }, }; const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (request) => { if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); return loadTs(path.join(path.dirname(full), `${request}.ts`)); }, }); const { createFacilityOptionsForm } = loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_Facility.ts"), ); const form = createFacilityOptionsForm(); form.setChainage(100); form.setFacility("pipe", { pipe_diameter_mm: 1000 }); // 시설마다 명세 폼이 따로 — 지금 시설의 폼(본체 상자 id 로 찾음) let body = null; const specOf = () => form.root .querySelectorAll(".b05-structure__spec") .find((s) => !body || s.children.some((k) => k.dataset?.specContainer === body)); let spec = specOf(); const boxesOf = (id) => spec.children.filter((k) => k.dataset?.specContainer === id); const tierRow = () => boxesOf("outlet")[0] ?.querySelectorAll("label") .find((l) => l.dataset.specItem === "outlet.tiers"); const tierInput = () => tierRow()?.querySelector("input"); const snap = () => ({ value: tierInput()?.value ?? null, boxes: boxesOf("soil_add").length, // 상자 첫 칸 이름 — 구조 고르기 없이 형태부터(54-30 · 사면안정 흙막이와 같은 칸) structures: boxesOf("soil_add").map( (b) => b.querySelectorAll("label")[0]?.children[0]?.textContent, ), }); async function main() { const out = { label: tierRow()?.children[0]?.textContent ?? null, start: snap() }; for (const n of ["2", "1", "5", "0"]) { const input = tierInput(); input.value = n; input.dispatchEvent(new Event("change")); await Promise.resolve(); await Promise.resolve(); out[`set${n}`] = snap(); } // 횡단 조정에서 바꿈 → 좌측 칸 · 상자 따라감(B06 이 recount 를 부름) stored = 2; form.recount(); out.fromCross = snap(); // 다른 구조(옛 값)면 칸을 가림 form.setFacility("pipe", { pipe_diameter_mm: 1000, outlet_type: "옛값" }); out.otherHidden = tierRow()?.hidden ?? null; // 세월교 · 박스 유출 흙막이(90-1) — 같은 단 칸 · 단 수 = 흙막이 상자 수 · 「+ 흙막이」 없음 · // 유출이 흙막이가 아니면 단 칸 · 옛 상자 숨김(값은 남김) const addButton = () => spec.children.find((k) => k.dataset?.specAdd === "soil_add") ? true : false; const old = { soil_add_1_form: "바자", soil_add_2_form: "돌망태", soil_add_3_form: "바자" }; for (const [facility, key, container] of [ ["ford_bridge", "outlet_type", "ford"], ["box_culvert", "wing_out", "body"], ]) { stored = 0; body = container; form.setFacility(facility, { [key]: "흙막이", ...old }); spec = specOf(); const steps = {}; for (const n of ["2", "0"]) { const input = tierInput(); input.value = n; input.dispatchEvent(new Event("change")); await Promise.resolve(); await Promise.resolve(); steps[`set${n}`] = { ...snap(), hidden: tierRow()?.hidden ?? null, add: addButton() }; } form.setFacility(facility, { [key]: "날개벽", ...old }); steps.wing = { boxes: boxesOf("soil_add").length, hidden: tierRow()?.hidden ?? null, add: addButton(), kept: Object.keys(form.readOptions()).filter((k) => k.startsWith("soil_add_")).length, }; out[facility] = steps; } out.writes = writes; process.stdout.write(JSON.stringify(out)); } main().catch((error) => { process.stderr.write(String(error?.stack ?? error)); process.exit(1); });