/* 횡단 조정 창 · 좌측 폼 한 곳 단위검증 헬퍼(PLAN 24-4a) — TS 를 그 자리에서 트랜스파일해 Node 로 돌린다. * 화면 그릇(DOM)은 조정 창이 쓰는 만큼만 흉내낸다. 결과는 표준출력에 JSON 한 벌. * * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); // ── 화면 그릇 흉내 — 요소 · 클래스 · 이벤트 · 글자 · `.클래스` 찾기만 ──────────── class FakeNode { constructor(tag) { this.tagName = tag.toUpperCase(); this.children = []; this.dataset = {}; this.style = {}; this.listeners = {}; this._text = ""; this._classes = new Set(); this.value = ""; const self = this; this.classList = { add: (...names) => names.forEach((n) => self._classes.add(n)), remove: (...names) => names.forEach((n) => self._classes.delete(n)), toggle: (name, force) => { const on = force === undefined ? !self._classes.has(name) : !!force; on ? self._classes.add(name) : self._classes.delete(name); return on; }, contains: (name) => self._classes.has(name), }; } set className(value) { this._classes = new Set(String(value).split(/\s+/).filter(Boolean)); } get className() { return [...this._classes].join(" "); } set textContent(value) { this._text = String(value); this.children = []; } get textContent() { return this._text + this.children.map((c) => c.textContent).join(""); } append(...kids) { kids.forEach((k) => this.children.push(k)); } setAttribute(name, value) { this[name] = value; } addEventListener(type, handler) { (this.listeners[type] ||= []).push(handler); } getClientRects() { return [1]; } get isConnected() { return true; } querySelectorAll(selector) { const found = []; const walk = (node) => node.children.forEach((kid) => { if (kid.classList && kid.classList.contains(selector.slice(1))) found.push(kid); walk(kid); }); walk(this); return found; } querySelector(selector) { return this.querySelectorAll(selector)[0] ?? null; } } globalThis.document = { createElement: (tag) => new FakeNode(tag), addEventListener: () => {}, removeEventListener: () => {}, }; globalThis.window = { addEventListener: () => {}, removeEventListener: () => {} }; globalThis.requestAnimationFrame = (fn) => fn(); const STUBS = { "./B06_Section_UI_Section_Common": { L: (key) => key }, "@ui/ui_template_elements": { showToast: () => {} }, "./B06_Section_UI_Page_Ford_Controls": { applyFordFormOptions: () => {}, applyBoxFormOptions: () => {}, }, }; function loadTs(file) { const full = path.resolve(file); if (cache.has(full)) return cache.get(full); const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { 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`)); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; } const B06 = (name) => loadTs(path.join(ROOT, "B06_Section", `${name}.ts`)); const { buildStructurePanel } = B06("B06_Section_UI_Cross_Structure_Panel"); const { buildFordPanel } = B06("B06_Section_UI_Cross_Ford_Panel"); const { buildBoxPanel } = B06("B06_Section_UI_Cross_Box_Panel"); const { applyPipeOptionsToCache } = B06("B06_Section_UI_Page_Pipe_Options"); /** 값 칸(input · select) 수 — 조정 창 완료 기준 수치. */ const fieldCount = (node) => node.children.reduce( (sum, kid) => sum + (kid.tagName === "INPUT" || kid.tagName === "SELECT" ? 1 : 0) + fieldCount(kid), 0, ); /** 보이는 줄의 종류(`__struct--*`) — 숨김(is-hidden) 줄은 뺀다. */ const shownRows = (root) => root.children .filter((kid) => !kid.classList.contains("is-hidden")) .flatMap((kid) => [...kid._classes].filter((c) => c.startsWith("b06-structure-panel__struct--")), ) .map((c) => c.replace("b06-structure-panel__struct--", "")) .sort(); let structure = "revet"; const wallDeps = { adjustFor: () => ({ x: 0, d: null, h: null, m: null }), nudge: () => {}, nudgeSlope: () => {}, nudgeHeight: () => {}, heightFor: () => 2, formFor: () => "돌쌓기(메)", setForm: () => {}, reset: () => {}, pipeLengthM: () => 10, close: () => {}, structureFor: () => structure, basinAdjustFor: () => ({ innerWidthM: 1, innerHeightM: 1.2, lateralM: 0, slopeM: 0 }), updateBasin: () => {}, nudgeBasinLateral: () => {}, resetBasin: () => {}, basinExtraState: () => ({ canAdd: false, count: 0 }), setBasinExtraCount: () => {}, hiddenPipe: () => false, canNudge: (key) => key !== "inlet" || structure === "revet", fineMove: () => false, extraState: () => ({ canAdd: true, count: 1 }), isLinkedCard: () => false, setExtraCount: () => {}, equalizeExtras: () => {}, spanFor: () => ({ lengthM: 10, beforeM: 5, afterM: 5 }), setSpan: () => {}, linkState: () => null, setLinked: () => {}, followGrade: () => true, setFollowGrade: () => {}, scrollKey: "t", }; const out = { overlay: {}, dock: {} }; for (const [label, key, kind] of [ ["outlet", "outlet", "revet"], ["extra", "extra0", "revet"], ["basin", "inlet", "L"], ]) { structure = kind; const overlay = buildStructurePanel(wallDeps); overlay.show(key); const dock = buildStructurePanel(wallDeps, { dock: true }); dock.show(key); out.overlay[label] = fieldCount(overlay.root); out.dock[label] = shownRows(dock.root); } const fordDeps = { adjustFor: () => ({ heightM: null, lateralM: 0, slopeM: 0 }), heightFor: () => 1.5, nudgeHeight: () => {}, nudge: () => {}, nudgeSlope: () => {}, reset: () => {}, slabLengthM: () => 7.6, close: () => {}, }; const fordOverlay = buildFordPanel(fordDeps); fordOverlay.show("inlet"); const fordDock = buildFordPanel(fordDeps, { dock: true }); fordDock.show("inlet"); out.overlay.ford = fieldCount(fordOverlay.root); out.dock.ford = shownRows(fordDock.root); const boxDeps = { adjustFor: () => ({ lengthM: 0, riseM: 0 }), nudgeLength: () => {}, nudgeRise: () => {}, reset: () => {}, wingRoleFor: () => "inlet", bodyLengthM: () => 5, slopeRatio: () => null, close: () => {}, }; const boxOverlay = buildBoxPanel(boxDeps); boxOverlay.show("left"); const boxDock = buildBoxPanel(boxDeps, { dock: true }); boxDock.show("left"); out.overlay.box = fieldCount(boxOverlay.root); out.dock.box = shownRows(boxDock.root); // ── 폼 → 횡단 캐시: 폼이 보낸 것이 칸 전부 — 비운 칸은 자동으로(빈 값) ────────── const updates = []; const section = { chainage_m: 100, culvert: { diameter_m: 1, hidden_pipe: false, inlet: { revet_height_m: 3, revet_length_m: 10 }, outlet: { revet_height_m: 3, revet_form: "콘크리트" }, }, }; const ctx = { detail: () => ({ cross_sections: [section] }), ford: {}, box: {}, revetOffset: { update: (chainageM, role, patch) => updates.push({ role, ...patch }), adjustFor: () => ({ x: 0, d: null, h: null, m: null }), }, overrideOptions: () => {}, refreshCard: () => {}, }; applyPipeOptionsToCache(ctx, 100, { outlet_revet_height_m: 2.5, outlet_revet_form: "돌쌓기(메)" }); const filled = updates.splice(0); applyPipeOptionsToCache(ctx, 100, {}); // 초안 되얹기(24-7a)는 일부 칸만 — 안 온 칸 · 기슭막이 높이 · 형태를 건드리지 않는다. const clearedSpec = { outlet: { ...section.culvert.outlet }, inlet: { ...section.culvert.inlet } }; section.culvert.outlet = { revet_height_m: 3, revet_form: "콘크리트" }; const clearedUpdates = updates.splice(0); applyPipeOptionsToCache(ctx, 100, { outlet_revet_length_m: 12 }, false); out.replay = { updates: updates.splice(0), outletSpec: { ...section.culvert.outlet } }; updates.push(...clearedUpdates); out.cache = { filled, cleared: updates, outletSpec: clearedSpec.outlet, inletSpec: clearedSpec.inlet, }; process.stdout.write(JSON.stringify(out));