/* 346-4 되돌리기 한 줄 헬퍼 — 진짜 `_Profile_History`(페이지 사진첩) · `_RouteEdit_History`(편집기 단추) · * `_RouteEdit_Draft`(다리 감싸기)를 트랜스파일해 가짜 창 · 세션에서 돌림. 다리 = 세션 칸 route-draft 를 * 바로 쓰는 흉내(`design_pipeline_page` 와 같은 때). 결과 JSON 한 벌(표준출력). */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const data = new Map(); globalThis.sessionStorage = { get length() { return data.size; }, key: (i) => [...data.keys()][i] ?? null, getItem: (k) => (data.has(k) ? data.get(k) : null), setItem: (k, v) => data.set(k, String(v)), removeItem: (k) => data.delete(k), }; const win = new EventTarget(); win.setTimeout = setTimeout; win.clearTimeout = clearTimeout; globalThis.window = win; const cache = new Map(); 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) => request.startsWith(".") ? loadTs(`${path.resolve(path.dirname(full), request)}.ts`) : require(request); new Function("exports", "module", "require", js)(box.exports, box, localRequire); return box.exports; } const page = path.join(ROOT, "B05_Profile"); const album = loadTs(path.join(page, "B05_Profile_UI_Profile_History.ts")); const edit = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_History.ts")); const draftLink = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Draft.ts")); const P = "p1"; const ROUTE = `aislo:draft:route-draft:${P}`; const GRADE = `aislo:draft:alignment-draft:${P}:7`; const CROSS = `aislo:draft:crossdesign:${P}:7`; const SELECT = `aislo:draft:selection:${P}`; const tick = (ms = 5) => new Promise((resolve) => setTimeout(resolve, ms)); const snap = (nodes, lock = false) => ({ planned: nodes, curveOn: nodes.map(() => true), curveRadius: nodes.map(() => null), curveLock: nodes.map(() => null), curveArc: nodes.map(() => null), apexLock: nodes.map((_, i) => lock && i === 1), picked: 1, rec: lock ? "tag-moved" : "tag-open", }); (async () => { const out = {}; let profileReads = 0; const pageAlbum = album.createProfileHistory(() => (profileReads += 1)); const opened = snap([ [0, 0], [50, 0], [100, 0], ]); const raw = edit.createRouteEditHistory(opened); const bridge = { saved: () => JSON.parse(sessionStorage.getItem(ROUTE) ?? "null"), edited: (draft) => draft ? sessionStorage.setItem(ROUTE, JSON.stringify({ ...draft, rev: 1, terrain: null })) : sessionStorage.removeItem(ROUTE), }; const history = draftLink.watch(raw, bridge, () => false); const clicks = {}; const button = (act) => ({ disabled: false, addEventListener: (_type, fn) => (clicks[act] = fn), }); const buttons = { '[data-act="undo"]': button("undo"), '[data-act="redo"]': button("redo") }; const editor = []; // 편집기에 되살린 걸음(가운데 노드 x · 고정 · 꼬리표 · 글) const controls = edit.bindHistoryControls({ overlay: { querySelector: (sel) => buttons[sel] }, getHistory: () => history, restore: (s, message) => editor.push([s.planned[1][0], s.apexLock[1], s.rec, message]), }); const state = () => ({ route: bridge.saved()?.nodes?.[1]?.[0] ?? null, grade: sessionStorage.getItem(GRADE), cross: sessionStorage.getItem(CROSS), undo: !buttons['[data-act="undo"]'].disabled, redo: !buttons['[data-act="redo"]'].disabled, }); controls.sync(); out.start = state(); // ① 평면 점 옮김 — 끄는 동안 두 번 쌓아도 놓을 때 한 걸음 win.dispatchEvent(new Event("pointerdown")); history.commit( snap([ [0, 0], [60, 5], [100, 0], ]), ); history.commit( snap( [ [0, 0], [70, 5], [100, 0], ], true, ), ); out.heldUndo = pageAlbum.canUndo(); win.dispatchEvent(new Event("pointerup")); await tick(); // ② 계획고 ▲ — 종단도가 쌓음 sessionStorage.setItem(GRADE, '{"station_offsets":{"20.000":0.1}}'); pageAlbum.record(); // 고름만 바뀜 — 사진 밖이라 걸음 안 늘어남 sessionStorage.setItem(SELECT, '{"chainageM":40}'); pageAlbum.record(); // ③ 횡단 카드 고침 — 쌓는 줄 없음 · 칸 바꿈(change) 뒤 기다려 한 걸음 sessionStorage.setItem(CROSS, '{"20.000":{"ground_type":"soil"}}'); win.dispatchEvent(new Event("change")); await tick(450); out.afterEdits = state(); // Ctrl+Z 세 번 — 거꾸로(종단도 단축키 = 사진첩 undo) · 셋째는 편집기 [되돌리기] 단추 pageAlbum.undo(); out.undo1 = state(); pageAlbum.undo(); out.undo2 = state(); out.editorBeforeRouteUndo = editor.length; clicks.undo(); out.undo3 = state(); out.editorAfterUndo = [...editor]; // 다시하기 세 번 — 첫째는 편집기 [다시하기] · 나머지는 Ctrl+Y clicks.redo(); out.redo1 = state(); pageAlbum.redo(); pageAlbum.redo(); out.redo3 = state(); out.editorAfterRedo = editor.slice(out.editorAfterUndo.length); out.profileReads = profileReads; controls.dispose(); process.stdout.write(JSON.stringify(out)); })();