/* 272 헬퍼(468 · 466-C) — 사진첩 빈 칸 · 세션 맵 빈 쓰기 · 숨은 종단도 파생값 · 배지 자릿수. * node helper_272_value_hold.mjs <묶음.mjs> * vite ssr 로 묶은 진짜 모듈(helper_272_entry.ts) · 표준출력 JSON 한 벌. * 짝 = `test_272_history_empty_cells.py` · `test_272_hidden_summary.py`. */ 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(), }; }; // 손길 듣기(pointerdown · keydown)가 붙는 창 — 사진첩 시험(346-4)과 같은 꼴 const win = new EventTarget(); win.setTimeout = setTimeout; win.clearTimeout = clearTimeout; win.sessionStorage = store(); win.localStorage = store(); win.innerHeight = 900; win.matchMedia = () => ({ matches: false, addEventListener: () => undefined }); win.requestAnimationFrame = (run) => setTimeout(run, 0); globalThis.window = win; globalThis.screen = {}; globalThis.sessionStorage = win.sessionStorage; globalThis.localStorage = win.localStorage; globalThis.requestAnimationFrame = win.requestAnimationFrame; const any = new Proxy(function () {}, { get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any), apply: () => any, construct: () => any, }); globalThis.document = any; localStorage.setItem("frd_current_project_id", "p1"); const m = await import(pathToFileURL(process.argv[2]).href); const out = {}; const S = sessionStorage; const key = (name) => `aislo:draft:${name}:p1:7`; const SIX = ["basinadjust", "crossw", "extraspan", "extrawall", "inletstruct", "revetx"]; const touch = () => win.dispatchEvent(new Event("keydown")); /* ── H1 · 되돌린 뒤 빈 칸이 생김(R4) — 「{}」 · 「null」 · 「""」 쓰기는 걸음 아님 · ↷ 남음 ── */ { const h = m.createProfileHistory(() => {}); touch(); S.setItem(key("alignment-draft"), JSON.stringify({ station_offsets: { 100: 1 } })); h.record(); const edited = h.canUndo(); h.undo(); const undone = { undo: h.canUndo(), redo: h.canRedo(), grade: S.getItem(key("alignment-draft")) }; // B06 로 넘어가는 누름(손길) 뒤 첫 마운트 · 갈아끼움 이동(moveSessionMaps)이 빈 맵 여섯을 씀 · 다른 칸은 null · 빈 글 touch(); SIX.forEach((name) => S.setItem(key(name), "{}")); S.setItem(key("rockb"), "null"); S.setItem(key("cutslope"), ""); h.record(); out.h1 = { edited, undone, after: { undo: h.canUndo(), redo: h.canRedo() } }; // ↷ = 사용자 조작 그대로 한 걸음 out.h1.redone = h.redo(); out.h1.redoGrade = S.getItem(key("alignment-draft")); out.h1.final = { undo: h.canUndo(), redo: h.canRedo() }; S.clear(); localStorage.setItem("frd_current_project_id", "p1"); } /* ── H2 · 「[]」 = 목록 초안(관 · 구조물 모두 지움) — 걸음 그대로 · 되돌리면 칸 없음 ── */ { const h = m.createProfileHistory(() => {}); touch(); S.setItem("aislo:draft:pipes:p1", "[]"); h.record(); const step = h.canUndo(); h.undo(); out.h2 = { step, undonePipes: S.getItem("aislo:draft:pipes:p1") }; h.redo(); out.h2.redonePipes = S.getItem("aislo:draft:pipes:p1"); S.clear(); localStorage.setItem("frd_current_project_id", "p1"); } /* ── H3 · 빈 칸이 있던 걸음으로 되돌림 = 빈 칸(없음과 같음) · 값 칸은 그대로 ── */ { S.setItem(key("crossw"), "{}"); S.setItem(key("revetx"), JSON.stringify({ "100.00:inlet": { x: 1 } })); const h = m.createProfileHistory(() => {}); touch(); S.setItem(key("crossw"), JSON.stringify({ "100.00": 6 })); h.record(); const step = h.canUndo(); h.undo(); out.h3 = { step, crossw: S.getItem(key("crossw")), revetx: S.getItem(key("revetx")) }; S.clear(); localStorage.setItem("frd_current_project_id", "p1"); } /* ── H4 · 세션 칸 창구(472-5 5-D · 메모리 맵 없음) — 빈 쓰기 = 칸 지움 · 값 있으면 씀 · 연동 두 겹 ── */ { const width = m.sessionCells( () => key("crossw"), (v) => (typeof v === "number" ? v : undefined), ); width.clear(); const emptyWrite = S.getItem(key("crossw")); width.set("100.00", 6); const filledWrite = S.getItem(key("crossw")); width.delete("100.00"); const clearedWrite = S.getItem(key("crossw")); const detached = m.sessionCells( () => key("revetlink"), (v) => !!v, "detached", ); const grade = m.sessionCells( () => key("revetlink"), (v) => !!v, "grade", ); detached.set("100.00", true); grade.set("100.00", false); const linkBoth = S.getItem(key("revetlink")); detached.clear(); const linkGrade = S.getItem(key("revetlink")); grade.clear(); out.h4 = { emptyWrite, filledWrite, clearedWrite, linkBoth, linkGrade, linkCleared: S.getItem(key("revetlink")), }; S.clear(); localStorage.setItem("frd_current_project_id", "p1"); } /* ── 숨은 종단도(본문 0 크기) — 낡음 예약 · 유토곡선 셈 · 배분 선반입 여전히 · 펼침과 같은 값 ── */ const samples = [0, 20, 40, 60, 80, 100].map((c) => ({ chainage_m: c, elevation_m: 100 + c * 0.05, valid: true, })); const plan = [0, 20, 40, 60, 80, 100].map((c) => ({ chainage_m: c, elevation_m: 100 + c * 0.04 })); const design = (c, cut, fill, z) => ({ station_id: `station_${c}`, chainage_m: c, design: { cut_area_m2: cut, fill_area_m2: fill, ground_type: "soil", design_elevation_m: z, }, }); const detail = { longitudinal: { length_m: 100, samples, stations: samples.map((s) => ({ station_id: `station_${s.chainage_m}`, chainage_m: s.chainage_m, })), design_profiles: [{ id: "design_grade_line", samples: plan }], }, cross_sections: [ design(0, 2, 0, 100), design(20, 3, 1, 100.8), design(40, 1, 4, 101.6), design(60, 0, 6, 999), // 낡음 — 계획선과 어긋난 측점(다시 셈 예약 대상) design(80, 2, 2, 103.2), design(100, 4, 0, 104), ], }; const alignment = { samples: plan, edits: { station_offsets: {}, curve_radii: {} }, stations: plan.map((p) => ({ chainage_m: p.chainage_m, plan_elevation_m: p.elevation_m, kind: "regular", })), curves: [], balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 }, policy: { station_interval_m: 20, edit_step_m: 0.1 }, }; const ONES = { soil: { loose: 1, compacted: 1 }, ripping_rock: { loose: 1, compacted: 1 }, blasting_rock: { loose: 1, compacted: 1 }, }; function ctxFor(width, rec) { const body = { clientWidth: width, clientHeight: width ? 400 : 0, scrollLeft: 0, parentElement: null, replaceChildren: () => (rec.painted += 1), addEventListener: () => undefined, }; return { body, onStaleDesigns: (k) => (rec.stale.push(k), false), callbacks: undefined, massHaul: { draw: (p) => rec.mass.push(p) }, tableOverlay: { isOpen: () => false, contentHeight: () => 0, setTable: () => undefined }, store: { edits: () => ({}), savedRadii: () => ({}), resetStation: () => undefined }, detail: () => detail, alignment: () => alignment, base: () => null, stationInterval: () => 20, irregularStations: () => [], minCoverTargets: () => [], structures: () => [], structureTypes: () => [], selectedStationId: () => null, setSelectedStationId: () => undefined, selectedStructureId: () => null, setSelectedStructureId: () => undefined, stationDisplay: () => ({ station: 0, cumulative: 0 }), setLastSize: () => (rec.sized += 1), renderBalance: () => (rec.balance += 1), applyHeightCascade: () => ({ chartHeight: 300 }), clearMainDragCooldown: () => undefined, selectStation: () => undefined, applyEdits: () => undefined, handleToolPick: () => false, zoom: () => ({ x: 1 }), holdElevationRange: (next) => next ?? undefined, toolActive: () => false, tools: { selectedRuns: () => [], mountOverlay: () => undefined }, stationIdAtStructure: () => null, redraw: () => undefined, setElevationWindowUpdater: () => undefined, }; } const pick = (p) => p && { pendingRecalc: p.pendingRecalc, staleChainages: p.staleChainages, crossSections: p.crossSections.length, longitudinal: p.longitudinal, stations: p.stationSource.stations.map((s) => s.chainage_m), maxChainageM: p.axis.maxChainageM, }; const hiddenRec = { stale: [], mass: [], painted: 0, sized: 0, balance: 0 }; m.renderProfile(ctxFor(0, hiddenRec)); const shownRec = { stale: [], mass: [], painted: 0, sized: 0, balance: 0 }; let shownError = null; try { m.renderProfile(ctxFor(1200, shownRec)); } catch (error) { shownError = String(error?.stack ?? error); } out.hidden = { stale: hiddenRec.stale, mass: pick(hiddenRec.mass[0]), massCount: hiddenRec.mass.length, painted: hiddenRec.painted, sized: hiddenRec.sized, }; out.shown = { stale: shownRec.stale, mass: pick(shownRec.mass[0]), massCount: shownRec.mass.length, painted: shownRec.painted, error: shownError, }; out.expectedStale = m.staleKey({ longitudinal: { design_profiles: [{ samples: plan }] }, cross_sections: detail.cross_sections, }); /* 유토곡선 판(진짜) — 판 접힘 · 펼침인데 본문 숨음(자리 0) = 총괄값 · 배분 선반입만 · 그림 없음 */ function massPanel(open) { localStorage.setItem("aislo:pref:masshaul-open", String(open)); const rec = { summary: [], scheduled: 0 }; const prefetch = { schedule: () => (rec.scheduled += 1), current: () => null, }; const panel = m.createRouteMassHaulPanel( () => undefined, (s) => rec.summary.push(s), prefetch, ); panel.setContext({ conversion: ONES }); return { panel, rec }; } const series = m.computeMassHaulSeries( { length_m: 100, design_profiles: [{ samples: plan }] }, detail.cross_sections, ONES, undefined, ["cross"], ); const finalM3 = series[0]?.result.points.at(-1)?.cumulative_volume_m3 ?? null; const massParams = hiddenRec.mass[0]; out.panel = {}; for (const open of [false, true]) { const { panel, rec } = massPanel(open); let error = null; try { if (massParams) panel.draw({ ...massParams, pendingRecalc: false }); } catch (e) { error = String(e?.stack ?? e); } out.panel[open ? "open" : "closed"] = { summary: rec.summary.map((s) => s?.finalM3 ?? null), scheduled: rec.scheduled, error, }; } out.finalM3 = finalM3; /* ── 배지 표기 — 소수 한 자리 고정 ── */ { const make = () => { const el = { textContent: "", title: "", hidden: false, style: { setProperty: () => undefined }, classList: { toggle: () => undefined, add: () => undefined }, append: () => undefined, className: "", }; return el; }; const made = []; globalThis.document = { createElement: () => (made.push(make()), made.at(-1)) }; const badge = m.createMassHaulBadge(); const texts = []; for (const v of [-17570, -17570.7, 1234.56, 0]) { badge.set({ finalM3: v, cutNaturalM3: 0, cutCompactedM3: 0, fillCompactedM3: 0, surplusM3: 0, shortageM3: 0, }); texts.push(made[2].textContent); } out.badge = texts; globalThis.document = any; } process.stdout.write(JSON.stringify(out)); process.exit(0);