/* 472 덧(요구 272 C · D) 헬퍼 — 진짜 모듈(helper_472_open_no_draft_entry.ts)로 * ① 표준 횡단 패널 · 설정값 받기 = 세션 초안 칸 안 씀(같은 값이면 칸 없음) · 계산은 저장분 * ② 카드 단추 = 바꾼 칸만 보냄 · 세션 고름은 앞서 바꾼 칸에 얹음 * node helper_472_open_no_draft.mjs <묶음.mjs> <표준 횡단 고정 자료.json> * 짝 = `test_472_open_no_draft.py` · 표준출력 JSON 한 벌. */ import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; const store = () => { const m = new Map(); return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => m.set(k, String(v)), removeItem: (k) => m.delete(k), key: (i) => [...m.keys()][i] ?? null, get length() { return m.size; }, clear: () => m.clear(), }; }; /** 작은 가짜 요소 — 자식 · 듣는 함수 · 글만 들고 나머지는 빈 함수. */ function element(tag) { const base = { tagName: String(tag).toUpperCase(), children: [], listeners: {}, attrs: {}, style: {}, dataset: {}, textContent: "", classList: { add() {}, remove() {}, toggle() {}, contains: () => false }, append(...kids) { base.children.push(...kids); }, prepend(...kids) { base.children.unshift(...kids); }, insertBefore(kid) { base.children.push(kid); }, replaceChildren(...kids) { base.children = kids; }, addEventListener(type, fn) { (base.listeners[type] ??= []).push(fn); }, setAttribute(k, v) { base.attrs[k] = String(v); }, getAttribute: (k) => base.attrs[k] ?? null, querySelector: () => null, querySelectorAll: () => [], closest: () => null, clientWidth: 0, offsetWidth: 0, scrollWidth: 0, }; return new Proxy(base, { get: (t, k) => (k in t ? t[k] : typeof k === "string" ? () => undefined : undefined), }); } const noop = new Proxy(function () {}, { get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : noop), apply: () => noop, construct: () => noop, }); globalThis.window = globalThis; globalThis.sessionStorage = store(); globalThis.localStorage = store(); globalThis.document = new Proxy( { createElement: element, createElementNS: (_ns, tag) => element(tag) }, { get: (t, k) => (k in t ? t[k] : noop) }, ); globalThis.ResizeObserver = class { observe() {} disconnect() {} }; globalThis.getComputedStyle = () => ({ gap: "0" }); globalThis.requestAnimationFrame = (run) => setTimeout(run, 0); globalThis.addEventListener = () => undefined; globalThis.removeEventListener = () => undefined; globalThis.fetch = async () => new Response("{}", { status: 404 }); const m = await import(pathToFileURL(process.argv[2]).href); const draftKeys = () => Object.keys( Object.fromEntries([...Array(sessionStorage.length)].map((_, i) => [sessionStorage.key(i), 1])), ).filter((k) => k.startsWith("aislo:draft:")); const out = {}; // ① 표준 횡단 — config 와 저장분이 다른 프로젝트 const config = JSON.parse(readFileSync(process.argv[3], "utf8")).standard_cross_section; const stored = JSON.parse(JSON.stringify(config)); stored.rock.ditch.top_width_m = 0.9; // 저장분 = 사용자가 고쳐 저장한 값(config 와 다름) const context = { project_id: "pS", standard_cross_section: config, stored_standard_cross_section: stored, }; m.writeState("section-context", context, "pS"); await m.fetchSectionContext("pS"); // 캐시 길 — 예전엔 여기서 저장분을 초안 칸에 베낌 out.ctxDrafts = draftKeys(); const panel = m.createStandardPanel("pS", config, () => {}); out.panelDrafts = draftKeys(); out.panelRockDitch = panel.getValues().rock.ditch.top_width_m; out.effectiveRockDitch = m.effectiveStandardCross("pS").rock.ditch.top_width_m; panel.applyStored(stored); out.afterStored = draftKeys(); // 옛 판이 남긴 같은 값 칸 — 열면 걷음 m.writeState("std-cross", stored, "pS2"); m.writeState("section-context", { ...context, project_id: "pS2" }, "pS2"); m.createStandardPanel("pS2", config, () => {}); out.oldSameCell = m.readState("std-cross", "pS2"); // 저장분 없는 프로젝트 — config 그대로 = 칸 없음 · 계산은 config m.writeState("section-context", { project_id: "pC", standard_cross_section: config }, "pC"); m.createStandardPanel("pC", config, () => {}); out.configDrafts = draftKeys(); out.configRockDitch = m.effectiveStandardCross("pC").rock.ditch.top_width_m; // 사용자가 고친 값은 칸에 남음 const edited = JSON.parse(JSON.stringify(stored)); edited.soil.road_width_m = 3.5; m.writeState("std-cross", edited, "pE"); m.writeState("section-context", { ...context, project_id: "pE" }, "pE"); m.createStandardPanel("pE", config, () => {}); out.editedRoadWidth = m.readState("std-cross", "pE")?.soil.road_width_m ?? null; // ② 카드 단추 — 리핑암 · 좌절토 · 측구 자동 판정 sessionStorage.clear(); const section = { station_id: "No.1", chainage_m: 20, uphill_side: "left", design: { ground_type: "ripping_rock", section_mode: "left_cut", ditch_side: null, ditch_type: "standard", paved: false, two_stage_slope: false, ditch_choice: null, ditch_enabled: true, }, samples: [], }; const sent = []; const { bar, groundSegment } = m.buildDesignControls(section, (_c, change) => sent.push(change)); const buttons = []; const walk = (node) => { if (!node || typeof node !== "object") return; if (node.tagName === "BUTTON") buttons.push(node); (node.children ?? []).forEach(walk); }; walk(groundSegment); walk(bar); const press = (pick) => buttons.find(pick)?.listeners.click?.forEach((fn) => fn({})); out.soilPressed = buttons[0]?.getAttribute("aria-pressed"); press( (b) => b.textContent === "측구" || b.textContent === "측구 없음" || /측구/.test(b.textContent), ); out.ditchChange = sent.at(-1) ?? null; // 새 카드 · 토사 단추 — 지반만(측구형식은 이미 일반이라 안 실림) buttons.length = 0; const again = m.buildDesignControls(section, (_c, change) => sent.push(change)); walk(again.groundSegment); press(() => true); out.soilChange = sent.at(-1) ?? null; // 세션 고름 — 앞서 바꾼 칸에 얹음(지반은 안 박음) const detail = { cross_sections: [section] }; const sync = m.createDesignSync({ projectId: "pD", routeId: () => 7, detail: () => detail, view: () => noop, }); await sync.handleDesignChange(20, { ditch_choice: false }).catch(() => undefined); await sync.handleDesignChange(20, { two_stage_slope: true }).catch(() => undefined); out.choice = Object.fromEntries(m.crossDesignChoices("pD", 7)); await sync.handleDesignChange(20, {}).catch(() => undefined); out.choiceAfterEmpty = Object.fromEntries(m.crossDesignChoices("pD", 7)); // 소단 사본(시설 줄이 열 때 구조물에서 씀) — 떠남 확인에 안 잡힘 · 진짜 고침은 잡힘 sessionStorage.clear(); m.writeState( "berm", [{ start_m: 0, end_m: 40, width_m: 1, interval_m: 5, slope_deg: 45, depth_m: 1 }], "pB", 7, ); out.dirtyBermOnly = m.designDirty("pB"); m.writeCrossDesignChoice("pB", 7, 20, { ditch_choice: false }); out.dirtyWithEdit = m.designDirty("pB"); process.stdout.write(JSON.stringify(out)); process.exit(0);