/* PLAN 448(442-2) 단위검증 헬퍼 — 페이지 설정 저장 차례 · 초안 내보내기 · 기준표 거울 재료. * ① 노선 바꾼 [저장](472-6 6-B 한 본문)에도 settings 기본 조각이 실림(화면 몫 아님) * ② pageSettingsPart — 초안 없음 = 안 보냄 · 성공 = 최신 응답 캐시에 얹고 초안 비움 · 실패 = 초안 그대로 * ③ 끝 측점 계획고 편집(시 · 종점 조정) — 칸 값 = 첫 · 끝 측점 station_offsets * ④ TS 기준표(PROFILE_CRITERIA · SPEED_CHOICES) — 거울 시험이 파이썬과 대조 * TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 서버 창구는 가짜. 짝 = `test_442_2_page_settings.py`. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); globalThis.window = { setTimeout, clearTimeout }; const anything = () => new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) }); function load(file, stubs) { const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, (r) => stubs[r] ?? anything()); return box.exports; } /** 상대 경로 import 를 실제 파일로 따라 읽는 로더(순수 logic 묶음용) · 그 밖은 가짜. */ const cache = new Map(); function loadReal(file) { if (cache.has(file)) return cache.get(file); const box = { exports: {} }; cache.set(file, box.exports); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const req = (r) => { if (!r.startsWith(".")) return anything(); const target = path.resolve(path.dirname(file), r); return loadReal(fs.existsSync(`${target}.ts`) ? `${target}.ts` : target); }; new Function("exports", "module", "require", js)(box.exports, box, req); cache.set(file, box.exports); return box.exports; } async function run() { const out = {}; // ── ① 저장 차례 ── const calls = []; const KEYS = ["settings", "pipes", "structures", "plusStations", "alignment"]; const save = load(path.join(ROOT, "A00_Common", "design", "design_save.ts"), { "../../B05_Profile/B05_Profile_Api_PageSettings": { pageSettingsPart: (projectId) => ({ body: { projectId }, after: () => calls.push(`settings:${projectId}`), }), }, "./design_save_api": { SAVE_PART_KEYS: [...KEYS, "cross"], DesignSaveError: Error, postDesignSave: async (_p, _routeId, _confirm, parts, extra = {}) => { const sent = KEYS.filter((key) => parts[key]?.body); calls.push(`post:${extra.route ? "route:" : ""}${sent.join(",")}`); return { waited: false, steps: { route: { route_id: 9 } } }; }, }, "../../B05_Profile/B05_Profile_UI_RouteEdit_Apply": { pendingRouteSave: async () => ({ draft: 1 }), routeCarryPart: () => ({ carry: {}, cross: { body: {} } }), routeSaved: () => undefined, routeSaveSummary: () => "", rebuildAfterRouteSave: async () => undefined, }, }); const done = await save.saveDesign({ projectId: "p1", routeId: () => 3, parts: { settings: () => (calls.push("own-settings"), null) }, // 노선 바뀌면 화면 몫 건너뜀 crossPart: async () => (calls.push("cross"), null), failed: (error) => calls.push(`failed:${error.message}`), }); out.routeSaveDone = done; out.routeSaveCalls = calls; // ── ② 초안 내보내기 ── const session = new Map(); const puts = []; let mode = "ok"; let latest = { route_params: { options: { grade_class: "trunk", design_speed_kph: 20 }, max_grade_pct: 15 }, }; const api = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettings.ts"), { // 값 주인 = 설계 가게 칸(472-5) — 같은 세션 흉내 위에 "../A00_Common/design/design_store": { useDesignStore: (p) => ({ get: () => (session.has(`pagesettings:${p}`) ? session.get(`pagesettings:${p}`) : null), set: (_key, value) => value === null ? session.delete(`pagesettings:${p}`) : session.set(`pagesettings:${p}`, value), }), draftProjectId: () => "p1", }, "./B05_Profile_UI_Profile_History": { onPageHistory: () => () => undefined }, "../A00_Common/b_page_state": { readState: (name, p) => (session.has(`${name}:${p}`) ? session.get(`${name}:${p}`) : null), writeState: (name, value, p) => session.set(`${name}:${p}`, value), clearState: (name, p) => session.delete(`${name}:${p}`), }, "./B05_Profile_Api_Fetch": { readRouteLatestCache: () => latest, writeRouteLatestCache: (_p, value) => (latest = value), }, }); // 472-6 — 서버 한 창구가 settings 칸을 받고(가짜) 다 저장한 뒤에만 조각 정리(옛 창구 `flushPageSettings` 걷음) const flushPageSettings = async (projectId) => { const part = api.pageSettingsPart(projectId); if (!part) return; puts.push(part.body); if (mode === "boom") throw new Error("서버 500"); await part.after?.({ warnings: [], profile_updated: true }); }; await flushPageSettings("p1"); out.putsWithoutDraft = puts.length; const draft = api.pageSettingsOf({ designSpeed: 30, terrainType: "normal", maxGradePct: null, minVerticalRadius: 300, minTangentLength: null, enforcePipeClearance: true, stationDisplay: { station: 12, cumulative: 240 }, }); out.draft = draft; api.writePageSettings("p1", draft); out.restoreFromDraft = api.panelValuesOfDraft(api.readPageSettings("p1")); mode = "boom"; out.boom = await flushPageSettings("p1").then( () => "ok", (error) => error.message, ); out.draftAfterBoom = api.readPageSettings("p1"); mode = "ok"; await flushPageSettings("p1"); out.draftAfterOk = api.readPageSettings("p1"); out.latestAfterOk = latest; out.puts = puts.length; // ── ③ 끝 측점 계획고 편집 ── const logic = path.join(ROOT, "B05_Profile", "logic"); const alignment = loadReal(path.join(logic, "B05_Profile_UI_Profile_Alignment.ts")); const ends = loadReal(path.join(logic, "B05_Profile_EndOffsets.ts")); const base = { policy: { station_interval_m: 20, curve_radius_ratio: 5, default_curve_length_m: 20, curve_tangent_max_ratio: 0.5, curve_skip_legal_exception: true, balance_tolerance_percent: 5, edit_step_m: 0.1, grade_violation_policy: "warn", max_grade_pct: 14, curve_skip_delta_pct: 5, paved: false, }, onlyExplicitCurves: false, basePvi: [ { chainage_m: 0, elevation_m: 100 }, { chainage_m: 100, elevation_m: 105 }, { chainage_m: 200, elevation_m: 108 }, ], chainage: [0, 100, 200], ground: [100, 105, 108], stations: [0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200].map((s) => ({ station_id: `s${s}`, chainage_m: s, })), }; const empty = { station_offsets: {}, curve_radii: {} }; out.endsEmpty = ends.endOffsetsOf(base, empty); const raised = ends.withEndOffset(base, empty, "start", 1.25); out.endsRaised = ends.endOffsetsOf(base, raised); const both = ends.withEndOffset(base, raised, "end", -0.5); out.endsBoth = ends.endOffsetsOf(base, both); const plan = alignment.buildAlignment(base, both); out.planStart = plan.samples[0].elevation_m; out.planEnd = plan.samples[plan.samples.length - 1].elevation_m; out.backToZero = ends.endOffsetsOf(base, ends.withEndOffset(base, both, "start", 0)); // ── ④ 기준표 ── const fields = load(path.join(ROOT, "B05_Profile", "logic", "B05_Profile_Grade_Criteria.ts"), {}); out.criteria = fields.PROFILE_CRITERIA; out.speedChoices = fields.SPEED_CHOICES; process.stdout.write(JSON.stringify(out)); } run().catch((error) => { process.stderr.write(String(error.stack || error)); process.exit(1); });