/* 349 배수유역 자동/수동 흐름 헬퍼 — 화면 파일 `B05_Profile_UI_Drainage_Live.ts` · `_Run.ts` · * `_Suggest.ts` 를 그 자리에서 트랜스파일해 가짜 DOM · 시계 · fetch 로 돌리고 단계별 상태를 JSON 으로 냄. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const B05 = path.join(__dirname, "..", "..", "B05_Profile"); /* ---- 가짜 DOM ---- */ function el(tag) { const set = new Set(); return { tag, children: [], hidden: false, className: "", textContent: "", title: "", disabled: false, checked: false, listeners: {}, classList: { toggle: (name, on) => (on ? set.add(name) : set.delete(name)), add: (name) => set.add(name), contains: (name) => set.has(name), }, append(...kids) { this.children.push(...kids); }, before(kid) { this.beforeNode = kid; }, replaceChildren(...kids) { this.children = kids; }, addEventListener(type, fn) { (this.listeners[type] ??= []).push(fn); }, fire(type) { (this.listeners[type] ?? []).forEach((fn) => fn()); }, }; } global.document = { createElement: el }; /* ---- 가짜 시계 ---- */ let clock = 0; let timerId = 0; const timers = new Map(); global.window = { setTimeout(fn, ms) { timers.set(++timerId, { fn, at: clock + ms }); return timerId; }, clearTimeout(id) { timers.delete(id); }, }; function advance(ms) { clock += ms; for (const [id, timer] of [...timers]) { if (timer.at <= clock) { timers.delete(id); timer.fn(); } } } /* ---- 가짜 fetch(배수유역 live) ---- */ const calls = []; global.fetch = (url, init) => new Promise((resolve, reject) => { const call = { url, body: JSON.parse(init.body), aborted: false }; call.answer = (payload) => resolve({ ok: true, status: 200, json: async () => payload }); init.signal.addEventListener("abort", () => { call.aborted = true; reject(new DOMException("aborted", "AbortError")); }); calls.push(call); }); const flush = async () => { for (let i = 0; i < 10; i += 1) await new Promise((r) => setImmediate(r)); }; /* ---- 모듈 읽기(그 자리 트랜스파일 · 바깥 모듈은 가짜) ---- */ const state = new Map(); const detailCalls = []; let modeRow = 1; // ⚙ 창에서 고를 줄 — 0 수동 · 1 자동 const stubs = { "@config/config_frontend": { API_BASE_URL: "/api" }, "@ui/ui_template_progress": { createProgressCircle: () => ({ root: el("progress") }) }, // ⚙ 창 — 둘째 줄(자동) 라디오를 고름 "@ui/ui_template_modal": { openModal: (options) => { const body = el("modal"); options.mount(body, () => undefined); body.children[modeRow].children[0].fire("change"); }, }, "../A00_Common/b_page_state": { readStateRaw: (key) => state.get(key) ?? null, writeStateRaw: (key, value) => state.set(key, value), }, "../B04_PreProcess/B04_PreProcess_Api_Fetch": { computeDetailBasins: async (projectId, points) => { detailCalls.push(points); return { basins: [], pipe_points: points, stale: null }; }, }, "./B05_Profile_UI_Drainage_Parts": { analyzeFailed: (_error, fallback) => fallback }, }; const cache = new Map(); function load(file) { if (cache.has(file)) return cache.get(file); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(file, box.exports); const local = (name) => { if (stubs[name]) return stubs[name]; if (name.startsWith(".")) return load(path.join(path.dirname(file), `${name}.ts`)); return require(name); }; new Function("exports", "module", "require", js)(box.exports, box, local); return box.exports; } const { createDrainageLive } = load(path.join(B05, "B05_Profile_UI_Drainage_Live.ts")); const { createDetailRunner } = load(path.join(B05, "B05_Profile_UI_Drainage_Run.ts")); async function main() { const out = {}; const viewport = el("viewport"); const header = el("header"); const root = el("root"); root.querySelector = (selector) => selector === ".b05-drainage__viewport" ? viewport : selector === ".b05-drainage__header" ? header : null; let pipes = [ { chainage_m: 100, id: "a1", source: "stream", options: { pipe_diameter_mm: 1000 } }, ]; let applied = 0; let savedLoads = 0; let savedAnswer = null; let remembered = 0; let live; const status = el("status"); const runner = createDetailRunner({ projectId: () => "p1", analyzeButton: el("button"), status, showProgress: () => undefined, apply: (response) => { applied += 1; live.suggest.update(response); if (response.stale) live.markStale(); // 패널 apply 와 같은 줄 }, basinCount: () => 0, scheduleDraw: () => undefined, label: (key) => key, }); live = createDrainageLive({ root, basinList: el("list"), pipeRequest: () => pipes, runner, rememberPipes: () => (remembered += 1), // 저장분 다시 받기(349-0i) — 답은 시험이 정할 때 옴 loadSaved: () => { savedLoads += 1; return runner.run(() => new Promise((resolve) => (savedAnswer = resolve)), true); }, }); const [veil, busy] = viewport.children; const update = veil.children[0]; const gear = header.children[0]; const look = () => ({ stale: live.stale, blur: root.classList.contains("is-stale"), veil: !veil.hidden, busy: !busy.hidden, fetches: calls.length, applyOff: live.suggest.root.children[0].children[1].disabled, applyTitle: live.suggest.root.children[0].children[1].title, }); const draft = (rev, x) => ({ rev, nodes: [ [0, 0], [x, 300], ], curveOn: [true, true], }); const stage = (route) => live.stage({ inputs: { route, pipes } }); const answer = (call, extra = {}) => call.answer({ status: "success", generation: call.body.generation, basins: [{ chainage_m: 100, recommended_facility: "pipe", recommended_diameter_mm: 1200 }], pipe_points: pipes, pipe_candidates: [{ chainage_m: 200 }], stale: null, live: true, ...extra, }); // 수동 — 꼭짓점 옮김 = 흐림 + [업데이트] · 판 번호만 바뀐 같은 노선 = 그대로 stage(draft(1, 50)); out.moved = look(); stage(draft(2, 50)); out.sameAgain = look(); // [업데이트] → 요청 하나 · 묶음이 같은 입력으로 다시 돌아도 답을 받음 update.fire("click"); out.sent = { ...look(), body: calls[0].body }; stage(draft(3, 50)); out.rerunWhileBusy = { ...look(), aborted: calls[0].aborted }; answer(calls[0]); await flush(); out.answered = { ...look(), applied }; out.suggest = { shown: !live.suggest.root.hidden, title: live.suggest.root.children[0].children[0].textContent, rows: live.suggest.root.children[1].children.map((row) => row.textContent), }; // 제안 [적용] — 관 초안으로 세부 유역 다시 나눔 · 세션 초안에 담음 live.suggest.root.children[0].children[1].fire("click"); await flush(); out.applied = { points: detailCalls[0], remembered }; // 다시 옮김 → 요청 → 그 사이 또 옮김 = 앞 요청 끊김 · 다시 흐림 stage(draft(4, 80)); update.fire("click"); stage(draft(5, 90)); out.movedWhileBusy = { ...look(), aborted: calls[1].aborted }; // 낡은 동안 제안 [적용] — 끔 · 눌러도 옛 제안이 안 들어감 live.suggest.root.children[0].children[1].fire("click"); await flush(); out.staleApply = detailCalls.length; update.fire("click"); answer(calls[2]); await flush(); out.fresh = look(); // 노선 [저장] 뒤 — 서버 답이 「낡음」 = 흐림 + [업데이트](수동) stage(null); await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); out.savedStale = { ...look(), status: status.textContent }; // ⚙ 자동 — 바꾸는 순간 3 s 뒤 셈 · 흐림 막 · [업데이트] 없음 gear.fire("click"); out.autoSwitch = look(); advance(2999); out.auto2999 = look(); advance(1); out.auto3000 = { ...look(), route: calls[3].body.route }; answer(calls[3]); await flush(); out.autoDone = look(); // 자동 · 꼭짓점 옮김 = 조용히 3 s 뒤 · 그 사이 또 옮기면 처음부터 stage(draft(6, 120)); advance(2000); stage(draft(7, 130)); advance(2000); out.autoRestart = look(); advance(1000); answer(calls[4]); await flush(); out.autoMoved = { ...look(), mode: state.get("drainage-mode") }; // 자동 · 저장 뒤 낡음 = 3 s 뒤 조용히 await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); out.autoSavedStale = look(); advance(3000); answer(calls[5]); await flush(); out.autoSavedDone = look(); // 349-0i 되돌리기 — 자동: 끌기 → 낡음 → Ctrl+Z(초안 없어짐) = 3 s 셈 대신 저장분 const savedOk = { basins: [], pipe_points: pipes, stale: null }; stage(draft(20, 300)); advance(1000); stage(null); out.autoUndoWaiting = { ...look(), savedLoads }; advance(5000); out.autoUndoNoFetch = look(); savedAnswer(savedOk); await flush(); out.autoUndo = { ...look(), savedLoads }; // 수동: 끌기 → [업데이트] 요청 중 → Ctrl+Z = 초안용 답 끊고 저장분 · 흐림은 답 올 때까지 modeRow = 0; gear.fire("click"); stage(draft(21, 310)); update.fire("click"); const draftCall = calls[calls.length - 1]; stage(null); out.undoWaiting = { ...look(), aborted: draftCall.aborted, savedLoads }; savedAnswer(savedOk); await flush(); out.undo = { ...look(), savedLoads }; // 저장분 자체가 낡음 = 흐림 · [업데이트] 그대로 stage(draft(22, 320)); stage(null); savedAnswer({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" }); await flush(); out.undoSavedStale = { ...look(), savedLoads, mode: state.get("drainage-mode") }; // 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(cm) · id 는 봄 const { inputKey } = load(path.join(B05, "logic", "B05_Profile_Drainage_Live.ts")); const a = { id: "a1", chainage_m: 100 }; const b = { id: "b2", chainage_m: 250 }; const base = inputKey(draft(1, 50), [a, b]); out.keys = { rev: inputKey(draft(9, 50), [a, b]) === base, order: inputKey(draft(1, 50), [b, a]) === base, node: inputKey(draft(1, 51), [a, b]) === base, pipe: inputKey(draft(1, 50), [a, { ...b, chainage_m: 250.2 }]) === base, id: inputKey(draft(1, 50), [a, { ...b, id: "c3" }]) === base, saved: inputKey(null, null) === inputKey(undefined, undefined), }; process.stdout.write(JSON.stringify(out)); } main().catch((error) => { process.stderr.write(String(error && error.stack ? error.stack : error)); process.exit(1); });