/* PLAN 445 헬퍼 — [저장] 횡단 patch 바탕 · 세션 얹음 · 되돌림 · 옮긴 측점 입력. * 조정창(`createStationControls`)으로 고친 뒤 횡단 보기 [저장] 본문(`buildCrossPatches` → `cachedCrossPatches`)을 * 그대로 만들어 내놓음 — 시험이 서버 병합(`_apply_section_edits` → `merge_cross_section_design_batches`)에 넣어 * DB 설계를 견줌. TS 는 그 자리에서 트랜스파일 · 화면 · 서버 창구는 가짜. 짝 = `test_445_save_patch.py`. * node helper_445_save_patch.cjs */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const B06 = (name) => path.join(ROOT, "B06_Section", `${name}.ts`); const memory = () => { 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(), }; }; globalThis.sessionStorage = memory(); globalThis.localStorage = memory(); globalThis.window = globalThis; const anything = () => new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) }); 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 (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 bodyStub = () => ({ control: { selectedFor: () => false, select: () => undefined }, byChainage: () => new Map(), load: () => undefined, }); const load = makeLoader( [ B06("B06_Section_Section_Store"), B06("B06_Section_Section_Canon"), B06("B06_Section_Section_Adjusts"), B06("B06_Section_Cross_Refresh"), B06("B06_Section_Api_Culvert_Options"), B06("B06_Section_UI_Page_Station_Controls"), B06("B06_Section_UI_Page_Link_Session"), B06("B06_Section_UI_Cross_Culvert_Types"), B06("logic/B06_Section_Station_Swap"), B06("logic/B06_Section_UI_Page_Patches"), path.join(ROOT, "A00_Common", "b_page_state.ts"), path.join(ROOT, "A00_Common", "design", "design_selection.ts"), ], { "./B06_Section_Api_Fetch": { fetchSectionDetail: async () => job.detail, saveSections: async () => ({}), }, "./B06_Section_UI_Page_Ford_Controls": { createBodyControls: () => ({ ford: bodyStub(), box: bodyStub() }), }, "./B06_Section_UI_Page_Span_Control": { createSpanControl: () => ({ control: {}, spanKeyOf: (chainageM, wall) => `${chainageM.toFixed(2)}:${wall}`, }), }, "./B06_Section_Section_Drafts": { withDraftStations: async () => undefined, withDraftShapes: async () => undefined, forgetDraftStations: () => undefined, supersedeDraftStations: () => undefined, }, "./B06_Section_Structure_Layouts": { applyStructureAreaRows: () => undefined, structureAreaRows: () => [], computeStoredLayouts: () => null, }, "../B05_Profile/B05_Profile_Api_PlusStations": { loadSavedPlusStations: async () => undefined, plusStationsOf: () => [], }, }, ); const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); const P = "p445"; const R = 7; const { stateKey } = load(path.join(ROOT, "A00_Common", "b_page_state.ts")); const store = load(B06("B06_Section_Section_Store")); const { buildCrossPatches } = load(B06("logic/B06_Section_UI_Page_Patches")); const { createStationControls } = load(B06("B06_Section_UI_Page_Station_Controls")); /** 새 탭 = 빈 세션 · 서버 상세(정본) 받아 캐시에 · 조정창 하나. */ async function freshTab(clearSession = true) { if (clearSession) globalThis.sessionStorage.clear(); store.invalidateSectionDetail(P, R); const detail = await store.loadSectionDetail(P, R); const controls = createStationControls({ sessionKey: (kind) => stateKey(kind, P, R), refreshCard: () => undefined, detail: () => detail, crossHalfWidth: () => undefined, sampledHalfWidth: () => 20, ensureSampledWidth: async () => true, projectId: () => P, }); controls.load(); return { detail, controls }; } /** 횡단 보기 [저장] 본문 — 세션 고침(`buildCrossPatches`) → 바탕 위에 얹음(`cachedCrossPatches`). */ function savePatches(controls) { const overlay = buildCrossPatches({ rockOffsets: new Map(), cutSlopeRatios: new Map(), stationWidths: controls.widths, inletStructures: controls.inletStructures, basinAdjustments: controls.basinAdjustments, revetAdjusts: controls.revetAdjustsByChainage(), extraCounts: controls.extraCountsByChainage(), extraSpans: controls.extraSpansByChainage(), fordAdjusts: new Map(), boxAdjusts: new Map(), linkFlags: controls.linkFlagsByChainage(), }); return { overlay, patches: store.cachedCrossPatches(P, R, overlay) }; } (async () => { const at = job.at; const out = {}; // H3 — 유출만 고침 → 유입 · 집수 쪽 단 수 남음 { const { controls } = await freshTab(); controls.revetOffset.update(at, "outlet", { x: 3 }); out.h3 = savePatches(controls); } // M2 — 새 탭 ↺(유입) · 추가 벽 − · 빈 전체 폭 · 집수정 내공 2.1(M1) { const { controls } = await freshTab(); controls.revetOffset.reset(at, "inlet"); controls.extraWalls.setCount(at, 0, "outlet"); controls.applyGlobalWidth(undefined, [at]); controls.inletStructure.updateAdjust(at, { innerWidthM: 2.1, innerHeightM: 0.5 }); out.m2 = savePatches(controls); out.m2.countShown = controls.extraWalls.countFor({ chainage_m: at, design: {} }, "outlet"); // 저장 전 새로고침(세션은 남고 상세는 정본) — 되돌림이 살아 있어야 함 const again = await freshTab(false); out.m2reload = savePatches(again.controls); out.m2reload.cachedInlet = again.detail.cross_sections[0].design.revet_adjust?.inlet ?? null; out.m2reload.cachedCounts = again.detail.cross_sections[0].design.extra_wall_counts ?? null; } // H2 — 정본 행이 없는 자리(옮긴 관)의 캐시 설계 = 지반 등 입력까지 실음 { globalThis.sessionStorage.clear(); store.invalidateSectionDetail(P, R); await store.loadSectionDetail(P, R); // 정본 = job.detail(at 한 측점) const moved = await store.loadSectionDetail(P, R); moved.cross_sections.push({ ...job.moved }); out.h2 = store.cachedCrossPatches(P, R, []); moved.cross_sections.pop(); } process.stdout.write(JSON.stringify(out)); })().catch((error) => { process.stderr.write(String(error?.stack ?? error)); process.exit(1); });