/* 설계 가게 · 저장 한 줄기(계획서 24-2a) 단위검증 헬퍼 — * `A00_Common/design/design_store.ts` · `design_save.ts` · `B05_Profile_UI_Profile_Edit.ts` 를 * 그 자리에서 트랜스파일해 Node 로 돌린다. 서버 창구는 가짜로 두고 부른 차례만 적는다. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.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; }, }; } const session = memoryStorage(); globalThis.window = { sessionStorage: session, localStorage: memoryStorage() }; globalThis.sessionStorage = session; const calls = []; const record = (name, fail) => async () => { calls.push(name); if (fail) throw new Error(`${name} 실패`); }; let failStructures = false; // 노선 저장(346-3) — 초안 · 실패를 시험이 정함 let routeBody = null; let failRoute = false; // 6-B — 노선 저장 · 다시 얹기 · 넘김은 서버 한 본문 · 여기서는 옛 노선 세션 칸 원본 조각만 const routeStub = { pendingRouteSave: async () => routeBody, routeCarryPart: (_p, from) => ({ carry: { from }, cross: { body: { carried: from }, after: () => calls.push("cross:carried:after") }, }), routeSaved: () => calls.push("routeSaved"), routeSaveSummary: (saved, outside) => (calls.push(`summary:${saved.route_id}:${outside}`), ""), rebuildAfterRouteSave: async () => calls.push("rebuild"), }; class DesignSaveError extends Error { constructor(message, step, restored) { super(message); this.step = step; this.restored = restored; } } const SAVE_PART_KEYS = [ "settings", "pipes", "structures", "plusStations", "alignment", "culvertOptions", "uphill", "cross", ]; const saveApiStub = { SAVE_PART_KEYS, DesignSaveError, postDesignSave: async (_p, routeId, confirm, parts, extra = {}) => { const sent = SAVE_PART_KEYS.filter((key) => parts[key] && parts[key].body != null); const moved = extra.route ? `:route(carry ${extra.carry?.from})` : ""; calls.push(`post:${routeId}:${confirm}:${sent.join(",")}${moved}`); if (extra.route && parts.alignment?.body) { const body = parts.alignment.body; // 서버가 새 노선 번호로 고쳐 씀(test_472_6_route_save) calls.push(`alignment:${body.route_id}:${JSON.stringify(body.station_offsets)}`); } if (failStructures) throw new DesignSaveError("structures 실패", "structures", true); if (extra.route && failRoute) throw new DesignSaveError("노선 저장 실패", "route", true); const route = extra.route ? { route: { route_id: 9, pipes: { kept: 3, crossing: 1, concave: 0 }, cross_dropped: 1 }, carry: { outside: 2 }, } : {}; return { waited: false, steps: { alignment: { dropped_edit_chainages: [] }, ...route } }; }, }; const STUBS = { "../../B05_Profile/B05_Profile_UI_RouteEdit_Apply": routeStub, "@ui/ui_template_elements": { showToast: (text, kind) => calls.push(`toast:${kind}`), showLoadingOverlay: () => calls.push("overlay:on"), hideLoadingOverlay: () => calls.push("overlay:off"), }, "../../B05_Profile/B05_Profile_Api_Pipes_Draft": { pipesPart: () => ({ body: { points: [] }, after: () => calls.push("pipes:after") }), }, "../../B05_Profile/B05_Profile_Api_Structures": { structuresPart: async () => ({ body: { base_revision: 0, structures: [] }, after: () => calls.push("structures:after"), }), }, "../../B05_Profile/B05_Profile_Api_PlusStations": { outsideNotice: (count) => `노선 밖 ${count}`, plusStationsPart: () => ({ body: null, after: () => calls.push("plusStations:after") }), }, "../../B05_Profile/B05_Profile_Api_Fetch": { routePartsBody: async () => ({ irregular_stations: [] }), // 상단측 차례 = 종단 보기 몫과 같은 본문(463) }, "../../B06_Section/B06_Section_Api_Culvert_Options": { culvertOptionsPart: (key) => ({ body: { "10.00": {} }, after: () => calls.push(`culvert:after:${key}`), }), }, // 448 페이지 설정 — 초안 없음(차례만 · 내보내기는 test_442_2_page_settings) "../../B05_Profile/B05_Profile_Api_PageSettings": { pageSettingsPart: () => null, }, // 472-6 한 본문 창구 — 보낸 칸만 적음 · 구조물 실패 = 서버가 그 차례에서 되돌림 "./design_save_api": saveApiStub, "./design_leave": { designSignature: () => "", markDesignClean: () => undefined }, }; const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (request) => { if (STUBS[request]) return STUBS[request]; if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); return require(request); }, }); const { useDesignStore } = loadTs(path.join(ROOT, "A00_Common", "design", "design_store.ts")); const edit = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Edit.ts")); const { saveDesign } = loadTs(path.join(ROOT, "A00_Common", "design", "design_save.ts")); async function run() { const out = {}; // ① 가게 — 등록 키 밑 · 같은 칸이면 두 가게가 같은 값 · 듣는 쪽 const a = useDesignStore("p1", 7); const b = useDesignStore("p1", 99); // 프로젝트 칸은 노선과 무관 const heard = []; const off = b.subscribe("pipes", (value) => heard.push(value.length)); a.set("pipes", [{ chainage_m: 10, source: "user" }]); out.pipesShared = b.get("pipes"); out.pipesKey = [...Array(session.length).keys()].map((i) => session.key(i)); off(); a.set("pipes", [{ chainage_m: 10 }, { chainage_m: 20 }]); out.heard = heard; out.otherProject = useDesignStore("p2", 7).get("pipes"); out.noRoute = useDesignStore("p1", null).get("alignmentDraft"); a.set("selection", { chainageM: 12.5, structureId: "s-1" }); out.selection = b.get("selection"); a.set("pipes", null); a.set("selection", null); // ② 계획고 가게 한 벌 — B05 패널 · B06 계획선 편집이 같은 벌 const saved = { station_offsets: {}, curve_radii: { "40.000": 30 } }; const b05 = edit.createProfileEditStore(7, saved, () => undefined); const b06 = edit.createProfileEditStore(7, saved, () => undefined); b06.replace({ station_offsets: { "20.000": 0.5 }, curve_radii: saved.curve_radii }); out.b05SeesB06 = b05.edits().station_offsets; out.b05Dirty = b05.dirty(); // B05 가 자기 손에 쥔 값으로 이어 고쳐도 B06 편집이 안 사라짐(옛 두 가게 흠) b05.replace({ station_offsets: { ...b05.edits().station_offsets, "60.000": -0.2 }, curve_radii: b05.edits().curve_radii, }); out.afterBoth = b06.edits().station_offsets; out.draft = useDesignStore(null, 7).get("alignmentDraft").station_offsets; out.pending = edit.pendingAlignment(7).station_offsets; // 되돌리기가 초안을 바꾼 뒤 B05 가 다시 세움 — B06 쪽 손잡이도 같은 벌을 봄 useDesignStore(null, 7).set("alignmentDraft", { station_offsets: { "20.000": 0.1 }, curve_radii: {}, }); edit.createProfileEditStore(7, saved, () => undefined); out.rebuiltSeenByB06 = b06.edits().station_offsets; // ③ 저장 한 줄기 — 차례 · 기본 창구 · 보기 몫 · 계획고 저장됨 표시 calls.length = 0; const ok = await saveDesign({ projectId: "p1", routeId: () => 7, prepare: record("prepare"), parts: { pipes: (base) => ({ body: base.body, after: async (response) => { await base.after(response); calls.push("pipes:screen"); }, }), }, crossPart: async (confirm) => ({ body: { confirm }, after: () => calls.push(`cross:after:${confirm}`), }), done: async (confirm) => calls.push(`done:${confirm}`), failed: () => calls.push("failed"), }); out.saveOk = ok; out.saveCalls = [...calls]; out.afterSave = { dirty: b05.dirty(), draft: useDesignStore(null, 7).get("alignmentDraft") }; // ④ 구조물이 실패하면 그 차례에서 멈춤 · 차례 이름째 알림(429 ⑨) · 노선 없으면 안 나감 calls.length = 0; failStructures = true; out.tolerantOk = await saveDesign( { projectId: "p1", routeId: () => 7, crossPart: async (confirm) => ({ body: { confirm } }), failed: (error) => (calls.push("failed"), (out.tolerantError = error.message)), }, { confirm: true }, ); failStructures = false; out.tolerant = [...calls]; calls.length = 0; out.noRouteSave = await saveDesign({ projectId: "p1", routeId: () => null, cross: async () => calls.push("cross"), failed: () => calls.push("failed"), }); out.noRouteCalls = [...calls]; // ⑥ 노선이 바뀜(346-3) — 노선 먼저 · 뒤 차례는 새 노선(9) · 화면 몫 건너뜀 · 횡단 초안 넘김 · 다시 세움 b05.replace({ station_offsets: { "30.000": 0.2 }, curve_radii: {} }); // 옛 노선(7) 계획고 초안 routeBody = { draft: { rev: 1, nodes: [ [0, 0], [10, 0], ], }, }; calls.length = 0; out.routeOk = await saveDesign({ projectId: "p1", routeId: () => 7, prepare: record("prepare"), steps: { pipes: record("pipes:screen"), alignment: record("alignment:screen") }, cross: async (confirm) => calls.push(`cross:${confirm}`), done: async (confirm) => calls.push(`done:${confirm}`), failed: () => calls.push("failed"), }); out.routeCalls = [...calls]; // ⑦ 노선 저장 실패 — 거기서 멈춤 · 다른 초안 그대로 b05.replace({ station_offsets: { "50.000": 0.3 }, curve_radii: {} }); failRoute = true; calls.length = 0; out.routeFailOk = await saveDesign({ projectId: "p1", routeId: () => 7, cross: async (confirm) => calls.push(`cross:${confirm}`), done: async (confirm) => calls.push(`done:${confirm}`), failed: () => calls.push("failed"), }); out.routeFailCalls = [...calls]; out.routeFailDraft = edit.pendingAlignment(7)?.station_offsets ?? null; failRoute = false; // ⑧ [확정] 중 노선이 바뀜 — 노선 저장 뒤 다시 세움 · 확정 단계는 안 닫음(새 노선으로 다시) calls.length = 0; await saveDesign( { projectId: "p1", routeId: () => 7, cross: async (confirm) => calls.push(`cross:${confirm}`), done: async (confirm) => calls.push(`done:${confirm}`), failed: () => calls.push("failed"), }, { confirm: true }, ); out.routeConfirmCalls = [...calls]; // ⑨ 노선 초안 동안 계획고 = 노선 0 칸(sub7 DRAFT_ROUTE_ID) — 그 칸이 새 노선에 나가고 두 칸 다 비움 b05.replace({ station_offsets: { "30.000": 0.2 }, curve_radii: {} }); // 옛 노선(7) 초안 useDesignStore(null, 0).set("alignmentDraft", { station_offsets: { "35.000": 0.4 }, curve_radii: {}, }); calls.length = 0; await saveDesign({ projectId: "p1", routeId: () => 7, cross: async (confirm) => calls.push(`cross:${confirm}`), failed: () => calls.push("failed"), }); out.draftRouteCalls = calls.filter((c) => c.startsWith("alignment:")); out.draftRouteLeft = { draft: edit.pendingAlignment(0), old: edit.pendingAlignment(7) }; routeBody = null; // ⑤ [초기화] — 초안 · 한 벌 모두 버림 b05.replace({ station_offsets: { "80.000": 1 }, curve_radii: {} }); edit.clearAlignmentDrafts(); out.cleared = { draft: useDesignStore(null, 7).get("alignmentDraft"), pending: edit.pendingAlignment(7), }; return out; } run().then( (out) => console.log(JSON.stringify(out)), (error) => { console.error(error); process.exit(1); }, );