feat(472-6 6-B 노선 바뀐 [저장] 한 본문): 노선 저장 · 관 · 구조물 다시 얹기 · 횡단 고침 넘김을 서버 한 창구로 — 전부 아니면 없음
- POST /design/save 에 route(노선 초안) · carry(옛 노선 세션 칸) 칸 · 서버 = 노선 저장 → Node facility.rebase(rebasePipeDraft · rebaseStructureDraft) → 6-A 차례(새 노선) → Node stations.carry(routeCarryPatches) → 횡단 저장 - 되돌림 범위 = 노선 행 · 자식 표 · 단계 · 배수 폴더 · 노선 파일 · 지표면 확정(설계 지형 바꿈) - 손 상단측은 안 넘김(요구 274) · 노선 바뀐 [확정] = [저장](새 노선 본 뒤 다시) - 브라우저 = 노선 초안 + 사용자 칸만 한 본문 · 옛 차례 길(saveRouteDraft 뒤 flush · carryAfterRouteSave · keepDraftsOnNewRoute) · 부름 0 된 flush 창구 여섯 · saveCachedCrossPatches 걷음 - 등록부 R9 stations.carry · F10 facility.rebase · 옛 차례 단언 시험 한 본문 꼴로 · test_472_6_route_save(옛 길 = 새 길 · 실패 되돌림) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,13 @@ function loadTs(file) {
|
||||
}
|
||||
|
||||
const api = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_Api_Structures.ts"));
|
||||
// 472-6 — B06 [저장] = 한 본문 구조물 조각(옛 기본 창구 `flushPendingStructures` 걷음) · 서버 창구 = 가짜 PUT
|
||||
async function saveLikeOneBody(projectId) {
|
||||
const part = await api.structuresPart(projectId);
|
||||
if (!part) return;
|
||||
await api.saveStructures(projectId, part.body.base_revision, part.body.structures);
|
||||
await part.after?.(undefined);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const P = "p1";
|
||||
@@ -87,13 +94,13 @@ const api = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_Api_Structures.ts
|
||||
const notice = api.staleDefinitionNotice(P, opened.definition_version);
|
||||
const staleOnOpen = api.definitionStale(P);
|
||||
// 고침 없음 [저장](B06 기본 창구) — 정본을 그대로 다시 써 판 도장
|
||||
await api.flushPendingStructures(P);
|
||||
await saveLikeOneBody(P);
|
||||
const afterSave = { version: server.definition_version, puts: [...puts] };
|
||||
// 다시 열기 — 알림 없음 · 또 [저장] 해도 안 나감
|
||||
const reopened = await api.fetchStructures(P);
|
||||
const again = api.staleDefinitionNotice("p1-새탭", reopened.definition_version);
|
||||
api.staleDefinitionNotice(P, reopened.definition_version);
|
||||
await api.flushPendingStructures(P);
|
||||
await saveLikeOneBody(P);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
notice,
|
||||
|
||||
@@ -38,32 +38,18 @@ const STUBS = {
|
||||
},
|
||||
"../B06_Section/B06_Section_Section_Store": {
|
||||
invalidateSectionDetail: (_p, routeId) => calls.push(`invalidate:${routeId ?? "all"}`),
|
||||
loadSectionDetail: async (_p, routeId) => (
|
||||
calls.push(`load:${routeId}`),
|
||||
{ tag: routeId, cross_sections: [] }
|
||||
),
|
||||
saveCachedCrossPatches: async (_p, routeId) => (calls.push(`crossSave:${routeId}`), 1),
|
||||
},
|
||||
// 6-B — 넘김은 서버(Node · 같은 TS) · 여기서는 옛 노선 세션 칸 원본만 냄
|
||||
"../B06_Section/B06_Section_Station_Keys": {
|
||||
useStationKeys: (_p, routeId, detail) => calls.push(`keys:${routeId}:${detail.tag}`),
|
||||
carryStationDrafts: (_p, from, to, opts) => (
|
||||
calls.push(`carry:${from}->${to}${opts?.quiet ? ":quiet" : ""}`),
|
||||
2
|
||||
),
|
||||
// 417 — 누가거리 열쇠 조절 옮김
|
||||
carryChainageStates: (_p, from, to) => (calls.push(`carryAt:${from}->${to}`), 1),
|
||||
routeCarryInputs: (_p, from) => (calls.push(`carryInputs:${from}`), { from_keys: [], from }),
|
||||
},
|
||||
"../B06_Section/B06_Section_Section_Adjusts": {
|
||||
withDraftAdjusts: (_d, _p, routeId) => calls.push(`adjusts:${routeId}`),
|
||||
withCarriedEdits: (_d, _p, routeId) => calls.push(`edits:${routeId}`),
|
||||
"../B06_Section/B06_Section_Api_Fetch": {
|
||||
sectionConfirmBody: (standard, patches, massHaul) => ({ standard, patches, massHaul }),
|
||||
},
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => (calls.push("pipes"), { pipe_points: [] }),
|
||||
},
|
||||
"../A00_Common/design/design_pipe_ids": { pipeChainageOf: () => null },
|
||||
"../B06_Section/B06_Section_UI_Standard_Panel": { readStandardCrossSession: () => ({ std: 1 }) },
|
||||
"@util/common_util_mass_haul_balance_view": { balloonOffsetsPayload: () => ({ b: [1, 2] }) },
|
||||
"./B05_Profile_Api_Fetch": {
|
||||
clearRouteLatestCache: () => calls.push("latest:clear"),
|
||||
flushUphillOverrides: async () => calls.push("uphill"), // 옮긴 뒤 · 횡단 패치 앞(443-2)
|
||||
},
|
||||
"./B05_Profile_Api_Replan": {
|
||||
replanRoute: async (_p, body) => {
|
||||
@@ -153,9 +139,17 @@ const apply = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Ap
|
||||
out.keptAfterSave = kept;
|
||||
out.summary = apply.routeSaveSummary(saved, 2);
|
||||
|
||||
// 저장 뒤 횡단 초안 넘김 차례 — 새 상세 → 측점 열쇠 → 넘김 → 넘긴 고침 저장
|
||||
// 노선 바뀐 [저장] 횡단 조각(6-B) — 옛 노선 세션 칸 원본 + 표준 횡단 · 풍선 · 저장 뒤 상단측 · 종류 바꿈 비움
|
||||
calls.length = 0;
|
||||
out.outside = await apply.carryAfterRouteSave("p1", 7, 9);
|
||||
writeState("uphill", { "40.00": "right" }, "p1");
|
||||
writeState("kindreset", { "45.00": "culvert" }, "p1");
|
||||
const part = apply.routeCarryPart("p1", 7);
|
||||
out.carry = part.carry;
|
||||
out.crossBody = part.cross.body;
|
||||
out.uphillBeforeAfter = readState("uphill", "p1");
|
||||
await part.cross.after(undefined);
|
||||
out.uphillAfter = readState("uphill", "p1");
|
||||
out.kindresetAfter = readState("kindreset", "p1");
|
||||
out.carryCalls = [...calls];
|
||||
|
||||
// 추천 [적용] — as_initial · 잠금 막 · 초안 비움 · 다시 세움
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* PLAN 429 단위검증 헬퍼 — [저장] 길 정합.
|
||||
* ④ 노선 저장 뒤 횡단 저장 = 고침이 없어도 한 번(`saveCachedCrossPatches`)
|
||||
* ④ 노선 저장 뒤 횡단 저장 = 고침이 없어도 한 번(472-6 6-B — 서버 한 본문 · 시험은 글 단언)
|
||||
* ⑨ 추가 측점 = 계산 묶음 입력(`plusStations` · stations 부터) · 가게 초안 바뀜 → 묶음에 넣음(`feedFacilities`)
|
||||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 서버 창구 · 화면 모듈은 가짜. 짝 = `test_429_save_path.py`. */
|
||||
const fs = require("fs");
|
||||
@@ -39,24 +39,7 @@ function loader(stubs, real = () => false) {
|
||||
async function run() {
|
||||
const out = {};
|
||||
|
||||
// ④ 고침이 없어도 횡단 저장 한 번 — 서버가 지금 종단으로 횡단을 다시 셈
|
||||
const sent = [];
|
||||
const store = loader({
|
||||
"./B06_Section_Api_Fetch": {
|
||||
saveSections: async (...args) => (sent.push(args.map((a) => a ?? null)), {}),
|
||||
fetchSectionDetail: async () => null,
|
||||
},
|
||||
// 445 H5 — 노선 저장 뒤 본문 = 횡단 보기와 같은 칸(세션 고침 · 표준 횡단면 · 풍선)
|
||||
"./B06_Section_Cross_Design_Session": { crossDesignChoices: () => new Map() },
|
||||
"./B06_Section_UI_Page_Persist": {
|
||||
readRockBoundarySession: () => ({}),
|
||||
readCutSlopeSession: () => ({}),
|
||||
},
|
||||
"./logic/B06_Section_UI_Page_Patches": { mergeCrossPatches: (a, b) => [...a, ...b] },
|
||||
"@util/common_util_mass_haul_balance_view": { balloonOffsetsPayload: () => ({}) },
|
||||
})(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
|
||||
out.noPatchReturn = await store.saveCachedCrossPatches("p1", 9);
|
||||
out.noPatchSent = sent;
|
||||
// ④ 노선 저장 뒤 횡단 저장 한 번 = 서버 한 본문 몫(472-6 6-B · test_429_save_path 글 단언)
|
||||
|
||||
// ⑨ 묶음 — 추가 측점이 바뀌면 stations 부터 · 같은 값이면 안 셈
|
||||
const DESIGN = path.join(ROOT, "A00_Common", "design");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* PLAN 448(442-2) 단위검증 헬퍼 — 페이지 설정 저장 차례 · 초안 내보내기 · 기준표 거울 재료.
|
||||
* ① DESIGN_SAVE_ORDER 에 settings(route 뒤 · pipes 앞) · 노선 바꾼 [저장]에도 settings 기본 창구가 돎
|
||||
* ② flushPageSettings — 초안 없음 = 안 보냄 · 성공 = 최신 응답 캐시에 얹고 초안 비움 · 실패 = 초안 그대로
|
||||
* ① 노선 바꾼 [저장](472-6 6-B 한 본문)에도 settings 기본 조각이 실림(화면 몫 아님)
|
||||
* ② pageSettingsPart — 초안 없음 = 안 보냄 · 성공 = 최신 응답 캐시에 얹고 초안 비움 · 실패 = 초안 그대로
|
||||
* ③ 끝 측점 계획고 편집(시 · 종점 조정) — 칸 값 = 첫 · 끝 측점 station_offsets
|
||||
* ④ TS 기준표(PROFILE_CRITERIA · SPEED_CHOICES) — 거울 시험이 파이썬과 대조
|
||||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 서버 창구는 가짜. 짝 = `test_442_2_page_settings.py`. */
|
||||
@@ -47,24 +47,36 @@ async function run() {
|
||||
|
||||
// ── ① 저장 차례 ──
|
||||
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": {
|
||||
flushPageSettings: async (projectId) => calls.push(`settings:${projectId}`),
|
||||
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 }),
|
||||
saveRouteDraft: async () => (calls.push("route"), { route_id: 9 }),
|
||||
carryAfterRouteSave: async () => [],
|
||||
routeCarryPart: () => ({ carry: {}, cross: { body: {} } }),
|
||||
routeSaved: () => undefined,
|
||||
routeSaveSummary: () => "",
|
||||
rebuildAfterRouteSave: async () => undefined,
|
||||
},
|
||||
});
|
||||
out.order = [...save.DESIGN_SAVE_ORDER];
|
||||
const done = await save.saveDesign({
|
||||
projectId: "p1",
|
||||
routeId: () => 3,
|
||||
steps: { settings: async () => calls.push("own-settings") }, // 노선 바뀌면 화면 몫 건너뜀
|
||||
cross: async () => calls.push("cross"),
|
||||
parts: { settings: () => (calls.push("own-settings"), null) }, // 노선 바뀌면 화면 몫 건너뜀
|
||||
crossPart: async () => (calls.push("cross"), null),
|
||||
failed: (error) => calls.push(`failed:${error.message}`),
|
||||
});
|
||||
out.routeSaveDone = done;
|
||||
@@ -96,16 +108,19 @@ async function run() {
|
||||
clearState: (name, p) => session.delete(`${name}:${p}`),
|
||||
},
|
||||
"./B05_Profile_Api_Fetch": {
|
||||
savePageSettings: async (projectId, body) => {
|
||||
puts.push(body);
|
||||
if (mode === "boom") throw new Error("서버 500");
|
||||
return { warnings: [], profile_updated: true };
|
||||
},
|
||||
readRouteLatestCache: () => latest,
|
||||
writeRouteLatestCache: (_p, value) => (latest = value),
|
||||
},
|
||||
});
|
||||
await api.flushPageSettings("p1");
|
||||
// 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,
|
||||
@@ -120,13 +135,13 @@ async function run() {
|
||||
api.writePageSettings("p1", draft);
|
||||
out.restoreFromDraft = api.panelValuesOfDraft(api.readPageSettings("p1"));
|
||||
mode = "boom";
|
||||
out.boom = await api.flushPageSettings("p1").then(
|
||||
out.boom = await flushPageSettings("p1").then(
|
||||
() => "ok",
|
||||
(error) => error.message,
|
||||
);
|
||||
out.draftAfterBoom = api.readPageSettings("p1");
|
||||
mode = "ok";
|
||||
await api.flushPageSettings("p1");
|
||||
await flushPageSettings("p1");
|
||||
out.draftAfterOk = api.readPageSettings("p1");
|
||||
out.latestAfterOk = latest;
|
||||
out.puts = puts.length;
|
||||
|
||||
@@ -86,17 +86,14 @@ async function run() {
|
||||
},
|
||||
"@config/config_frontend": { API_BASE_URL: "", API_TIMEOUT_MS: 1000 },
|
||||
});
|
||||
// 472-6 — 상단측 차례 = 한 본문 `uphill` 칸(본문은 세션 · 비우기는 서버가 다 저장한 뒤 조각 정리 ·
|
||||
// 하위 차례 실패 = 서버가 그 차례에서 되돌림 · 옛 창구 `flushUphillOverrides` · `saveRouteParts` 걷음)
|
||||
state.set("uphill:p1", { "40.000": "right" });
|
||||
reply = { failed_parts: ["상단측 병합"] };
|
||||
out.flushFailed = await settle(api.flushUphillOverrides("p1"));
|
||||
out.uphillAfterFail = state.get("uphill:p1") ?? null;
|
||||
reply = { failed_parts: [] };
|
||||
out.flushOk = await settle(api.flushUphillOverrides("p1"));
|
||||
out.uphillAfterOk = state.get("uphill:p1") ?? null;
|
||||
out.sentAgain = await settle(api.flushUphillOverrides("p1")); // 비었으니 안 보냄
|
||||
out.exported = ["flushUphillOverrides", "saveRouteParts", "routePartsBody"].filter(
|
||||
(name) => typeof api[name] === "function",
|
||||
);
|
||||
out.uphillAfterBody = state.get("uphill:p1") ?? null;
|
||||
out.bodies = bodies;
|
||||
reply = { failed_parts: ["비정규 측점 횡단"] };
|
||||
out.partsFailed = await settle(api.saveRouteParts("p1", { irregular_stations: [] }));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ STUBS["B05_Profile_Api_Structures"] = {
|
||||
readPendingStructures: () => null, // 구조물 초안 없음 — 노선 저장 뒤 얹기(459) 안 돎
|
||||
};
|
||||
STUBS["B05_Profile_Api_PlusStations"] = {
|
||||
flushPlusStations: async () => undefined,
|
||||
outsideNotice: () => "",
|
||||
plusStationsPart: () => ({ body: null }),
|
||||
};
|
||||
STUBS["B05_Profile_Api_Fetch"] = {
|
||||
@@ -108,30 +108,36 @@ STUBS["design_save_api"] = {
|
||||
"uphill",
|
||||
"cross",
|
||||
],
|
||||
DesignSaveError: class extends Error {},
|
||||
postDesignSave: async (_projectId, routeId, confirm, parts) => {
|
||||
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]);
|
||||
}
|
||||
return { waited: false, steps: {} };
|
||||
// 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,
|
||||
saveRouteDraft: async () => (calls.push(["ROUTE"]), { route_id: 8 }),
|
||||
carryAfterRouteSave: async (_projectId, from, to, options) => {
|
||||
calls.push(["CARRY", from, to, options?.save ?? true]);
|
||||
failing("carry");
|
||||
return 0;
|
||||
},
|
||||
routeCarryPart: (_projectId, from) => ({ carry: { from }, cross: { body: {} } }),
|
||||
routeSaved: () => void calls.push(["ROUTE_SAVED"]),
|
||||
routeSaveSummary: () => "",
|
||||
rebuildAfterRouteSave: async () => void calls.push(["REBUILD"]),
|
||||
};
|
||||
@@ -180,10 +186,10 @@ async function run() {
|
||||
alignment.set(0, { pvi: { 120: 1.5 } });
|
||||
out.routeFail = await once({ route: true, fail: "structures" });
|
||||
out.alignAfterFail = Object.fromEntries(alignment);
|
||||
out.nextSave = await once({ routeId: 8 });
|
||||
// 노선 저장 뒤 횡단 넘김 자체가 실패 → 두 번 안 넘김
|
||||
out.nextSave = await once({ route: true }); // 다음 [저장] = 같은 초안 그대로 다시
|
||||
// 472-6 6-B — 횡단 넘김(서버 Node) 실패도 노선까지 되돌림 · 화면은 그대로
|
||||
alignment.clear();
|
||||
out.carryFail = await once({ route: true, fail: "carry" });
|
||||
out.carryFail = await once({ route: true, fail: "cross" });
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/* 472-6 6-B 헬퍼 — 노선 바뀐 [저장] 횡단 넘김: 브라우저 옛 길(세션 함수를 옛 `carryAfterRouteSave` →
|
||||
* `saveCachedCrossPatches` 차례 그대로) 결과 · 새 길(서버 Node `stations.carry`) 입력을 JSON 한 벌로.
|
||||
* node helper_472_6_route_carry.mjs <묶음.mjs>
|
||||
* 시험(`test_472_6_route_save.py`)이 새 길 입력을 진짜 Node 입구로 돌려 두 결과를 견줌. */
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const store = () => {
|
||||
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.window = globalThis;
|
||||
globalThis.sessionStorage = store();
|
||||
globalThis.localStorage = store();
|
||||
const any = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||
apply: () => any,
|
||||
construct: () => any,
|
||||
});
|
||||
globalThis.document = any;
|
||||
globalThis.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
|
||||
const [bundlePath] = process.argv.slice(2);
|
||||
const m = await import(pathToFileURL(bundlePath).href);
|
||||
const P = "p4726";
|
||||
const R = 7; // 옛 노선
|
||||
const R2 = 9; // 새 노선(서버가 저장한 것)
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||
|
||||
const pipe = { structure: "파형강관 D800", culvert: {} };
|
||||
const section = (chainage_m, kind, extra = {}) => ({
|
||||
station_id: `s${chainage_m}`,
|
||||
chainage_m,
|
||||
kind,
|
||||
label: String(chainage_m),
|
||||
samples: [],
|
||||
design: {},
|
||||
...extra,
|
||||
});
|
||||
const detailOf = (sections) => ({
|
||||
longitudinal: { stations: sections, profile_alignment: { policy: { station_interval_m: 20 } } },
|
||||
cross_sections: sections,
|
||||
});
|
||||
/* 새 노선 서버 상세 — 50 m 뒤가 3 m 밀림(관 80 → 83) · MC 110 → 112 · 구조물 130 사라짐 · 끝 140 → 143 */
|
||||
const fresh = () =>
|
||||
detailOf([
|
||||
section(0, "bp"),
|
||||
section(20, "regular", { design: { ground_type: "rock" } }),
|
||||
section(40, "regular"),
|
||||
section(60, "regular", { design: { display_half_width_m: 5, ground_type: "soil" } }),
|
||||
section(83, "irregular", pipe),
|
||||
section(103, "regular"),
|
||||
section(112, "curve"),
|
||||
section(123, "regular"),
|
||||
section(143, "ep"),
|
||||
]);
|
||||
/* 옛 노선 열쇠 목록(브라우저가 끌기 동안 세운 것) · 측점 주인(초안 자리) · 관 장부 */
|
||||
m.useStationKeyList(P, R, [
|
||||
{ key: "end:BP", chainageM: 0 },
|
||||
{ key: "grid:1", chainageM: 20 },
|
||||
{ key: "grid:2", chainageM: 40 },
|
||||
{ key: "grid:3", chainageM: 60 },
|
||||
{ key: "pipe:p80", chainageM: 80 },
|
||||
{ key: "grid:5", chainageM: 100 },
|
||||
{ key: "curve:110.00", chainageM: 110 },
|
||||
{ key: "grid:6", chainageM: 120 },
|
||||
{ key: "structure:s1", chainageM: 130 },
|
||||
{ key: "end:EP", chainageM: 140 },
|
||||
]);
|
||||
m.useStationOwners(P, [{ chainage_m: 83, owners: ["p80"] }], [{ id: "p80" }]);
|
||||
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 83 }] }, P);
|
||||
const pipesBefore = [{ id: "p80", chainage_m: 80 }];
|
||||
const pipesAfter = [
|
||||
{ id: "p80", chainage_m: 83, options: { inlet_revet_height_m: 1.8, inlet_revet_form: "I" } },
|
||||
];
|
||||
|
||||
/* 옛 노선 세션 고침 — 측점 열쇠 칸 다섯 · 누가거리 칸 · 프로젝트 칸(종류 바꿈 · 상단측) */
|
||||
m.writeCrossDesignChoice(P, R, 20, {
|
||||
ground_type: "soil",
|
||||
section_mode: "left_cut",
|
||||
ditch_type: "u_flume",
|
||||
two_stage_slope: true,
|
||||
});
|
||||
m.writeCrossDesignChoice(P, R, 130, { ground_type: "soil", section_mode: "right_cut" }); // 밖
|
||||
m.writeShoulderExtend(P, R, 40, "left", 0.5);
|
||||
m.writeBermDepth(P, R, 130, "right", 1); // 밖
|
||||
m.writeState("rockb", { "grid:5": 1.5, "@at": { "grid:5": 100 } }, P, R);
|
||||
m.writeState("cutslope", { "curve:110.00": 0.8, "@at": { "curve:110.00": 110 } }, P, R);
|
||||
const wall = { x: 0.3, d: null, h: 1.5, m: null };
|
||||
m.writeState("revetx", { "80.00:outlet": wall, "40.00:inlet": wall }, P, R); // 40 = 일반 측점 → 버림
|
||||
m.writeState("basinadjust", { "80.00": { depth_m: 1.2 } }, P, R);
|
||||
m.writeState("crossw", { "60.00": 6 }, P, R);
|
||||
m.writeState("structure-notes", { "130.00": "메모", "20.00": "앞 메모" }, P, R);
|
||||
m.writeState("detail-table", { "80.00": { rows: 2 } }, P, R);
|
||||
m.writeState("revetlink", { detached: { "80.00": true }, grade: {} }, P, R);
|
||||
m.writeState("kindreset", { "60.00": "culvert" }, P); // 일반 측점 = 관 · 구조물 열쇠 아님 → 버림
|
||||
m.writeState("uphill", { "60.00": "right" }, P);
|
||||
|
||||
const out = {};
|
||||
/* 새 길 입력 — 브라우저가 한 본문 `carry` 칸에 싣는 것(넘기기 전 세션 그대로) */
|
||||
out.carry = clone(m.routeCarryInputs(P, R));
|
||||
out.detail = fresh();
|
||||
out.pipesBefore = pipesBefore;
|
||||
out.pipesAfter = pipesAfter;
|
||||
|
||||
/* 옛 길 — `loadSectionDetail`(관 옵션 얹기) → `carryAfterRouteSave` → `saveCachedCrossPatches` 차례 그대로 */
|
||||
const detail = fresh();
|
||||
m.layPipeOptions(detail, pipesAfter);
|
||||
m.useStationKeys(P, R2, detail);
|
||||
const newAt = (id) => pipesAfter.find((p) => id !== undefined && p.id === id)?.chainage_m ?? null;
|
||||
const pipeAt = (key) => {
|
||||
const id = key.startsWith("pipe:") ? key.slice(5) : null;
|
||||
if (!id) return null;
|
||||
return id.startsWith("pipe-") ? m.pipeChainageOf(id, P) : newAt(id);
|
||||
};
|
||||
const movedAt = (chainageM) =>
|
||||
newAt(pipesBefore.find((p) => Math.abs(p.chainage_m - chainageM) < 0.01)?.id);
|
||||
const outside =
|
||||
m.carryStationDrafts(P, R, R2, { quiet: true }) +
|
||||
m.carryChainageStates(P, R, R2, { keyAt: pipeAt, movedAt });
|
||||
m.withDraftAdjusts(detail, P, R2);
|
||||
m.withCarriedEdits(detail, P, R2);
|
||||
const base = m.crossPatchesFromCache(
|
||||
detail,
|
||||
m.kindChangedChainages(P),
|
||||
detail.cross_sections.map((s) => s.chainage_m),
|
||||
);
|
||||
const overlay = [];
|
||||
m.crossDesignChoices(P, R2).forEach((choice, chainage) => {
|
||||
const patch = {
|
||||
chainage_m: chainage,
|
||||
ground_type: choice.ground_type,
|
||||
section_mode: choice.section_mode,
|
||||
};
|
||||
if (choice.ditch_side !== undefined) patch.ditch_side = choice.ditch_side;
|
||||
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
|
||||
if (choice.paved !== undefined) patch.paved = choice.paved;
|
||||
if (choice.two_stage_slope !== undefined) patch.two_stage_slope = choice.two_stage_slope;
|
||||
if (typeof choice.ditch_choice === "boolean") patch.ditch_choice = choice.ditch_choice;
|
||||
overlay.push(patch);
|
||||
});
|
||||
// 옛 `readRockBoundarySession` · `readCutSlopeSession` = 측점 열쇠 칸을 새 목록 누가거리로(`readKeyed`)
|
||||
const keyed = (name) => [...m.readKeyed(name, P, R2)];
|
||||
for (const [chainage, offset] of keyed("rockb"))
|
||||
if (typeof offset === "number")
|
||||
overlay.push({ chainage_m: Number(chainage), rock_boundary_offset_m: offset });
|
||||
for (const [chainage, ratio] of keyed("cutslope"))
|
||||
overlay.push({ chainage_m: Number(chainage), cut_slope_ratio_user: ratio });
|
||||
out.oldPatches = JSON.parse(
|
||||
JSON.stringify(
|
||||
m.mergeCrossPatches(
|
||||
base,
|
||||
overlay.filter((p) => Number.isFinite(p.chainage_m)),
|
||||
),
|
||||
),
|
||||
);
|
||||
out.oldOutside = outside;
|
||||
|
||||
/* 관 · 구조물 초안 다시 얹기 — 브라우저 옛 길이 부르던 같은 함수(서버 Node `facility.rebase` 와 견줌) */
|
||||
out.rebaseInput = {
|
||||
pipes: {
|
||||
draft: [
|
||||
{ id: "p80", chainage_m: 80, x: 80, y: 0 }, // 따라가기만 함 → 서버 자리
|
||||
{ id: "p20", chainage_m: 25, x: 25, y: 0 }, // 사용자가 옮김 → 그대로
|
||||
],
|
||||
before: [
|
||||
{ id: "p80", chainage_m: 80, x: 80, y: 0 },
|
||||
{ id: "p20", chainage_m: 20, x: 20, y: 0 },
|
||||
],
|
||||
after: [
|
||||
{ id: "p80", chainage_m: 83, x: 80, y: 0 },
|
||||
{ id: "p20", chainage_m: 20, x: 20, y: 0 },
|
||||
{ id: "pnew", chainage_m: 120, x: 117, y: 2 }, // 서버가 새로 놓은 교차 관
|
||||
],
|
||||
},
|
||||
structures: {
|
||||
draft: [{ structure_id: "s1", chainage_m: 60, start_m: null, end_m: null, x: 60, y: 0 }],
|
||||
before: [{ structure_id: "s1", chainage_m: 60, start_m: null, end_m: null, x: 60, y: 0 }],
|
||||
after: [{ structure_id: "s1", chainage_m: 63, start_m: null, end_m: null, x: 60, y: 0 }],
|
||||
},
|
||||
};
|
||||
const r = out.rebaseInput;
|
||||
out.oldRebase = {
|
||||
pipes: m.rebasePipeDraft(r.pipes.draft, r.pipes.before, r.pipes.after),
|
||||
structures: m.rebaseStructureDraft(r.structures.draft, r.structures.before, r.structures.after),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,29 @@
|
||||
/* 472-6 6-B 시험 묶음 진입점 — 브라우저 옛 길(세션 함수 차례 · 옛 `carryAfterRouteSave` + `saveCachedCrossPatches`)과
|
||||
* 새 길 입력(`routeCarryInputs`)을 한 벌로 묶어 Node 헬퍼(helper_472_6_route_carry.mjs)가 부름. */
|
||||
export {
|
||||
carryChainageStates,
|
||||
carryStationDrafts,
|
||||
readKeyed,
|
||||
routeCarryInputs,
|
||||
useStationKeyList,
|
||||
useStationKeys,
|
||||
useStationOwners,
|
||||
} from "../../B06_Section/B06_Section_Station_Keys";
|
||||
export {
|
||||
crossDesignChoices,
|
||||
writeBermDepth,
|
||||
writeCrossDesignChoice,
|
||||
writeShoulderExtend,
|
||||
} from "../../B06_Section/B06_Section_Cross_Design_Session";
|
||||
export {
|
||||
layPipeOptions,
|
||||
withCarriedEdits,
|
||||
withDraftAdjusts,
|
||||
} from "../../B06_Section/B06_Section_Section_Adjusts";
|
||||
export { crossPatchesFromCache } from "../../B06_Section/B06_Section_Route_Carry_Patch";
|
||||
export { mergeCrossPatches } from "../../B06_Section/logic/B06_Section_UI_Page_Patches";
|
||||
export { kindChangedChainages } from "../../B06_Section/B06_Section_Api_Culvert_Options";
|
||||
export { pipeChainageOf } from "../../A00_Common/design/design_pipe_ids";
|
||||
export { readState, writeState } from "../../A00_Common/b_page_state";
|
||||
export { rebasePipeDraft, rebaseStructureDraft } from "../../common_util/common_util_route_follow";
|
||||
export { chainageKey } from "../../common_util/common_util_station_key";
|
||||
@@ -33,14 +33,14 @@ let failStructures = false;
|
||||
// 노선 저장(346-3) — 초안 · 실패를 시험이 정함
|
||||
let routeBody = null;
|
||||
let failRoute = false;
|
||||
// 6-B — 노선 저장 · 다시 얹기 · 넘김은 서버 한 본문 · 여기서는 옛 노선 세션 칸 원본 조각만
|
||||
const routeStub = {
|
||||
pendingRouteSave: async () => routeBody,
|
||||
saveRouteDraft: async () => {
|
||||
calls.push("route");
|
||||
if (failRoute) throw new Error("노선 저장 실패");
|
||||
return { route_id: 9, pipes: { kept: 3, crossing: 1, concave: 0 }, cross_dropped: 1 };
|
||||
},
|
||||
carryAfterRouteSave: async (_p, from, to) => (calls.push(`carry:${from}->${to}`), 2),
|
||||
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"),
|
||||
};
|
||||
@@ -64,11 +64,23 @@ const SAVE_PART_KEYS = [
|
||||
const saveApiStub = {
|
||||
SAVE_PART_KEYS,
|
||||
DesignSaveError,
|
||||
postDesignSave: async (_p, routeId, confirm, parts) => {
|
||||
postDesignSave: async (_p, routeId, confirm, parts, extra = {}) => {
|
||||
const sent = SAVE_PART_KEYS.filter((key) => parts[key] && parts[key].body != null);
|
||||
calls.push(`post:${routeId}:${confirm}:${sent.join(",")}`);
|
||||
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);
|
||||
return { waited: false, steps: { alignment: { dropped_edit_chainages: [] } } };
|
||||
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 = {
|
||||
@@ -79,41 +91,22 @@ const STUBS = {
|
||||
hideLoadingOverlay: () => calls.push("overlay:off"),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Pipes_Draft": {
|
||||
flushPendingPipes: () => record("pipes")(),
|
||||
pipesPart: () => ({ body: { points: [] }, after: () => calls.push("pipes:after") }),
|
||||
readPendingPipes: () => null, // 관 초안 없음 — 노선 저장 뒤 얹기(349-0u)는 안 돎
|
||||
writePendingPipes: () => undefined,
|
||||
},
|
||||
"../../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: [] }),
|
||||
},
|
||||
"../../common_util/common_util_route_follow": {
|
||||
rebasePipeDraft: (draft) => draft,
|
||||
rebaseStructureDraft: (draft) => draft,
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": {
|
||||
flushPendingStructures: () => record("structures", failStructures)(),
|
||||
structuresPart: async () => ({
|
||||
body: { base_revision: 0, structures: [] },
|
||||
after: () => calls.push("structures:after"),
|
||||
}),
|
||||
readPendingStructures: () => null, // 구조물 초안 없음 — 노선 저장 뒤 얹기(459)는 안 돎
|
||||
writePendingStructures: () => undefined,
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_PlusStations": {
|
||||
flushPlusStations: () => record("plusStations")(),
|
||||
outsideNotice: (count) => `노선 밖 ${count}`,
|
||||
plusStationsPart: () => ({ body: null, after: () => calls.push("plusStations:after") }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Fetch": {
|
||||
flushUphillOverrides: () => record("uphill")(),
|
||||
flushRouteParts: () => record("uphill")(), // 상단측 차례 = 종단 보기 몫과 같은 본문(463)
|
||||
routePartsBody: async () => ({ irregular_stations: [] }),
|
||||
saveProfileAlignment: async (_p, routeId, edits) =>
|
||||
calls.push(`alignment:${routeId}:${JSON.stringify(edits.station_offsets)}`),
|
||||
routePartsBody: async () => ({ irregular_stations: [] }), // 상단측 차례 = 종단 보기 몫과 같은 본문(463)
|
||||
},
|
||||
"../../B06_Section/B06_Section_Api_Culvert_Options": {
|
||||
flushCulvertOptions: async (_p, key) => calls.push(`culvert:${key}`),
|
||||
culvertOptionsPart: (key) => ({
|
||||
body: { "10.00": {} },
|
||||
after: () => calls.push(`culvert:after:${key}`),
|
||||
@@ -121,7 +114,6 @@ const STUBS = {
|
||||
},
|
||||
// 448 페이지 설정 — 초안 없음(차례만 · 내보내기는 test_442_2_page_settings)
|
||||
"../../B05_Profile/B05_Profile_Api_PageSettings": {
|
||||
flushPageSettings: async () => undefined,
|
||||
pageSettingsPart: () => null,
|
||||
},
|
||||
// 472-6 한 본문 창구 — 보낸 칸만 적음 · 구조물 실패 = 서버가 그 차례에서 되돌림
|
||||
@@ -149,7 +141,7 @@ function loadTs(file) {
|
||||
|
||||
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, DESIGN_SAVE_ORDER } = loadTs(
|
||||
const { saveDesign } = loadTs(
|
||||
path.join(ROOT, "A00_Common", "design", "design_save.ts"),
|
||||
);
|
||||
|
||||
@@ -222,7 +214,6 @@ async function run() {
|
||||
out.saveOk = ok;
|
||||
out.saveCalls = [...calls];
|
||||
out.afterSave = { dirty: b05.dirty(), draft: useDesignStore(null, 7).get("alignmentDraft") };
|
||||
out.order = DESIGN_SAVE_ORDER;
|
||||
|
||||
// ④ 구조물이 실패하면 그 차례에서 멈춤 · 차례 이름째 알림(429 ⑨) · 노선 없으면 안 나감
|
||||
calls.length = 0;
|
||||
|
||||
@@ -80,8 +80,9 @@ def test_서버는_그_본문이면_비정규_측점_횡단을_맞춤(got):
|
||||
def test_종단_보기_몫은_기본_창구_함수를_부름():
|
||||
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
|
||||
# 472-6 — 종단 보기 몫은 기본 조각 본문 그대로(메모리만 더 비움) · 노선 바뀐 저장은 옛 창구
|
||||
# 472-6 — 종단 보기 몫은 기본 조각 본문 그대로(메모리만 더 비움) · 노선 바뀐 저장도 한 본문(6-B ·
|
||||
# 옛 창구 `flushRouteParts` 걷음 · 노선 바뀌면 손 상단측 안 보냄 = 요구 274)
|
||||
assert "uphill: (base) =>" in actions and "body: base.body," in actions
|
||||
assert "saveRouteParts(" not in actions and "flushRouteParts" not in actions
|
||||
assert "body: await routePartsBody(projectId)," in save
|
||||
assert "return () => flushRouteParts(projectId);" in save
|
||||
assert "flushRouteParts" not in save and "if (moved) return null;" in save
|
||||
|
||||
@@ -75,19 +75,17 @@ def test_저장_뒤_가게_초안_비움_결과_한_줄(out):
|
||||
|
||||
|
||||
def test_초안_넘김_차례(out):
|
||||
assert out["carryCalls"] == [
|
||||
"invalidate:9",
|
||||
"load:9",
|
||||
"keys:9:9",
|
||||
"pipes", # 관 열쇠 → 새 자리(서버가 옮긴 관)
|
||||
"carry:7->9:quiet",
|
||||
"carryAt:7->9", # 417 — 조절 · 측점 폭 · 상세 표도 새 노선 측점으로
|
||||
"uphill", # 443-2 — 상단측도 새 측점으로 옮긴 뒤 · 횡단 패치 앞
|
||||
"adjusts:9",
|
||||
"edits:9", # 옮긴 고침을 새 상세 설계 칸에 → 같은 [저장] 횡단 패치
|
||||
"crossSave:9",
|
||||
]
|
||||
assert out["outside"] == 3 # 측점 열쇠 2 + 누가거리 열쇠 1
|
||||
"""472-6 6-B — 넘김(측점 열쇠 · 누가거리 칸 · 얹기 · patch)은 서버가 같은 TS 로(Node `stations.carry` ·
|
||||
`test_472_6_route_save`) · 브라우저는 옛 노선 세션 칸 원본 + 횡단 본문만 · 저장 뒤 상단측 · 종류 바꿈 비움."""
|
||||
assert out["carryCalls"][0] == "carryInputs:7"
|
||||
assert out["carry"] == {"from_keys": [], "from": 7}
|
||||
assert out["crossBody"] == {
|
||||
"standard": {"std": 1},
|
||||
"massHaul": {"balloon_offsets": {"b": [1, 2]}},
|
||||
}
|
||||
assert out["uphillBeforeAfter"] == {"40.00": "right"} # 다 저장하기 전엔 그대로
|
||||
assert out["uphillAfter"] is None and out["kindresetAfter"] is None
|
||||
assert any("상단측 손 고침 1곳" in c for c in out["carryCalls"]) # 요구 274 — 새 지형이 정함
|
||||
|
||||
|
||||
def test_추천_적용은_as_initial_저장_뒤_다시_세움(out):
|
||||
@@ -95,8 +93,8 @@ def test_추천_적용은_as_initial_저장_뒤_다시_세움(out):
|
||||
body = out["recommendBody"]
|
||||
assert body["as_initial"] is True and body["initial"] == {"mode": "earthwork"}
|
||||
assert body["draft"]["nodes"] == [[1, 1], [9, 9]]
|
||||
# 잠금 막 하나 · 저장 직전 저장 관 떠 둠(417 — 저장 뒤 같은 관 id 새 자리로 조절 옮김)
|
||||
assert out["recommendCalls"] == ["overlay:on", "pipes", "replan", "overlay:off"]
|
||||
# 잠금 막 하나(472-6 6-B — 저장 직전 저장 관 떠 두기는 서버 한 본문 몫 · 추천은 초안을 비우니 안 받음)
|
||||
assert out["recommendCalls"] == ["overlay:on", "replan", "overlay:off"]
|
||||
assert out["recommendDraftsLeft"] == {"route": None, "pipes": None} # 새 초기값에서 다시
|
||||
assert out["rebuilt"] == 1
|
||||
|
||||
@@ -114,9 +112,12 @@ def test_노선_반영_단추_없앰_배선():
|
||||
apply = _read("B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts")
|
||||
assert "addEventListener" not in apply # 단추 듣기 없음 — [저장]이 부름
|
||||
save = _read("A00_Common/design/design_save.ts")
|
||||
assert save.index('"route",') < save.index('"pipes",')
|
||||
assert "await pendingRouteSave(ctx.projectId)" in save
|
||||
assert "carryAfterRouteSave(ctx.projectId, fromRouteId, routeId)" in save
|
||||
# 6-B — 노선 초안 = 한 본문 `route` 칸 · 서버가 맨 앞에서 노선 저장(옛 차례 길 걷음)
|
||||
assert "route ? { route, carry: carried?.carry } : {}" in save
|
||||
assert "carryAfterRouteSave" not in save and "saveRouteDraft" not in save
|
||||
server = _read("B06_Section/B06_Section_Router_DesignSave.py")
|
||||
assert server.index('step("route"') < server.index('step("settings"')
|
||||
initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts")
|
||||
at = initial.index("onApply: (picked) => {")
|
||||
assert "saveRecommendedRoute(" in initial[at : at + 900]
|
||||
@@ -129,8 +130,11 @@ def test_넘김_알림은_결과_한_줄에_합침():
|
||||
keys = _read("B06_Section/B06_Section_Station_Keys.ts")
|
||||
assert "{ quiet = false }: { quiet?: boolean } = {}," in keys
|
||||
assert "if (outside.size && !quiet)" in keys
|
||||
apply = _read("B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts")
|
||||
assert "carryStationDrafts(projectId, fromRouteId, toRouteId, { quiet: true })" in apply
|
||||
# 6-B — 서버 넘김이 버린 수(`outside` · 측점 열쇠 + 누가거리 칸)를 결과 한 줄에(`routeSaveSummary`)
|
||||
patch = _read("B06_Section/B06_Section_Route_Carry_Patch.ts")
|
||||
assert "outside: keyed.outside.size + chain.outside.size" in patch
|
||||
save = _read("A00_Common/design/design_save.ts")
|
||||
assert "routeSaveSummary(saved, carryOutside)" in save
|
||||
|
||||
|
||||
def test_노선_반영_글자_없음():
|
||||
|
||||
@@ -34,9 +34,13 @@ def test_노선이_바뀌면_좌표로_따라감(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_저장_차례_계획고_앞() -> None:
|
||||
save = (ROOT / "A00_Common/design/design_save.ts").read_text(encoding="utf-8")
|
||||
order = save[save.index("DESIGN_SAVE_ORDER = [") :]
|
||||
# 472-6 — 차례 = 한 본문 칸 차례(브라우저) · 서버 한 창구 차례
|
||||
api = (ROOT / "A00_Common/design/design_save_api.ts").read_text(encoding="utf-8")
|
||||
order = api[api.index("SAVE_PART_KEYS = [") :]
|
||||
assert order.index('"structures"') < order.index('"plusStations"') < order.index('"alignment"')
|
||||
server = (ROOT / "B06_Section/B06_Section_Router_DesignSave.py").read_text(encoding="utf-8")
|
||||
assert server.index('step("structures"') < server.index('step("plusStations"')
|
||||
assert server.index('step("plusStations"') < server.index('step("alignment"')
|
||||
|
||||
def test_옛_측점_옮기기는_추가_측점을_구조물로_안_만듦() -> None:
|
||||
# 저장된 추가 측점 = 이름 "" 비정규 측점 — 옛 자유 글 옮기기가 「기타」 구조물로 바꾸면 안 됨
|
||||
|
||||
@@ -48,9 +48,17 @@ def _ground() -> list[dict]:
|
||||
|
||||
|
||||
def test_노선_저장_뒤_고침이_없어도_횡단_저장_한_번(out):
|
||||
# 본문 없이 = 서버가 다시 셈만 · 풍선 자리는 늘 실음(445 H5 — 횡단 보기 [저장]과 같은 본문)
|
||||
assert out["noPatchSent"] == [["p1", 9, None, None, {"balloon_offsets": {}}]]
|
||||
assert out["noPatchReturn"] == 0
|
||||
"""472-6 6-B — 노선 바뀐 [저장]은 서버가 횡단 차례를 늘 한 번(넘긴 patch 가 없어도 · 뒤에 나간 계획고로
|
||||
다시 셈) · 브라우저 횡단 조각 = 풍선 자리 늘 실음(445 H5 — 횡단 보기 [저장]과 같은 본문)."""
|
||||
server = (ROOT / "B06_Section" / "B06_Section_Router_DesignSave.py").read_text(encoding="utf-8")
|
||||
moved = server[server.index("carry_cross(project_id, route_id") :]
|
||||
moved = moved[: moved.index(" elif request.cross is not None:")]
|
||||
assert 'body["cross_patches"] = carried.get("patches") or None' in moved
|
||||
assert "crossing = save_sections(project_id, route_id" in moved
|
||||
assert 'cross = await step("cross", crossing)' in server.replace("\n", " ").replace(" ", "")
|
||||
apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
||||
part = apply[apply.index("export function routeCarryPart") :]
|
||||
assert "balloon_offsets: balloonOffsetsPayload()," in part
|
||||
|
||||
|
||||
def test_서버_저장이_횡단_높이를_저장_종단에서(tmp_path):
|
||||
|
||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -338,13 +339,19 @@ def test_E3_뜰_params_없으면_안_씀():
|
||||
|
||||
|
||||
def test_저장_차례_settings_는_노선_뒤_관_앞(out):
|
||||
order = out["order"]
|
||||
assert order.index("route") + 1 == order.index("settings") < order.index("alignment")
|
||||
# 472-6 — 차례 = 서버 한 창구(노선 맨 앞 · 6-B)
|
||||
server = (HERE.parents[1] / "B06_Section" / "B06_Section_Router_DesignSave.py").read_text(
|
||||
"utf-8"
|
||||
)
|
||||
names = ("route", "settings", "pipes", "alignment")
|
||||
at = [re.search(r'step\(\s*"' + name + '"', server).start() for name in names]
|
||||
assert at == sorted(at)
|
||||
|
||||
|
||||
def test_노선_바꾼_저장에도_settings_기본_창구(out):
|
||||
assert out["routeSaveDone"] is True
|
||||
assert out["routeSaveCalls"] == ["route", "settings:p1"] # 화면 몫(own) 아님 · 기본 창구
|
||||
# 한 본문에 노선 · settings 기본 조각 · 화면 몫(own) · 화면 횡단 조각 아님 · 다 저장한 뒤 정리
|
||||
assert out["routeSaveCalls"] == ["post:route:settings", "settings:p1"]
|
||||
|
||||
|
||||
def test_초안_내보내기(out):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- 옛 누가거리 열쇠 상단측 = 측점 열쇠(옮김 목록 `ROUTE_CARRY_STATES` · 415 · 417 규칙)로 새 노선 측점에.
|
||||
- 새 노선에 같은 측점이 없으면 뺌 · 수를 돌려줌(「노선 밖 고침」 · 상단측 알림).
|
||||
- 저장 한 줄기: 노선을 저장했으면 상단측 차례는 건너뛰고 `carryAfterRouteSave` 가 옮긴 뒤 · 횡단 패치 앞에 보냄.
|
||||
- 저장 한 줄기(472-6 6-B · 요구 274): 노선이 바뀐 [저장]은 손 상단측을 안 보냄 · 새 지형이 정함(옛 443-2 갈음).
|
||||
Node 헬퍼 `helper_443_2_uphill_carry.mjs` 가 vite ssr 로 묶은 진짜 모듈(`helper_443_2_entry.ts`)을 부름.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_443_2_uphill_carry.py -q
|
||||
@@ -67,11 +67,16 @@ def test_세션_없으면_그대로(out):
|
||||
|
||||
|
||||
def test_노선_저장이면_상단측_차례는_옮긴_뒤():
|
||||
"""요구 274(사용자 확정 · 472-6 6-B) — 노선이 바뀐 [저장]은 손 상단측을 안 들고 감 · 새 지형이 정함.
|
||||
옛 443-2(옮긴 뒤 보냄)를 갈음 — 상단측 차례 안 보냄 · 서버도 노선 바뀜이면 그 칸을 안 받음 · 저장 뒤 세션
|
||||
상단측 비우고 수만 알림. 같은 노선 안의 측점 열쇠 옮김(아래 헬퍼 시험)은 그대로."""
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
|
||||
assert 'if (step === "uphill" && routeSaved) continue;' in save
|
||||
assert "if (moved) return null;" in save # 상단측 조각 없음
|
||||
apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text("utf-8")
|
||||
body = apply.split("export async function carryAfterRouteSave(")[1].split("\n}\n")[0]
|
||||
carry = body.index("carryChainageStates(")
|
||||
flush = body.index("await flushUphillOverrides(projectId)")
|
||||
assert carry < flush < body.index("saveCachedCrossPatches(") # 옮긴 뒤 · 횡단 패치 앞
|
||||
assert "상단측 ${uphillDropped}곳" in body # 못 맞춤 알림
|
||||
body = apply.split("export function routeCarryPart(")[1].split("\n}\n")[0]
|
||||
assert 'clearState("uphill", projectId);' in body and "상단측 손 고침 ${uphill}곳" in body
|
||||
keys = (ROOT / "B06_Section" / "B06_Section_Station_Keys.ts").read_text("utf-8")
|
||||
inputs = keys.split("export function routeCarryInputs(")[1].split("\n}\n")[0]
|
||||
assert 'state === "uphill"' in inputs # 넘김 입력에서 뺌
|
||||
server = (ROOT / "B06_Section" / "B06_Section_Router_DesignSave.py").read_text("utf-8")
|
||||
assert "if request.uphill is not None and not moved:" in server
|
||||
@@ -60,14 +60,23 @@ def test_구조물_차례가_토스트로_삼키지_않음():
|
||||
|
||||
|
||||
def test_상단측_실패면_세션_그대로_성공이면_비움(out):
|
||||
assert out["flushFailed"] == "상단측 병합 실패"
|
||||
assert out["uphillAfterFail"] == {"40.000": "right"}
|
||||
assert out["flushOk"] == "ok" and out["uphillAfterOk"] is None
|
||||
assert len(out["bodies"]) == 3 # 실패 · 성공 · (빈 세션은 안 보냄) · 확정 하위 하나
|
||||
"""472-6 — 상단측 = 한 본문 `uphill` 칸 · 본문 = 세션 · 세션 비움 = 서버가 다 저장한 뒤 조각 정리만
|
||||
(실패면 정리가 안 돎 = 세션 그대로 · 옛 창구 `flushUphillOverrides` 걷음)."""
|
||||
assert out["exported"] == ["routePartsBody"] # 옛 창구 둘 걷음 · 본문 한 벌만
|
||||
assert out["uphillAfterBody"] == {"40.000": "right"} and out["bodies"] == []
|
||||
fetch = (ROOT / "B05_Profile" / "B05_Profile_Api_Fetch.ts").read_text("utf-8")
|
||||
assert "uphill_overrides: uphillOverridesOf(projectId)," in fetch # 본문 = 세션
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
|
||||
part = save[save.index('case "uphill":') :]
|
||||
assert 'after: () => clearState("uphill", projectId),' in part[:500]
|
||||
assert "for (const key of SAVE_PART_KEYS) await parts[key]?.after?.(" in save # 성공 뒤에만
|
||||
|
||||
|
||||
def test_확정_하위_실패면_던짐(out):
|
||||
assert out["partsFailed"] == "비정규 측점 횡단 실패"
|
||||
"""472-6 — 하위 차례(비정규 측점 횡단 · 상단측 병합) 실패 = 서버 한 본문이 상단측 차례에서 실패 · 되돌림."""
|
||||
server = (ROOT / "B06_Section" / "B06_Section_Router_DesignSave.py").read_text("utf-8")
|
||||
assert 'if parts.get("failed_parts"):' in server
|
||||
assert 'raise StepFailed("uphill", " · ".join(parts["failed_parts"]) + " 실패")' in server
|
||||
|
||||
|
||||
def test_종단_보기_상단측_차례도_같은_창구():
|
||||
|
||||
@@ -231,17 +231,22 @@ def test_M5_새_행은_버린_수에_안_셈():
|
||||
|
||||
|
||||
def test_H5_노선_저장_뒤_본문_M7_kindreset_옮김():
|
||||
store = (ROOT / "B06_Section" / "B06_Section_Section_Store.ts").read_text(encoding="utf-8")
|
||||
body = store[store.index("export async function saveCachedCrossPatches") :]
|
||||
# 472-6 6-B — 노선 저장 뒤 본문 = 서버 Node 넘김(`routeCarryPatches` · 옛 `saveCachedCrossPatches` ·
|
||||
# `sessionOverlay` 몸통 그대로) · 표준 횡단면 · 풍선 = 브라우저 횡단 조각(`routeCarryPart`)
|
||||
patch = (ROOT / "B06_Section" / "B06_Section_Route_Carry_Patch.ts").read_text(encoding="utf-8")
|
||||
body = patch[patch.index("export function routeCarryPatches") :]
|
||||
for needle in (
|
||||
"sessionOverlay(projectId, routeId)",
|
||||
"readStandardCrossSession(projectId)",
|
||||
"balloon_offsets: balloonOffsetsPayload()",
|
||||
"crossPatchesFromCache(detail, kindChanged, savedAt)",
|
||||
'read<CardChoice>("crossdesign")',
|
||||
'read<unknown>("rockb")',
|
||||
'read<number>("cutslope")',
|
||||
"mergeCrossPatches(",
|
||||
):
|
||||
assert needle in body, needle
|
||||
overlay = store[store.index("function sessionOverlay") :]
|
||||
for needle in ("crossDesignChoices(", "readRockBoundarySession(", "readCutSlopeSession("):
|
||||
assert needle in overlay, needle
|
||||
apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
||||
part = apply[apply.index("export function routeCarryPart") :]
|
||||
for needle in ("readStandardCrossSession(projectId)", "balloon_offsets: balloonOffsetsPayload()"):
|
||||
assert needle in part, needle
|
||||
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
assert (
|
||||
"cachedCrossPatches(projectId, routeId, edits.crossPatches)" in persist
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
· 관 정본 PUT = 세션 관 초안 한 번(B06 고침 직후 [저장] = 초안 값) · 배수판 메모리는 안 보냄
|
||||
· 초안 없음 = [저장] PUT 없음(판이 안 서도 관 수 그대로) · [확정] = 칸 없이(서버 저장분) · 빈 초안 = 다 지움
|
||||
· 노선 저장 뒤 구조물 차례 실패 → 0 칸 계획고가 새 노선 칸에 · 횡단 초안 넘김(저장 없이) · 다시 세움 →
|
||||
다음 [저장]이 새 노선으로 계획고를 보냄 · 넘김 자체가 실패하면 두 번 안 넘김
|
||||
· 노선 바뀐 [저장] 중 구조물 · 횡단 넘김 차례 실패(472-6 6-B) → 서버가 노선까지 되돌림 · 초안 그대로(0 칸
|
||||
계획고 그대로 · 다시 안 세움) → 다음 [저장]이 같은 초안을 그대로 다시 보냄(옛 433-12 「새 노선 칸으로 옮김」 갈음)
|
||||
· 관 PUT 끝 측점 맞춤 실패 = 응답 `stations_failed`
|
||||
짝: `helper_449_save_pipes.cjs`(실제 `design_save.ts` 를 Node 로 · 서버 창구만 가짜).
|
||||
"""
|
||||
@@ -69,19 +69,23 @@ def test_빈_초안은_다_지움(out):
|
||||
|
||||
|
||||
def test_노선_저장_뒤_실패_초안_새_노선(out):
|
||||
"""472-6 6-B — 한 본문 · 서버가 구조물 차례에서 실패하면 노선까지 되돌림 → 초안 그대로 · 다시 안 세움."""
|
||||
case = out["routeFail"]
|
||||
assert not case["ok"] and "구조물" in case["failure"]
|
||||
assert case["calls"] == [["ROUTE"], ["CARRY", 7, 8, False], ["REBUILD"]]
|
||||
assert out["alignAfterFail"] == {"8": {"pvi": {"120": 1.5}}} # 0 칸 비움 · 새 노선 칸
|
||||
assert not case["ok"] and "구조물 차례에서 실패" in case["failure"]
|
||||
assert case["calls"] == [["SAVE", "none", False, "ROUTE"], ["ALIGN", 7, {"pvi": {"120": 1.5}}]]
|
||||
assert out["alignAfterFail"] == {"0": {"pvi": {"120": 1.5}}} # 노선 초안 칸 그대로
|
||||
align = [call for call in out["nextSave"]["calls"] if call[0] == "ALIGN"]
|
||||
assert align == [["ALIGN", 8, {"pvi": {"120": 1.5}}]]
|
||||
assert align == [["ALIGN", 7, {"pvi": {"120": 1.5}}]] # 다음 [저장]이 그대로 다시
|
||||
assert (
|
||||
out["nextSave"]["calls"][-1] == ["REBUILD"] and ["ROUTE_SAVED"] in out["nextSave"]["calls"]
|
||||
)
|
||||
|
||||
|
||||
def test_넘김_실패면_두_번_안_넘김(out):
|
||||
"""472-6 6-B — 넘김은 서버 한 본문 안(Node) · 실패 = 노선까지 되돌림 · 화면 넘김 · 다시 세움 없음."""
|
||||
case = out["carryFail"]
|
||||
assert not case["ok"]
|
||||
assert [call for call in case["calls"] if call[0] == "CARRY"] == [["CARRY", 7, 8, True]]
|
||||
assert case["calls"][-1] == ["REBUILD"]
|
||||
assert not case["ok"] and "횡단 차례에서 실패" in case["failure"]
|
||||
assert case["calls"] == [["SAVE", "none", False, "ROUTE"]]
|
||||
|
||||
|
||||
def test_공용_저장이_판_메모리를_안_보냄():
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Node 입구 = `A00_Common/design/logic/design_calc_node.ts`(`build:design-calc` · id → 함수 표).
|
||||
|
||||
못박는 것
|
||||
- id · # 하나씩 · 1.1 표 # 전부(R1 – N1 · 43).
|
||||
- id · # 하나씩 · 1.1 표 # 전부(R1 – N1 · 43) + R9 · F10(472-6 6-B).
|
||||
- id 마다 실행기 있음 — `node:` 는 입구 표에 · `py:` · `browser:` · ts · 짝 이름은 그 파일에 ·
|
||||
거울 시험 파일 있음.
|
||||
- 묶음 `STAGE_DEPS`(`design_pipeline.ts`) = 등록부 입력에서 만든 기댐(같은지만 · 묶음 안 고침).
|
||||
@@ -46,6 +46,8 @@ NODE_IDS = {
|
||||
"facility.follow",
|
||||
"cross.design",
|
||||
"cross.berm_spans",
|
||||
"facility.rebase",
|
||||
"stations.carry",
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
@@ -101,6 +103,8 @@ GEOMETRY_PULLS = (
|
||||
"B06_Section/B06_Section_UI_Cross_Revetment.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Wall.ts",
|
||||
"B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts",
|
||||
# 노선 바뀐 [저장] 넘김 patch(472-6 6-B) — 세션 칸 → 설계 칸 얹기가 관 벽 · 단 · 세월 · BOX 기본값(위 그림 파일)을 씀
|
||||
"B06_Section/B06_Section_Route_Carry_Patch.ts",
|
||||
)
|
||||
KNOWN_PULLS = set(SESSION_PULLS) | set(GEOMETRY_PULLS)
|
||||
|
||||
@@ -190,8 +194,9 @@ def test_id_하나씩_표_전부(loaded) -> None:
|
||||
nos = [row["no"] for row in rows]
|
||||
assert len(ids) == len(set(ids))
|
||||
assert len(nos) == len(set(nos))
|
||||
want = {f"{k}{i}" for k, n in (("R", 8), ("G", 4), ("P", 8), ("C", 7)) for i in range(1, n + 1)}
|
||||
want |= {f"F{i}" for i in range(1, 10)} | {"E1", "E2"} | {f"D{i}" for i in range(1, 5)} | {"N1"}
|
||||
want = {f"{k}{i}" for k, n in (("R", 9), ("G", 4), ("P", 8), ("C", 7)) for i in range(1, n + 1)}
|
||||
# R9 · F10 = 노선 바뀐 [저장] 다시 얹기(472-6 6-B)
|
||||
want |= {f"F{i}" for i in range(1, 11)} | {"E1", "E2"} | {f"D{i}" for i in range(1, 5)} | {"N1"}
|
||||
assert set(nos) == want
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
"""PLAN 472-6 6-B — 노선 바뀐 [저장]도 한 본문 · 다시 얹기 서버 Node(같은 TS).
|
||||
|
||||
① 넘김 — 브라우저 옛 길(세션 함수를 옛 `carryAfterRouteSave` → `saveCachedCrossPatches`
|
||||
차례로) 결과 == 서버 Node `stations.carry`(`routeCarryPatches`) 결과(patch · 버린 수) ·
|
||||
손 상단측 안 넘김(요구 274).
|
||||
② 다시 얹기 — 관 · 구조물 초안(`rebasePipeDraft` · `rebaseStructureDraft`) ==
|
||||
Node `facility.rebase`.
|
||||
③ 옛 코드 실패 — 한 본문이 `route` · `carry` 칸을 받음 · 브라우저가 노선 저장 창구를 안 부름.
|
||||
④ 실제 창구(검증_sub8 · DB · storage) — 노선 차례 뒤 실패(시험 갈래 · 넘김 실패) = 노선 행 ·
|
||||
자식 표 · storage 그대로 · 정상 한 본문 = 옛 차례(노선 저장 → 다시 얹기 → 횡단 저장)와
|
||||
행 · 파일 같음.
|
||||
검증 프로젝트가 없는 자리(다른 PC · DB 없음)는 ④ 건너뜀.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_6_route_save.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from common_util import common_util_save_guard as guard # noqa: E402
|
||||
from common_util.common_util_calc_runner import run_calc # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _node(*args: str, timeout: int = 300) -> str:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", *args], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", timeout=timeout
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return proc.stdout
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory) -> dict:
|
||||
"""vite ssr 로 진짜 모듈을 묶어 옛 길을 돌림(417 꼴)."""
|
||||
work = tmp_path_factory.mktemp("r6b")
|
||||
_node(
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_472_6_route_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
)
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_472_6_route_entry.js", bundle)
|
||||
return json.loads(_node(str(HERE / "helper_472_6_route_carry.mjs"), str(bundle)))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def carried(out) -> dict:
|
||||
inputs = {
|
||||
**out["carry"],
|
||||
"detail": out["detail"],
|
||||
"pipes_before": out["pipesBefore"],
|
||||
"pipes_after": out["pipesAfter"],
|
||||
}
|
||||
result = run_calc(["stations.carry"], {"carry": inputs})
|
||||
assert result is not None
|
||||
return result["stations.carry"]
|
||||
|
||||
|
||||
def _by_chainage(patches: list[dict]) -> dict[str, dict]:
|
||||
return {f"{patch['chainage_m']:.2f}": patch for patch in patches}
|
||||
|
||||
|
||||
def test_넘김_옛_브라우저_길과_서버_Node_같음(out, carried) -> None:
|
||||
assert carried["patches"] == out["oldPatches"]
|
||||
assert carried["outside"] == out["oldOutside"]
|
||||
|
||||
|
||||
def test_넘김_값_사용자_칸(out, carried) -> None:
|
||||
"""같은 값이 뜻있는지 — 카드 선택 · 사면 쉬프트 · 암선 · 암 경사(MC 옮김) · 관 조절 · 메모가
|
||||
새 측점에."""
|
||||
by = _by_chainage(carried["patches"])
|
||||
assert by["20.00"]["ground_type"] == "soil" and by["20.00"]["two_stage_slope"] is True
|
||||
assert by["20.00"]["ditch_type"] == "u_flume"
|
||||
assert "section_mode" not in by["20.00"] # 단면 유형 = 새 지형이 정함
|
||||
assert by["20.00"]["structure_note"] == "앞 메모"
|
||||
assert by["40.00"]["shoulder_extend_left_m"] == 0.5
|
||||
assert "revet_adjust" not in by["40.00"] # 일반 측점 옹벽 조절 = 안 넘김(관 · 구조물 열쇠만)
|
||||
assert by["60.00"]["display_half_width_m"] == 6 # 세션 폭이 저장분(5) 위에
|
||||
assert by["103.00"]["rock_boundary_offset_m"] == 1.5
|
||||
assert by["112.00"]["cut_slope_ratio_user"] == 0.8 # MC 110 → 112(곡선 열쇠 옮김)
|
||||
pipe = by["83.00"]
|
||||
assert pipe["revet_adjust"]["outlet"]["x"] == 0.3 # 관 따라 80 → 83
|
||||
assert pipe["revet_adjust"]["inlet"]["h"] == 1.8 # 관 옵션 벽 높이(저장 관 · 새 자리)
|
||||
assert pipe["basin_adjust"]["depth_m"] == 1.2 and pipe["detail_table"] == {"rows": 2}
|
||||
assert pipe["revet_link_detached"] is True
|
||||
assert not any(
|
||||
p.get("structure_reset") for p in carried["patches"]
|
||||
) # 일반 측점 종류 바꿈 = 버림
|
||||
assert carried["outside"] == out["oldOutside"] >= 4 # 구조물 130 고침 셋 · 40 옹벽 · 60 종류
|
||||
|
||||
|
||||
def test_손_상단측은_안_넘김_요구274(out) -> None:
|
||||
assert "uphill" not in out["carry"]["states"]
|
||||
assert out["carry"]["states"]["kindreset"] == {"60.00": "culvert"}
|
||||
|
||||
|
||||
def test_다시_얹기_옛_함수와_서버_Node_같음(out) -> None:
|
||||
result = run_calc(["facility.rebase"], {"rebase": out["rebaseInput"]})
|
||||
assert result is not None
|
||||
assert result["facility.rebase"] == out["oldRebase"]
|
||||
pipes = {pipe["id"]: pipe["chainage_m"] for pipe in result["facility.rebase"]["pipes"]}
|
||||
assert pipes == {"p80": 83, "p20": 25, "pnew": 120} # 따라감 · 사용자 옮김 그대로 · 새 교차 관
|
||||
assert result["facility.rebase"]["structures"][0]["chainage_m"] == 63
|
||||
|
||||
|
||||
def test_옛_코드_실패_한_본문이_노선을_받음() -> None:
|
||||
from B06_Section.B06_Section_Router_DesignSave import DesignSaveRequest
|
||||
|
||||
request = DesignSaveRequest.model_validate(
|
||||
{
|
||||
"route_id": 7,
|
||||
"route": {"draft": {"nodes": [[0, 0], [10, 0]]}},
|
||||
"carry": {"states": {}, "from_keys": None, "owners": [], "pipe_ids": None},
|
||||
}
|
||||
)
|
||||
assert request.route is not None and request.carry is not None
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text(encoding="utf-8")
|
||||
assert "saveRouteDraft" not in save and "carryAfterRouteSave" not in save
|
||||
assert re.search(r"postDesignSave\([^;]*route \? \{ route, carry", save, re.S)
|
||||
apply = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
||||
assert "export async function carryAfterRouteSave" not in apply
|
||||
|
||||
|
||||
# ── 실제 창구 ──────────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT = "9872e9a7-e483-427b-a6b2-72cdbc063bc3" # 검증_sub8(OWNERS.md)
|
||||
VOLATILE = ("created_at", "updated_at", "confirmed_at", "started_at", "completed_at")
|
||||
|
||||
|
||||
def _db_ready() -> Path | None:
|
||||
try:
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import close_db_pool, init_db_pool
|
||||
|
||||
async def find():
|
||||
pool = await init_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
return await get_project_storage_relative_path(connection, UUID(PROJECT))
|
||||
finally:
|
||||
await close_db_pool()
|
||||
|
||||
root = Path(resolve_stored_project_path(asyncio.run(find())))
|
||||
return root if (root / "B05_Profile" / "route").is_dir() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
ROOT_DIR = _db_ready()
|
||||
live = pytest.mark.skipif(ROOT_DIR is None, reason="검증_sub8 · DB 없음")
|
||||
|
||||
|
||||
def _rows() -> dict:
|
||||
from config.config_db import close_db_pool, init_db_pool
|
||||
|
||||
async def run():
|
||||
pool = await init_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
return await guard.snapshot_rows(connection, PROJECT, surface=True)
|
||||
finally:
|
||||
await close_db_pool()
|
||||
|
||||
return asyncio.run(run())
|
||||
|
||||
|
||||
def _state(root: Path) -> tuple[dict, dict]:
|
||||
"""(행 — 시각 칸 뺌, 파일 내용 해시) — 노선 바뀐 저장 범위 전부."""
|
||||
import hashlib
|
||||
|
||||
clean = {
|
||||
table: [{k: str(v) for k, v in row.items() if k not in VOLATILE} for row in values]
|
||||
for table, values in _rows().items()
|
||||
}
|
||||
files = {
|
||||
relative: hashlib.sha1(path.read_bytes()).hexdigest()
|
||||
for relative, path in guard._files(root, guard.STORAGE_SCOPE + guard.PIPE_SCOPE).items()
|
||||
if not relative.startswith("_derived/")
|
||||
}
|
||||
return clean, files
|
||||
|
||||
|
||||
def _plan_draft() -> dict:
|
||||
"""지금 계획노선 노드 · 곡선 그대로 + 가운데 노드 하나를 3 m 옆으로(노선 끌기)."""
|
||||
from B05_Profile.B05_Profile_Router_Replan import read_route_plan
|
||||
from config.config_db import close_db_pool, init_db_pool
|
||||
|
||||
async def run():
|
||||
await init_db_pool()
|
||||
try:
|
||||
return await read_route_plan(UUID(PROJECT))
|
||||
finally:
|
||||
await close_db_pool()
|
||||
|
||||
plan = asyncio.run(run())
|
||||
nodes = plan["nodes"]
|
||||
at = len(nodes) // 2
|
||||
points = [[float(n["x"]), float(n["y"])] for n in nodes]
|
||||
points[at][1] += 3.0
|
||||
return {
|
||||
"rev": 1,
|
||||
"nodes": points,
|
||||
"curveOn": [True] * len(nodes),
|
||||
"curveRadius": [None] * len(nodes),
|
||||
"stationSnap": bool(plan.get("station_snap")),
|
||||
"stationSnapStep": plan.get("station_snap_step"),
|
||||
}
|
||||
|
||||
|
||||
def _save(body: dict):
|
||||
from B06_Section.B06_Section_Router_DesignSave import DesignSaveRequest, save_design
|
||||
from config.config_db import close_db_pool, init_db_pool
|
||||
|
||||
async def run():
|
||||
await init_db_pool()
|
||||
try:
|
||||
return await save_design(UUID(PROJECT), DesignSaveRequest.model_validate(body), None)
|
||||
finally:
|
||||
await close_db_pool()
|
||||
|
||||
response = asyncio.run(run())
|
||||
return response.status_code, json.loads(bytes(response.body).decode("utf-8"))
|
||||
|
||||
|
||||
def _latest_route_id() -> int:
|
||||
return max(int(row["id"]) for row in _rows()["routes"])
|
||||
|
||||
|
||||
def _body(draft: dict, **extra) -> dict:
|
||||
return {
|
||||
"route_id": _latest_route_id(),
|
||||
"route": {"draft": draft},
|
||||
"carry": {"states": {}, "from_keys": None, "owners": [], "pipe_ids": None},
|
||||
"cross": {},
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
@live
|
||||
def test_노선_차례_뒤_실패면_노선_행_storage_그대로(monkeypatch) -> None:
|
||||
before = _state(ROOT_DIR)
|
||||
monkeypatch.setenv("AISLO_DESIGN_SAVE_FAIL_AFTER", "route")
|
||||
status, body = _save(_body(_plan_draft()))
|
||||
assert status == 409 and body["step"] == "route" and body["restored"] is True, body
|
||||
assert _state(ROOT_DIR) == before # 새 노선 행 · 자식 표 · 노선 파일 · 관 · 구조물 그대로
|
||||
|
||||
|
||||
@live
|
||||
def test_넘김_실패면_노선까지_그대로(monkeypatch) -> None:
|
||||
from B06_Section import B06_Section_Router_DesignSave_Route as moving
|
||||
|
||||
before = _state(ROOT_DIR)
|
||||
|
||||
def broken(ids, inputs):
|
||||
if ids == ["stations.carry"]:
|
||||
raise RuntimeError("넘김 일부러 실패")
|
||||
return run_calc(ids, inputs)
|
||||
|
||||
monkeypatch.setattr(moving, "run_calc", broken)
|
||||
status, body = _save(_body(_plan_draft()))
|
||||
assert status == 409 and body["step"] == "cross" and body["restored"] is True, body
|
||||
assert "넘김 일부러 실패" in body["message"]
|
||||
assert _state(ROOT_DIR) == before
|
||||
|
||||
|
||||
def _normalized(state: tuple[dict, dict], new_ids: set[int]) -> tuple[dict, dict]:
|
||||
"""새 노선 번호 · 행 id · 계산한 때(`computed_at`)를 지운 비교 꼴 — 옛 길 · 새 길이 다른
|
||||
자동 번호 · 시각을 받음."""
|
||||
rows, files = state
|
||||
numbers = "|".join(str(i) for i in sorted(new_ids, reverse=True)) or "x"
|
||||
# 3D 파일 = 0 채운 번호(`corridor_0444.json`)
|
||||
rename = re.compile(rf"(?<!\d)0*({numbers})(?!\d)")
|
||||
keep = {}
|
||||
for table, values in rows.items():
|
||||
out = []
|
||||
for row in values:
|
||||
row = {k: v for k, v in row.items() if k not in ("id", "route_id", "computed_at")}
|
||||
out.append(rename.sub("N", json.dumps(row, sort_keys=True, ensure_ascii=False)))
|
||||
keep[table] = sorted(out)
|
||||
return keep, {rename.sub("N", k): v for k, v in files.items()}
|
||||
|
||||
|
||||
@live
|
||||
def test_정상_한_본문은_옛_차례와_같음() -> None:
|
||||
from B05_Profile.B05_Profile_Route_Save import RouteSaveRequest
|
||||
from B05_Profile.B05_Profile_Router_Replan import replan_route
|
||||
from B06_Section import B06_Section_Router_DesignSave_Route as moving
|
||||
from B06_Section.B06_Section_Router_Confirm import save_sections
|
||||
from B06_Section.B06_Section_Schema import SectionConfirmRequest
|
||||
from config.config_db import close_db_pool, get_db_pool, init_db_pool
|
||||
|
||||
start = _state(ROOT_DIR)
|
||||
draft = _plan_draft()
|
||||
old_ids = {int(row["id"]) for row in _rows()["routes"]}
|
||||
|
||||
async def old_way():
|
||||
"""옛 브라우저 차례 — 노선 저장(`/route/replan`) → 횡단 넘김 patch → 횡단 저장."""
|
||||
await init_db_pool()
|
||||
try:
|
||||
async with get_db_pool().acquire() as connection:
|
||||
rows = await guard.snapshot_rows(connection, PROJECT, surface=True)
|
||||
files = guard.snapshot_storage(ROOT_DIR, guard.STORAGE_SCOPE + guard.PIPE_SCOPE)
|
||||
before = await moving.saved_facilities(UUID(PROJECT))
|
||||
saved = await replan_route(
|
||||
UUID(PROJECT), RouteSaveRequest.model_validate({"draft": draft})
|
||||
)
|
||||
carried = await moving.carry_cross(UUID(PROJECT), int(saved["route_id"]), None, before)
|
||||
await save_sections(
|
||||
UUID(PROJECT),
|
||||
int(saved["route_id"]),
|
||||
SectionConfirmRequest.model_validate({"cross_patches": carried["patches"] or None}),
|
||||
)
|
||||
return rows, files
|
||||
finally:
|
||||
await close_db_pool()
|
||||
|
||||
rows, files = asyncio.run(old_way())
|
||||
old = _state(ROOT_DIR)
|
||||
|
||||
async def undo():
|
||||
await init_db_pool()
|
||||
try:
|
||||
files.restore()
|
||||
async with get_db_pool().acquire() as connection:
|
||||
await guard.restore_rows(connection, PROJECT, rows)
|
||||
finally:
|
||||
await close_db_pool()
|
||||
|
||||
asyncio.run(undo())
|
||||
assert _state(ROOT_DIR) == start
|
||||
try:
|
||||
status, body = _save(_body(draft))
|
||||
assert status == 200 and body["status"] == "success", body
|
||||
assert set(body["steps"]) == {"route", "carry", "cross"}
|
||||
new = _state(ROOT_DIR)
|
||||
ids = {int(row["id"]) for row in _rows()["routes"]} | {
|
||||
int(row["id"]) for row in json.loads(json.dumps(old[0]["routes"]))
|
||||
}
|
||||
ids -= old_ids
|
||||
(rows_new, files_new), (rows_old, files_old) = _normalized(new, ids), _normalized(old, ids)
|
||||
assert [t for t in rows_new if rows_new[t] != rows_old.get(t)] == []
|
||||
assert (
|
||||
sorted(
|
||||
k
|
||||
for k in files_new.keys() | files_old.keys()
|
||||
if files_new.get(k) != files_old.get(k)
|
||||
)
|
||||
== []
|
||||
)
|
||||
finally:
|
||||
asyncio.run(undo())
|
||||
files.discard()
|
||||
assert _state(ROOT_DIR) == start
|
||||
@@ -15,32 +15,28 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
#: (파일, 함수 이름, 비었을 때 돌아가는 표시)
|
||||
#: (파일, 함수 이름, 비었을 때 돌아가는 표시) — 472-6 한 본문 조각(빈 초안 = null 조각 = 서버가 그 차례를
|
||||
#: 건너뜀 · 옛 창구 함수는 6-B 에서 걷음). 상단측은 463 부터 늘 나감(비정규 측점 횡단 맞춤 · 노선 바뀜이면 안 나감).
|
||||
CHANNELS = [
|
||||
(
|
||||
ROOT / "B05_Profile" / "B05_Profile_Api_Pipes_Draft.ts",
|
||||
"flushPendingPipes",
|
||||
"pipesPart",
|
||||
"관 목록",
|
||||
),
|
||||
(
|
||||
ROOT / "B05_Profile" / "B05_Profile_Api_Fetch.ts",
|
||||
"flushUphillOverrides",
|
||||
"상단측(측구 방향)",
|
||||
),
|
||||
(
|
||||
ROOT / "B05_Profile" / "B05_Profile_Api_Structures.ts",
|
||||
"flushPendingStructures",
|
||||
"structuresPart",
|
||||
"구조물",
|
||||
),
|
||||
(
|
||||
ROOT / "B06_Section" / "B06_Section_Api_Culvert_Options.ts",
|
||||
"flushCulvertOptions",
|
||||
"culvertOptionsPart",
|
||||
"관 옵션",
|
||||
),
|
||||
]
|
||||
|
||||
#: 비었을 때 빠져나가는 모양 — `return;` 만 있는 이른 반환.
|
||||
_EARLY_RETURN = re.compile(r"\breturn\s*;")
|
||||
#: 비었을 때 빠져나가는 모양 — 빈 조각(`return null;`) 이른 반환.
|
||||
_EARLY_RETURN = re.compile(r"\breturn\s+null\s*;")
|
||||
|
||||
|
||||
def _body(path: Path, name: str) -> str:
|
||||
@@ -69,21 +65,20 @@ def test_저장이_창구를_다섯_다_거친다():
|
||||
B05 [임시저장] · B06 [저장] · [확정]이 모두 그것을 탄다."""
|
||||
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text(encoding="utf-8")
|
||||
for token in (
|
||||
"flushCulvertOptions(",
|
||||
"flushRouteParts(", # 상단측 차례 = 종단 보기 몫과 같은 본문(463)
|
||||
"flushPendingPipes(",
|
||||
"flushPendingStructures(",
|
||||
"culvertOptionsPart(",
|
||||
"body: await routePartsBody(projectId),", # 상단측 차례 = 종단 보기 몫과 같은 본문(463)
|
||||
"pipesPart(",
|
||||
"structuresPart(",
|
||||
):
|
||||
assert token in save, f"{token} 가 저장 한 줄기에서 사라짐"
|
||||
# 472-6 — 노선 안 바뀐 저장은 한 본문 조각(조정창 구간값 = 세션 `culvertopt`) · 노선 바뀐
|
||||
# 저장은 위 창구 차례 그대로
|
||||
# 472-6 — 노선이 바뀌든 안 바뀌든 한 본문 조각(조정창 구간값 = 세션 `culvertopt` · 옛 창구 차례 걷음)
|
||||
assert 'culvertOptionsPart(stateKey("culvertopt", projectId, routeId))' in save, (
|
||||
"B06 조정창 구간값이 한 본문 저장에서 빠짐"
|
||||
)
|
||||
# 차례가 실패하면 그 차례에서 멈추고 이름째 알림(429 ⑨) · 한 본문은 서버가 차례 이름째 되돌림
|
||||
assert "throw stepFailed(step, error)" in save, "실패한 차례에서 멈춤이 빠짐"
|
||||
# 차례가 실패하면 서버가 차례 이름째 되돌림(429 ⑨ · 전부 아니면 없음)
|
||||
assert "STEP_LABEL[error.step]" in save, "한 본문 실패 차례 이름이 빠짐"
|
||||
order = save[save.index("DESIGN_SAVE_ORDER = [") :]
|
||||
api = (ROOT / "A00_Common" / "design" / "design_save_api.ts").read_text(encoding="utf-8")
|
||||
order = api[api.index("SAVE_PART_KEYS = [") :]
|
||||
order = order[: order.index("]")]
|
||||
names = ["pipes", "structures", "alignment", "culvertOptions", "uphill", "cross"]
|
||||
assert [order.index(f'"{n}"') for n in names] == sorted(order.index(f'"{n}"') for n in names), (
|
||||
|
||||
@@ -38,8 +38,8 @@ def test_저장이_면적을_모으기_전에_다시_계산한다() -> None:
|
||||
shared = _body(SHARED.read_text(encoding="utf-8"), "export async function saveSharedDesign")
|
||||
assert "prepare: crossSave?.prepare" in shared, "한 벌이 재계산을 저장 한 줄기에 안 넘긴다"
|
||||
save = SAVE.read_text(encoding="utf-8")
|
||||
# 472-6 — 노선 안 바뀐 저장 = 한 본문(`saveOneBody`) · 재계산 뒤에 횡단 조각을 모음
|
||||
run = _body(save, "async function saveOneBody", 4000)
|
||||
# 472-6 — [저장] = 한 본문(`saveDesign` · 노선 바뀜도) · 재계산 뒤에 횡단 조각을 모음
|
||||
run = _body(save, "export async function saveDesign", 4000)
|
||||
assert run.index("ctx.prepare?.()") < run.index("ctx.crossPart(confirm)"), (
|
||||
"저장 한 줄기가 재계산보다 면적 수집을 먼저 부른다 — 옛 면적이 실린다"
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ Node 헬퍼(`helper_design_store_save.cjs`)가 TS 를 트랜스파일해 돌린
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
@@ -48,8 +49,14 @@ def test_계획고_가게_한_벌(out):
|
||||
|
||||
|
||||
def test_저장_차례(out):
|
||||
assert out["order"] == [
|
||||
"route",
|
||||
# 472-6 — 차례 = 한 본문 칸(브라우저) · 서버 한 창구 · 노선(346-3 · 6-B)은 서버 맨 앞
|
||||
api = open(
|
||||
os.path.join(HERE, "..", "..", "A00_Common", "design", "design_save_api.ts"),
|
||||
encoding="utf-8",
|
||||
).read()
|
||||
keys = api[api.index("SAVE_PART_KEYS = [") :]
|
||||
keys = re.findall(r'"([A-Za-z]+)"', keys[: keys.index("]")])
|
||||
assert keys == [
|
||||
"settings", # 448 페이지 설정 — 노선 뒤
|
||||
"pipes",
|
||||
"structures",
|
||||
@@ -58,7 +65,13 @@ def test_저장_차례(out):
|
||||
"culvertOptions",
|
||||
"uphill",
|
||||
"cross",
|
||||
] # 346-3 노선 먼저
|
||||
]
|
||||
server = open(
|
||||
os.path.join(HERE, "..", "..", "B06_Section", "B06_Section_Router_DesignSave.py"),
|
||||
encoding="utf-8",
|
||||
).read()
|
||||
at = [re.search(rf'step\(\s*"{key}"', server).start() for key in ["route", *keys]]
|
||||
assert at == sorted(at)
|
||||
assert out["saveOk"] is True
|
||||
# 472-6 — 노선 초안 없으면 조각을 모아 한 요청(서버 한 창구 · 전부 아니면 없음) ·
|
||||
# 저장 뒤 조각 정리는 차례대로 · 본문 없는 조각(추가 측점 초안 없음)도 정리는 돎
|
||||
@@ -97,19 +110,21 @@ def test_초기화는_초안과_한_벌을_버림(out):
|
||||
|
||||
|
||||
def test_노선이_바뀌면_노선_먼저_뒤_차례는_새_노선(out):
|
||||
"""346-3 — 노선 초안이 있으면 맨 앞 · 관 · 구조물 기본 창구 · 계획고 초안(옛 노선 7 칸)을 새 노선 9 로 ·
|
||||
화면 몫 일(옛 노선 판)은 건너뜀 · 횡단 초안 넘김(측점 열쇠) · 결과 한 줄 · 다시 세움."""
|
||||
"""346-3 · 472-6 6-B — 노선 초안이 있으면 같은 한 본문에 `route` · `carry`(옛 노선 7 세션 칸) · 관 · 구조물
|
||||
기본 조각 · 계획고 초안(옛 노선 7 칸 · 서버가 새 노선 9 로) · 상단측 안 보냄(요구 274) · 화면 몫 일(옛 노선 판)은
|
||||
건너뜀 · 횡단 = 넘김 조각 · 다 저장한 뒤 정리 · 결과 한 줄 · 다시 세움."""
|
||||
assert out["routeOk"] is True
|
||||
assert out["routeCalls"] == [
|
||||
"overlay:on",
|
||||
"prepare",
|
||||
"route",
|
||||
"pipes",
|
||||
"structures",
|
||||
"plusStations",
|
||||
'alignment:9:{"30.000":0.2}',
|
||||
"culvert:aislo:draft:culvertopt:p1:7",
|
||||
"carry:7->9", # 상단측은 넘김(carryAfterRouteSave)이 새 측점으로 옮긴 뒤 보냄(443-2)
|
||||
"post:7:false:pipes,structures,alignment,culvertOptions,cross:route(carry 7)",
|
||||
'alignment:7:{"30.000":0.2}',
|
||||
"routeSaved",
|
||||
"pipes:after",
|
||||
"structures:after",
|
||||
"plusStations:after",
|
||||
"culvert:after:aislo:draft:culvertopt:p1:7",
|
||||
"cross:carried:after",
|
||||
"summary:9:2",
|
||||
"toast:info",
|
||||
"done:false",
|
||||
@@ -121,24 +136,31 @@ def test_노선이_바뀌면_노선_먼저_뒤_차례는_새_노선(out):
|
||||
|
||||
|
||||
def test_노선이_그대로면_안_보냄(out):
|
||||
assert "route" not in out["saveCalls"]
|
||||
assert not any("route" in call for call in out["saveCalls"])
|
||||
|
||||
|
||||
def test_노선_저장_실패는_멈추고_초안_남음(out):
|
||||
assert out["routeFailOk"] is False
|
||||
assert out["routeFailCalls"] == ["overlay:on", "route", "failed", "overlay:off"]
|
||||
assert out["routeFailCalls"] == [
|
||||
"overlay:on",
|
||||
"post:7:false:pipes,structures,alignment,culvertOptions,cross:route(carry 7)",
|
||||
'alignment:7:{"50.000":0.3}',
|
||||
"failed", # 서버가 노선 차례에서 되돌림 · 정리 없음
|
||||
"overlay:off",
|
||||
]
|
||||
assert out["routeFailDraft"] == {"50.000": 0.3} # 계획고 초안 그대로
|
||||
|
||||
|
||||
def test_확정_중_노선이_바뀌면_다시_세우고_확정은_다시(out):
|
||||
calls = out["routeConfirmCalls"]
|
||||
assert calls[1] == "route" and calls[-2:] == ["rebuild", "overlay:off"]
|
||||
assert calls[1].startswith("post:7:true:") and calls[1].endswith(":route(carry 7)")
|
||||
assert calls[-2:] == ["rebuild", "overlay:off"]
|
||||
assert "done:true" not in calls and "cross:true" not in calls
|
||||
|
||||
|
||||
def test_노선_초안_칸_계획고가_새_노선으로(out):
|
||||
"""346-3 덧 — 노선 초안 동안 계획고는 노선 0 칸(sub7) · 노선 저장 뒤 그 칸이 새 노선 9 로 · 0 · 7 칸 다 비움."""
|
||||
assert out["draftRouteCalls"] == ['alignment:9:{"35.000":0.4}']
|
||||
assert out["draftRouteCalls"] == ['alignment:7:{"35.000":0.4}'] # 서버가 새 노선 9 로
|
||||
assert out["draftRouteLeft"] == {"draft": None, "old": None}
|
||||
design = os.path.join(HERE, "..", "..", "A00_Common", "design")
|
||||
with open(os.path.join(design, "design_pipeline_page.ts"), encoding="utf-8") as f:
|
||||
|
||||
Reference in New Issue
Block a user