/* PLAN 319 단위검증 헬퍼 — 정본이 다른 종류로 바꿈(관 지점 ↔ 구조물 · 맹암거 · 개거) = 옛 것 지우고 같은 * 측점에 새 종류(패널 입력 배선) · 일반 측점 자리(grid) 종류 바꿈 · 지움 = 구조물만 뺀 사본에 새 종류(횡단 초안 맞춤). * 진짜 TS 를 그 자리에서 트랜스파일해 Node 로 돌림(서버 · 무거운 화면 모듈은 흉내) · 결과 = 표준출력 JSON 한 벌. * 짝 = `test_319_kind_swap.py`. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`); const B06 = (name) => path.join(ROOT, "B06_Section", `${name}.ts`); const TYPES = JSON.parse( fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"), ).types; class FakeStorage { constructor() { this.map = new Map(); } get length() { return this.map.size; } key(index) { return [...this.map.keys()][index] ?? null; } getItem(key) { return this.map.has(key) ? this.map.get(key) : null; } setItem(key, value) { this.map.set(key, String(value)); } removeItem(key) { this.map.delete(key); } } globalThis.window = { sessionStorage: new FakeStorage(), localStorage: new FakeStorage(), addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => true, requestAnimationFrame: (fn) => fn(), setTimeout, clearTimeout, }; globalThis.sessionStorage = window.sessionStorage; globalThis.localStorage = window.localStorage; globalThis.requestAnimationFrame = (fn) => fn(); globalThis.document = { activeElement: null, addEventListener: () => {} }; const anything = () => new Proxy({}, { get: (_t, key) => (key === "__esModule" ? false : () => undefined) }); /** `real` 에 든 파일만 진짜로 읽고 그 밖 상대 경로는 흉내 · `stubs` 는 지정자 그대로 갈음. */ function makeLoader(real, stubs = {}) { const cache = new Map(); const realSet = new Set(real.map((file) => path.resolve(file))); const load = (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 need = (spec) => { if (CATALOG.isCatalogRequest(spec)) return CATALOG; if (spec in stubs) return stubs[spec]; let target = null; if (spec.startsWith("@util/")) target = path.join(ROOT, "common_util", `${spec.slice(6)}.ts`); else if (spec.startsWith(".")) target = path.join(path.dirname(full), `${spec}.ts`); if (target && realSet.has(path.resolve(target))) return load(target); return anything(); }; new Function("exports", "module", "require", js)(box.exports, box, need); return box.exports; }; return load; } const out = {}; // ── 1. 패널 종류 바꿈(319-2) — 정본 다른 종류 = 지우고 같은 측점 addAt ───────────── const part1 = (async () => { const load = makeLoader( [ B05("B05_Profile_UI_Structures_Panel_Events"), B05("B05_Profile_Api_Structures"), B06("B06_Section_Api_Culvert_Options"), path.join(ROOT, "A00_Common", "b_page_state.ts"), path.join(ROOT, "A00_Common", "design", "design_selection.ts"), path.join(ROOT, "common_util", "common_util_summary_item.ts"), ], { "./logic/B05_Profile_UI_Structures_Define": { asRoad: (record) => record }, "@ui/ui_template_elements": { showConfirmDialog: async () => true, showToast: (text, kind) => toasts.push([text, kind ?? null]), }, "./B06_Section_Section_Drafts": { forgetPreviewAdjusts: () => {} }, }, ); const toasts = []; const { bindStructuresEvents } = load(B05("B05_Profile_UI_Structures_Panel_Events")); const typeMap = new Map(TYPES.map((type) => [type.type_id, type])); const el = () => ({ value: "", listeners: {}, addEventListener(t, fn) { this.listeners[t] = fn; }, }); const fields = () => ({ station: el(), remainder: el(), wrap: { hidden: false } }); // commitOk = [수정] 반영이 받아 줌(패널은 기록을 새 것으로 갈아 끼움) · false = 막힘(필수 칸 등 · 323-6 알림) // addOk = 새 종류가 기본값으로 바로 놓임(패널 `addAt` 이 true) · false = 빈 칸 때문에 폼만(323-6 고침) const run = async ({ structures = [], pipes = [], editing = null, pipeAt = null, next, commitOk = true, addOk = true, }) => { toasts.length = 0; const calls = { loaded: [], removed: [], addAt: [], emit: 0, live: 0, reset: 0, editingSet: [], pipeSet: [], }; let editingId = editing; let selectedPipe = pipeAt; const ctx = { groupSelect: el(), typeSelect: el(), primary: el(), removeButton: el(), resetButton: el(), positionRow: el(), memoField: el(), optionRow: el(), optionInputs: [], anchorFields: fields(), startFields: fields(), endFields: fields(), facilityOptions: { root: el(), readOptions: () => ({}) }, groupBoxes: { shapes: () => ({}), get: () => ({}), set: () => {}, setShapes: () => {} }, structures, projectId: "P", callbacks: { onPipeRemove: (at) => calls.removed.push(at), onListRelease: () => {}, }, pipeFacilities: () => pipes, types: () => TYPES, typeMap: () => typeMap, currentType: () => (next ? typeMap.get(next) : null), editingId: () => editingId, setEditingId: (value) => { editingId = value; calls.editingSet.push(value); }, selectedPipeChainage: () => selectedPipe, setSelectedPipeChainage: (value) => { selectedPipe = value; calls.pipeSet.push(value); }, summary: () => null, setSummary: () => {}, cancelTempPipe: () => {}, resetForm: () => (calls.reset += 1), syncTypeOptions: () => {}, syncPlacementFields: () => {}, renderOptionFields: () => {}, syncFacilityForm: () => {}, liveCommit: () => { calls.live += 1; const at = structures.findIndex((s) => s.structure_id === editingId); if (commitOk && at >= 0) structures[at] = { ...structures[at] }; }, loadForm: (target) => calls.loaded.push(target?.type_id ?? null), emit: () => (calls.emit += 1), addAt: async (at, typeId) => (calls.addAt.push([at, typeId]), addOk), selectPipe: (at) => calls.loaded.push(`pipe@${at}`), interval: () => 20, }; bindStructuresEvents(ctx); ctx.typeSelect.listeners.change(); await new Promise((resolve) => setTimeout(resolve)); // 바꿈(지우고 새로)은 놓인 뒤에 옛 것을 지움 return { ...calls, left: structures.map((s) => s.type_id), toasts: [...toasts] }; }; const store = window.sessionStorage; const kindreset = () => JSON.parse(store.getItem("aislo:draft:kindreset:P") ?? "null"); store.setItem( "aislo:draft:culvertopt:P:1", JSON.stringify({ "80.00": { a: 1 }, "300.00": { b: 2 } }), ); out.pipe_to_underdrain = { ...(await run({ pipes: [{ chainage_m: 80, facility: "pipe" }], pipeAt: 80, next: "cross_underdrain", })), kindreset: kindreset(), culvertopt: JSON.parse(store.getItem("aislo:draft:culvertopt:P:1")), }; // 새로 놓인 맹암거(같은 측점) → 처음 종류(배관)로 되돌림 = 비울 자리 기록이 빠짐(318-4 규칙) out.underdrain_back_to_pipe = { ...(await run({ structures: [ { structure_id: "n1", type_id: "cross_underdrain", placement: "point", chainage_m: 80 }, ], editing: "n1", next: "pipe", })), kindreset: kindreset(), }; out.open_ditch_to_box = { ...(await run({ structures: [ { structure_id: "s1", type_id: "sign", placement: "point", chainage_m: 10 }, { structure_id: "s9", type_id: "open_ditch", placement: "point", chainage_m: 120 }, ], editing: "s9", next: "box_culvert", })), kindreset: kindreset(), }; out.pipe_to_box = await run({ pipes: [{ chainage_m: 200, facility: "pipe" }], pipeAt: 200, next: "box_culvert", }); out.underdrain_to_open_ditch = await run({ structures: [ { structure_id: "s2", type_id: "cross_underdrain", placement: "point", chainage_m: 60 }, ], editing: "s2", next: "open_ditch", }); out.to_blank = await run({ pipes: [{ chainage_m: 200, facility: "pipe" }], pipeAt: 200, next: null, }); // 323-6 — E · F 점형 ↔ 구간형 = 지우고 같은 기준 측점에 새로(구간 = 기본 길이는 `addAt`) · 같은 자리꼴 = 제자리 out.point_to_interval = await run({ structures: [{ structure_id: "m1", type_id: "mirror", placement: "point", chainage_m: 140 }], editing: "m1", next: "guardrail", }); out.interval_to_point = await run({ structures: [ { structure_id: "g1", type_id: "guardrail", placement: "interval", start_m: 100, chainage_m: 105, end_m: 120, }, ], editing: "g1", next: "mirror", }); out.interval_same = await run({ structures: [ { structure_id: "r1", type_id: "refuge", placement: "interval", start_m: 100, chainage_m: 105, end_m: 120, }, ], editing: "r1", next: "work_yard", }); // 바꿈(지우고 새로)인데 새 종류를 바로 못 놓음(빈 칸) = 옛 것 · 옛 폼 그대로 + 알림(323-6 고침 · 반사경 → 가드레일) out.replace_blocked = await run({ structures: [{ structure_id: "m2", type_id: "mirror", placement: "point", chainage_m: 300 }], editing: "m2", next: "guardrail", addOk: false, }); out.replace_blocked_pipe = await run({ pipes: [{ chainage_m: 200, facility: "pipe" }], pipeAt: 200, next: "cross_underdrain", addOk: false, }); // 막힘 = 옛 기록 · 옛 종류 폼으로 되돌리고 알림(조용한 되돌림 없앰) out.blocked = await run({ structures: [{ structure_id: "b1", type_id: "mirror", placement: "point", chainage_m: 30 }], editing: "b1", next: "curb", commitOk: false, }); })(); // ── 2. 일반 측점 자리(grid) — 종류 바꿈 = 새로 뜬 횡단 · 다 빠짐 = 시설 뺀 사본(319-2) ────── const state = { draftPipes: null, draftStructures: null, savedPipes: [], savedStructures: [] }; (async () => { await part1; const load = makeLoader( [ B06("B06_Section_Section_Drafts"), path.join(ROOT, "common_util", "common_util_culvert_sets.ts"), path.join(ROOT, "common_util", "common_util_structure_station.ts"), // 그림 자료 = 형상 얹기 한 벌(323-4) ...["attach", "shape_keys", "walls", "pavement", "underdrain"].map((name) => path.join(ROOT, "common_util", `common_util_structure_${name}.ts`), ), ], { "../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({ pipe_points: state.savedPipes }), }, "../B05_Profile/B05_Profile_Api_Structures": { fetchStructures: async () => ({ structures: state.savedStructures }), fetchStructureTypes: async () => TYPES, readPendingStructures: () => state.draftStructures, }, "../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes }, "./B06_Section_Api_Fetch": { previewStructureSections: async (_p, chainages) => chainages.map((at) => ({ chainage_m: at, kind: "irregular", design: { fresh: true } })), }, }, ); const drafts = load(B06("B06_Section_Section_Drafts")); const { forgetDraftStations } = drafts; // 상세 맞춤 = 측점 계획 → 형상 얹기(`Section_Store.withDraftWalls` 와 같은 차례) const withDraftStations = async (detail, projectId, routeId) => { await drafts.withDraftStations(detail, projectId, routeId); await drafts.withDraftShapes(detail, projectId); }; const D800 = { pipe_kind: "파형강관", pipe_diameter_mm: "800" }; const detailWith = (spec) => ({ cross_sections: [ { chainage_m: 20, kind: "regular", design: {} }, { chainage_m: 40, kind: "irregular", grid: true, design: { keep: 1 }, ...spec }, { chainage_m: 60, kind: "regular", design: {} }, ], }); const at40 = (detail) => { const section = detail.cross_sections.find((s) => Math.abs(s.chainage_m - 40) < 0.01); return { count: detail.cross_sections.length, keys: Object.keys(section).filter((k) => ["culvert", "box", "ford", "ford_pavement"].includes(k), ), structure: section.structure ?? null, preview: Boolean(section.preview), fresh: Boolean(section.design?.fresh), keep: section.design?.keep ?? null, }; }; const grid = {}; // 저장분 = 격자 40 위 배관 state.savedPipes = [{ chainage_m: 40, facility: "pipe", options: D800 }]; state.savedStructures = []; let detail = detailWith({ structure: "파형강관 D800", culvert: { chainage_m: 40 } }); state.draftPipes = []; state.draftStructures = [ { structure_id: "n1", type_id: "cross_underdrain", placement: "point", chainage_m: 40, options: {}, }, ]; await withDraftStations(detail, "P", 1); grid.pipe_to_underdrain = at40(detail); state.draftStructures = []; await withDraftStations(detail, "P", 1); grid.pipe_deleted = at40(detail); state.draftPipes = [{ chainage_m: 40, facility: "pipe", options: D800 }]; state.draftStructures = null; await withDraftStations(detail, "P", 1); grid.pipe_back = at40(detail); state.draftPipes = [{ chainage_m: 40, facility: "box_culvert" }]; await withDraftStations(detail, "P", 1); grid.pipe_to_box = at40(detail); // 저장분 = 격자 40 위 맹암거 → 배관 forgetDraftStations("P"); state.savedPipes = []; state.savedStructures = [ { structure_id: "s1", type_id: "cross_underdrain", placement: "point", chainage_m: 40, options: {}, }, ]; detail = detailWith({ structure: "맹암거", underdrain: { chainage_m: 40 } }); state.draftPipes = [{ chainage_m: 40, facility: "pipe", options: D800 }]; state.draftStructures = []; await withDraftStations(detail, "P", 1); grid.underdrain_to_pipe = at40(detail); forgetDraftStations("P"); out.grid = grid; process.stdout.write(JSON.stringify(out)); })().catch((error) => { process.stderr.write(String(error && error.stack ? error.stack : error)); process.exit(1); });