/* PLAN 449 헬퍼 — [저장] 관 차례 · 노선 저장 뒤 실패(433-7 · 433-12). 실제 `design_save.ts` · 관 초안 창구 · 가게를 * Node 로 · 서버 창구 · 계획고 초안 · 노선 편집 창구만 가짜. 짝 = `test_449_save_pipes.py`. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const CATALOG = require("./helper_322_catalog_module.cjs"); const ROOT = path.join(__dirname, "..", ".."); function memoryStorage() { const map = new Map(); return { getItem: (key) => (map.has(key) ? map.get(key) : null), setItem: (key, value) => map.set(key, String(value)), removeItem: (key) => map.delete(key), key: (index) => [...map.keys()][index] ?? null, get length() { return map.size; }, }; } globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage(), setTimeout, clearTimeout, }; const STUBS = { "/design_leave": { designSignature: () => "", markDesignClean: () => undefined } }; const { load, loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (request) => { if (CATALOG.isCatalogRequest(request)) return CATALOG; const key = Object.keys(STUBS).find((name) => request.endsWith(name)); if (key) return STUBS[key]; if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" }; if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); return require(request); }, }); let calls = []; let failAt = null; // 이 차례에서 throw let routeBody = null; const alignment = new Map(); // 노선 칸 → 계획고 초안 STUBS["ui_template_elements"] = { showLoadingOverlay: () => undefined, hideLoadingOverlay: () => undefined, showToast: () => undefined, }; const failing = (name) => { if (failAt === name) throw new Error(`${name} 409`); }; STUBS["B04_PreProcess_Api_Fetch"] = { fetchDetailPipePoints: async () => (calls.push(["GET"]), { pipe_points: [] }), saveDetailPipePoints: async (_projectId, points, recompute) => { calls.push(["PUT", points ?? null, !!recompute]); return {}; }, }; STUBS["B05_Profile_Api_Structures"] = { flushPendingStructures: async () => failing("structures"), structuresPart: async () => null, readPendingStructures: () => null, // 구조물 초안 없음 — 노선 저장 뒤 얹기(459) 안 돎 }; STUBS["B05_Profile_Api_PlusStations"] = { outsideNotice: () => "", plusStationsPart: () => ({ body: null }), }; STUBS["B05_Profile_Api_Fetch"] = { flushUphillOverrides: async () => undefined, flushRouteParts: async () => undefined, // 상단측 차례(463) routePartsBody: async () => ({}), saveProfileAlignment: async (_projectId, routeId, draft) => { calls.push(["ALIGN", routeId, draft]); return {}; }, }; STUBS["B05_Profile_UI_Profile_Edit"] = { pendingAlignment: (routeId) => alignment.get(routeId) ?? null, markAlignmentSaved: (routeId) => alignment.delete(routeId), writeAlignmentDraft: (routeId, edits) => alignment.set(routeId, edits), }; STUBS["B06_Section_Api_Culvert_Options"] = { flushCulvertOptions: async () => undefined, culvertOptionsPart: () => null, }; // 472-6 한 본문 창구 — 관 칸 · 계획고 칸만 적음(옛 PUT 줄과 같은 꼴) STUBS["design_save_api"] = { SAVE_PART_KEYS: [ "settings", "pipes", "structures", "plusStations", "alignment", "culvertOptions", "uphill", "cross", ], DesignSaveError: class extends Error { constructor(message, step, restored) { super(message); this.step = step; this.restored = restored; } }, postDesignSave: async (_projectId, routeId, confirm, parts, extra = {}) => { const pipes = parts.pipes?.body; calls.push([ "SAVE", pipes === undefined || pipes === null ? "none" : (pipes.points ?? null), confirm, ...(extra.route ? ["ROUTE"] : []), ]); const align = parts.alignment?.body; if (align) { const { route_id: _id, ...draft } = align; calls.push(["ALIGN", routeId, draft]); } // 472-6 6-B — 서버가 그 차례에서 실패 = 노선까지 되돌림(전부 아니면 없음) const Err = STUBS["design_save_api"].DesignSaveError; if (failAt) throw new Err(`${failAt} 409`, failAt, true); return { waited: false, steps: extra.route ? { route: { route_id: 8 } } : {} }; }, }; STUBS["B05_Profile_UI_RouteEdit_Apply"] = { pendingRouteSave: async () => routeBody, routeCarryPart: (_projectId, from) => ({ carry: { from }, cross: { body: {} } }), routeSaved: () => void calls.push(["ROUTE_SAVED"]), routeSaveSummary: () => "", rebuildAfterRouteSave: async () => void calls.push(["REBUILD"]), }; const save = load("A00_Common", "design", "design_save.ts"); const drafts = load("B05_Profile", "B05_Profile_Api_Pipes_Draft.ts"); const P = "proj-449"; async function once({ route = false, draft = null, confirm = false, fail = null, routeId = 7 }) { calls = []; failAt = fail; routeBody = route ? { draft: {} } : null; drafts.writePendingPipes(P, draft); let failure = null; const ok = await save.saveDesign( { projectId: P, routeId: () => routeId, // 화면 몫 = 배수판 다시 받기(PUT 없음) — 433-7 뒤 design_shared 와 같음 parts: { pipes: (base) => ({ body: base?.body ?? null, after: async (response) => { await base?.after?.(response); calls.push(["RELOAD"]); }, }), }, crossPart: async () => ({ body: {} }), failed: (error) => (failure = String(error && error.message ? error.message : error)), }, { confirm }, ); return { ok, failure, calls, left: drafts.readPendingPipes(P) }; } async function run() { window.localStorage.setItem("frd_current_project_id", P); const fifteen = [{ chainage_m: 15, source: "user", id: "p1" }]; const out = {}; out.draft = await once({ draft: fifteen }); // B06 고침 직후 [저장] = 초안 값 out.noDraft = await once({}); // 판 안 섬 · 초안 없음 = PUT 없음 out.confirmNoDraft = await once({ confirm: true }); // [확정] = 칸 없이(서버 저장분) out.emptyDraft = await once({ draft: [] }); // 사용자가 다 지움 = 빈 목록 // 433-12 — 노선 저장 뒤 구조물 실패 → 0 칸 계획고가 새 노선 칸에 · 다음 [저장]이 보냄 alignment.clear(); alignment.set(0, { pvi: { 120: 1.5 } }); out.routeFail = await once({ route: true, fail: "structures" }); out.alignAfterFail = Object.fromEntries(alignment); out.nextSave = await once({ route: true }); // 다음 [저장] = 같은 초안 그대로 다시 // 472-6 6-B — 횡단 넘김(서버 Node) 실패도 노선까지 되돌림 · 화면은 그대로 alignment.clear(); out.carryFail = await once({ route: true, fail: "cross" }); return out; } run() .then((out) => { process.stdout.write(JSON.stringify(out)); process.exit(0); }) .catch((error) => { process.stderr.write(String(error && error.stack ? error.stack : error)); process.exit(1); });