Merge remote-tracking branch 'origin/dev' into sub_laptop_6

This commit is contained in:
2026-10-18 19:32:00 +09:00
45 changed files with 1347 additions and 86 deletions
@@ -17,6 +17,7 @@ const STUBS = {
showLoadingOverlay: () => calls.overlay++,
hideLoadingOverlay: () => {},
showToast: () => {},
showConfirmDialog: async () => true, // 다시 만들기 확인창(446) — 이 시험은 늘 확인
},
"./B06_Section_Api_Fetch": {
regenerateSections: async (_p, _r, width) => calls.regenerate.push(width),
+6 -1
View File
@@ -95,15 +95,17 @@ async function toggles(projectId, shift) {
const steps = [];
const run = async (tag) => {
const updated = [];
let calls = 0; // 받는 쪽 = 카드 다시 그림 + 종단 · 유토 다시 그림(design_shared) — 한 걸음에 한 번
const out = await m.refreshSavedCross(
{ projectId, routeId: R, detail, edits: EDITS, alignment },
stations,
(rows) => updated.push(...rows),
(rows) => (calls++, updated.push(...rows)),
);
steps.push({
tag,
out: out ? Object.keys(out.byKey).length : null,
updated,
calls,
...pick(target.design),
});
};
@@ -126,6 +128,9 @@ async function toggles(projectId, shift) {
m.writeCrossDesignChoice(projectId, R, target.chainage_m, { ...choice });
await run(tag);
}
// 사면 이동 한 걸음(B06 ◀▶ 0.1 m · 깎기 쪽 · 441 · 요구 268-4)
m.writeShoulderExtend(projectId, R, target.chainage_m, job.mode.split("_")[0], 0.1);
await run("slope");
return { target: target.chainage_m, steps };
}
@@ -1,5 +1,8 @@
/* PLAN 439 시험 묶음 진입점 — 저장 노선 cross 차례(카드 단추 → 다시 셈 · 측점 어긋남) · 진짜 모듈 · vite ssr. */
export { refreshSavedCross } from "../../A00_Common/design/design_shared";
export { writeCrossDesignChoice } from "../../B06_Section/B06_Section_Cross_Design_Session";
export {
writeCrossDesignChoice,
writeShoulderExtend,
} from "../../B06_Section/B06_Section_Cross_Design_Session";
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
export { writeState } from "../../A00_Common/b_page_state";
@@ -78,4 +78,24 @@ out.move = {
same: after.map((s) => byKey.get(s.key) === s),
empty: run(null, null, null, null),
};
// 441 — 초안 없는 저장 노선 = 저장 스냅 설정 따름 · 초안이 있으면 초안 값(스냅 case = cases[1])
const snapCase = job.cases[1];
const savedRun = (route, saved) =>
stage
.createStationsStage({ types: () => job.types, saved: () => saved })({
generation: 1,
range: null,
signal: new AbortController().signal,
inputs: { route, pipes: job.pipes, structures: job.structures },
out: { route: { line: snapCase.line, curves: snapCase.curves, chainage: [], lengthM: 0 } },
before: {},
prev: null,
})
.map((s) => s.chainageM);
out.savedSnap = {
saved: savedRun(null, { station_snap: true, station_snap_step: null }),
savedOff: savedRun(null, { station_snap: false }),
noPlan: savedRun(null, null),
draftWins: savedRun({ stationSnap: false }, { station_snap: true }),
};
process.stdout.write(JSON.stringify(out));
+4 -1
View File
@@ -61,7 +61,10 @@ const STUBS = {
fetchDetailPipePoints: async () => (calls.push("pipes"), { pipe_points: [] }),
},
"../A00_Common/design/design_pipe_ids": { pipeChainageOf: () => null },
"./B05_Profile_Api_Fetch": { clearRouteLatestCache: () => calls.push("latest:clear") },
"./B05_Profile_Api_Fetch": {
clearRouteLatestCache: () => calls.push("latest:clear"),
flushUphillOverrides: async () => calls.push("uphill"), // 옮긴 뒤 · 횡단 패치 앞(443-2)
},
"./B05_Profile_Api_Replan": {
replanRoute: async (_p, body) => {
bodies.push(JSON.parse(JSON.stringify(body)));
@@ -128,6 +128,82 @@ const drag = Object.fromEntries(
}),
);
// ④ 끌기 훑기(441) — 편집 창 끌기 차례(`_Pointer`) 그대로: 하한 막힘 · 미리보기 측점 밖이 누를 때보다 늘면
// 그 걸음 되돌림 · 놓을 때 정본 맞춤(`applyEdit`). 측점 No. · IP 셋 × 12 방향 × 3 거리 · 고정 R(곡선표 반경 못박음).
const edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts"));
const copy = (c) => ({
...c,
planned: c.planned.map(([x, y]) => [x, y]),
curveRadius: [...c.curveRadius],
});
function sweep(fixedR) {
const start = columns(fit.fitCurveStations(columns(ROUTE), LIMITS, 1));
if (fixedR)
start.curveRadius = edits
.rebuildLine(copy(start), LIMITS)
.nodes.map((n) => (n.radius_m === null ? null : Math.round(n.radius_m * 100) / 100));
const floor = (built) =>
edits.curveShortfalls(built.nodes, LIMITS.limitRadiusM, LIMITS.limitArcM);
const off = (built) => fit.offStationCount(built.vertices, built.curves, INTERVAL);
const tally = { drags: 0, released: 0, offAfter: 0, strayed: 0, worst: [] };
for (const K of [2, 3, 4])
for (let deg = 0; deg < 360; deg += 30)
for (const reach of [9, 17, 26]) {
const hist = copy(start);
let st = copy(hist);
let built = edits.rebuildLine(st, LIMITS);
const offBefore = off(built);
let raw = null;
let movedOnce = false;
const preview = () => {
raw = st.planned.map(([x, y]) => [x, y]);
st = { ...st, planned: fit.fitDrag(st, hist.planned, K, LIMITS) };
built = edits.rebuildLine(st, LIMITS);
};
const p0 = hist.planned[K];
for (let t = 0.5; t <= reach + 1e-9; t += 0.5) {
if (raw) raw.forEach((v, i) => (st.planned[i] = v));
built = edits.rebuildLine(st, LIMITS);
const before = floor(built);
const previous = st.planned[K];
st.planned[K] = [
p0[0] + Math.cos((deg * Math.PI) / 180) * t,
p0[1] + Math.sin((deg * Math.PI) / 180) * t,
];
built = edits.rebuildLine(st, LIMITS);
if (edits.shortfallCrossed(before, floor(built))) {
st.planned[K] = previous;
preview();
continue;
}
preview();
if (off(built) > offBefore) {
raw.forEach((v, i) => (st.planned[i] = v));
st.planned[K] = previous;
preview();
tally.strayed += 1;
continue;
}
movedOnce = true;
}
tally.drags += 1;
if (!movedOnce) continue;
built = edits.rebuildLine(st, LIMITS);
if (edits.floorStop(hist, st.planned, built.nodes, LIMITS)) continue;
const from = fit.fitStart(hist, st);
st = { ...st, planned: fit.fitCurveStations(st, LIMITS, from) };
built = edits.rebuildLine(st, LIMITS);
tally.released += 1;
const left = fit.offStationMcs(built.vertices, built.curves, INTERVAL);
if (left.length) {
tally.offAfter += 1;
if (tally.worst.length < 5) tally.worst.push({ K, deg, reach, mc: left });
}
}
return tally;
}
const swept = { auto: sweep(false), fixed: sweep(true) };
// ③ MC 측점 값 · 눈금 글
const input = JSON.parse(process.argv[2] || '{"grid":[],"spans":[]}');
const snapped = Object.fromEntries(
@@ -147,5 +223,12 @@ const labels = {
};
console.log(
JSON.stringify({ keys, drag, snapped, labels, scale: [0.1, 1, 10, null].map(frame.mcScale) }),
JSON.stringify({
keys,
drag,
swept,
snapped,
labels,
scale: [0.1, 1, 10, null].map(frame.mcScale),
}),
);
+5
View File
@@ -0,0 +1,5 @@
/* 443-2 시험 묶음 진입점 — 노선 바뀐 [저장] 때 상단측(uphill)을 측점 열쇠로 새 측점에 옮김.
* Node 헬퍼 = helper_443_2_uphill_carry.mjs · 짝 = test_443_2_uphill_carry.py.
*/
export { carryChainageStates, useStationKeyList } from "../../B06_Section/B06_Section_Station_Keys";
export { readState, writeState } from "../../A00_Common/b_page_state";
@@ -0,0 +1,61 @@
/* 443-2 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_443_2_entry.ts)을 부르고 결과를 JSON 한 벌로.
* node helper_443_2_uphill_carry.mjs <묶음.mjs>
*/
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 m = await import(pathToFileURL(process.argv[2]).href);
const P = "p443";
const grid = (pairs) => pairs.map(([key, chainageM]) => ({ key, chainageM }));
// 옛 노선 = 20 m 일반측점 넷 · 새 노선 = 같은 번호가 밀림(21.5 · 43) · grid:3(60 m) 사라짐
m.useStationKeyList(
P,
7,
grid([
["end:BP", 0],
["grid:1", 20],
["grid:2", 40],
["grid:3", 60],
]),
);
m.useStationKeyList(
P,
8,
grid([
["end:BP", 0],
["grid:1", 21.5],
["grid:2", 43],
]),
);
m.writeState("uphill", { "20.000": "right", "40.000": "left", "60.000": "right" }, P);
const outside = m.carryChainageStates(P, 7, 8);
const moved = m.readState("uphill", P);
// 세션이 없으면 그대로(빈 칸 안 만듦)
m.writeState("uphill", null, P);
const outsideEmpty = m.carryChainageStates(P, 7, 8);
process.stdout.write(
JSON.stringify({ outside, moved, outsideEmpty, empty: m.readState("uphill", P) }),
);
+110
View File
@@ -0,0 +1,110 @@
/* PLAN 443 단위검증 헬퍼 — 저장 누락 ① (H6 · M3 · M6).
* H6 구조물 저장 실패(409 · 그 밖) = 던짐 · 초안 남음 · 다음 [저장]이 줄에 안 막힘
* M3 상단측 차례 성공 = uphill 세션 비움 · 실패 = 그대로
* M6 확정 응답 failed_parts = 던짐(그 차례에서 멈춤)
* TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 서버 창구는 가짜. 짝 = `test_443_save_stops.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;
}
const settle = (promise) =>
promise.then(
() => "ok",
(error) => String(error.message),
);
async function run() {
const out = {};
// ── H6 구조물 ──
class StructureConflictError extends Error {}
let pending = [{ structure_id: "s1" }];
let mode = "conflict";
const puts = [];
const bridgeMod = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Page_Structures.ts"), {
"./B05_Profile_Api_Structures": {
StructureConflictError,
fetchStructures: async () => ({ revision: 3, structures: [] }),
saveStructures: async (_p, revision, list) => {
puts.push(revision);
if (mode === "conflict") throw new StructureConflictError("409");
if (mode === "boom") throw new Error("서버 500");
return { revision: revision + 1 };
},
readPendingStructures: () => pending,
writePendingStructures: (_p, next) => (pending = next),
definitionStale: () => false,
staleDefinitionNotice: () => null,
},
"@ui/ui_template_elements": { showToast: () => undefined },
});
const quiet = new Proxy({}, { get: () => () => undefined });
const bridge = bridgeMod.createStructuresBridge({
projectId: "p1",
panel: () => ({ structures: quiet, values: () => ({}) }),
profilePanel: () => quiet,
renderStationLines: () => undefined,
isRestoring: () => false,
});
out.conflict = await settle(bridge.saveStructuresIfDirty());
out.pendingAfterConflict = pending;
mode = "boom";
out.boom = await settle(bridge.saveStructuresIfDirty()); // 앞 실패에 줄이 안 막힘
out.pendingAfterBoom = pending;
mode = "ok";
out.ok = await settle(bridge.saveStructuresIfDirty());
out.pendingAfterOk = pending;
out.puts = puts;
// ── M3 · M6 상단측 · 확정 하위 ──
const state = new Map();
let reply = {};
const bodies = [];
globalThis.fetch = async (_url, init) => {
bodies.push(JSON.parse(init.body));
return { ok: true, json: async () => reply };
};
const api = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_Fetch.ts"), {
"../A00_Common/b_page_state": {
readState: (name, p) => state.get(`${name}:${p}`) ?? null,
writeState: (name, value, p) => state.set(`${name}:${p}`, value),
clearState: (name, p) => state.delete(`${name}:${p}`),
},
"@config/config_frontend": { API_BASE_URL: "", API_TIMEOUT_MS: 1000 },
});
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.bodies = bodies;
reply = { failed_parts: ["비정규 측점 횡단"] };
out.partsFailed = await settle(api.saveRouteParts("p1", { irregular_stations: [] }));
return out;
}
run().then(
(out) => process.stdout.write(JSON.stringify(out)),
(error) => {
process.stderr.write(String(error.stack ?? error));
process.exit(1);
},
);
+145
View File
@@ -0,0 +1,145 @@
/* 444 헬퍼 — 관 구간값 예약(culvertopt)이 관 옮김을 따라가고 · 못 맞춘 값은 버리고 · 두 번째 [저장]이 안 덮음.
* node helper_444_culvertopt.mjs <묶음.mjs>
* 묶음 = 444 진입점(`helper_444_entry.ts` · 346-2 묶음 + 내보내기 · vite ssr 진짜 모듈) · 표준출력 JSON 한 벌.
* 짝 = test_444_culvertopt_single_draft.py · 틀 = helper_415_swap.mjs(415-3).
*/
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);
globalThis.__toasts = [];
/* 관 정본(서버) — GET 은 지금 관 · PUT 은 받은 몸통으로 바꿈 */
let saved = [];
const puts = [];
globalThis.fetch = async (url, init = {}) => {
if (String(url).includes("/drainage/pipe-points") && init.method === "PUT") {
const points = JSON.parse(init.body).points;
puts.push(points);
saved = points.map((point) => ({ ...point, lonlat: [0, 0] }));
}
return { ok: true, status: 200, json: async () => ({ pipe_points: saved, basins: [] }) };
};
const m = await import(pathToFileURL(process.argv[2]).href);
const P = "p444";
const R = 3;
const out = {};
const section = (chainage_m, kind, extra = {}) => ({
station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`,
chainage_m,
label: "",
kind,
center_z: 100,
azimuth_deg: 90,
center_x: chainage_m,
center_y: 0,
frame: { left_xy: [0, 1] },
samples: [{ offset_m: 0, elevation_m: 100, valid: true }],
design: { design_elevation_m: 101 },
...extra,
});
const detail = () => ({
longitudinal: {
length_m: 100,
stations: [],
profile_alignment: { policy: { station_interval_m: 20 } },
},
cross_sections: [
section(0, "bp"),
section(20, "regular"),
section(40, "regular"),
section(45.054, "irregular", { structure: "세월교", culvert: {} }),
section(60, "regular"),
section(80, "regular"),
section(100, "ep"),
],
});
const station = (key, chainageM, kind, id = null) => ({
key,
chainageM,
x: chainageM,
y: 0,
azimuthDeg: 90,
left: [0, 1],
kind,
id,
ground: { centerZ: 99, left: [{ offsetM: 0.5, z: 99.1 }], right: [{ offsetM: 0.5, z: 98.9 }] },
planRadiusM: null,
curveOuterSide: null,
curveWideningM: null,
});
const list = (pipeAt) =>
[
station("end:BP", 0, "end"),
station("grid:1", 20, "grid"),
station("grid:2", 40, "grid"),
station("pipe:pipe-s1", pipeAt, "pipe", "pipe-s1"),
station("grid:3", 60, "grid"),
station("grid:4", 80, "grid"),
station("end:EP", 100, "end"),
].sort((a, b) => a.chainageM - b.chainageM);
const key = `aislo:draft:culvertopt:${P}:${R}`;
/* 1. 관 5 m 옮김 — 옛 세션 구간값 예약도 관 새 자리로(조정창 맵 없이 · 415-3 과 같은 길) */
m.writeState("section-detail", detail(), P, R);
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 45.054 }] }, P);
await m.loadSectionDetail(P, R);
m.writeState("culvertopt", { 45.05: { ford_width_m: 4 } }, P, R);
const stop = m.followSwaps(
P,
() => R,
{ visible: () => true },
async () => undefined,
);
m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 50 }] }, P);
m.useDesignStore(P, null).set("stations", list(50));
await new Promise((resolve) => setTimeout(resolve, 30));
out.moved = m.readState("culvertopt", P, R);
stop();
/* 2. [저장] 두 번 — 첫 번째는 관 새 자리에 얹고 세션 비움 · 두 번째는 안 보냄(값 그대로) */
saved = [
{ id: "p5000", chainage_m: 50, source: "stream", facility: "ford", options: { ford_width_m: 3 } },
];
await m.flushCulvertOptions(P, key);
out.firstSave = {
puts: puts.length,
width: saved[0].options.ford_width_m,
left: sessionStorage.getItem(key),
};
await m.flushCulvertOptions(P, key);
out.secondSave = { puts: puts.length, width: saved[0].options.ford_width_m };
/* 3. 못 맞춘 값(지운 · 옮긴 관 옛 자리) — 다른 관에 안 얹고 버림 */
m.writeState("culvertopt", { "30.00": { ford_width_m: 9 } }, P, R);
await m.flushCulvertOptions(P, key);
out.unmatched = {
puts: puts.length,
width: saved[0].options.ford_width_m,
left: sessionStorage.getItem(key),
};
process.stdout.write(JSON.stringify(out));
+3
View File
@@ -0,0 +1,3 @@
/* 444 시험 묶음 진입점 — 346-2 갈아끼움 묶음 + 관 구간값 예약 내보내기(진짜 모듈 · vite ssr). */
export * from "./helper_346_2_swap_entry";
export { flushCulvertOptions } from "../../B06_Section/B06_Section_Api_Culvert_Options";
@@ -0,0 +1,57 @@
/* 카드 폭 넘김 다시 만들기 확인창(PLAN 446) 단위검증 헬퍼 — `createSampleWidener` 를 트랜스파일해 돌린다.
* 취소 = 재생성 없음 · 오버레이 셈 그대로 · 확인 = 재생성 한 번 · 오버레이 올림 · 내림 짝. 결과는 JSON 한 벌.
* 짝 = `test_446_widen_carry.py`.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const calls = { regenerate: [], overlay: 0, asked: [] };
let answer = false;
const STUBS = {
"@ui/ui_template_locale": {
currentLanguageIndex: 0,
ui_locales: new Proxy({}, { get: (_t, key) => [`${String(key)} {width}/{sampled}`] }),
},
"@ui/ui_template_elements": {
showLoadingOverlay: () => calls.overlay++,
hideLoadingOverlay: () => calls.overlay--,
showToast: () => {},
showConfirmDialog: async (message) => {
calls.asked.push(message);
return answer;
},
},
"./B06_Section_Api_Fetch": {
regenerateSections: async (_p, _r, width) => calls.regenerate.push(width),
},
"./B06_Section_Section_Store": { loadSectionDetail: async () => ({ cross_sections: [] }) },
};
const file = path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Common.ts");
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]);
(async () => {
const widen = box.exports.createSampleWidener({
target: () => ({ projectId: "p", routeId: 1 }),
sampledHalfWidth: () => 20,
applyDetail: () => {},
});
const out = {};
out.within = await widen(15); // 표본 안 = 묻지 않음
out.withinAsked = calls.asked.length;
out.cancel = await widen(21);
out.cancelCalls = calls.regenerate.length;
out.cancelOverlay = calls.overlay;
answer = true;
out.ok = await widen(21);
out.okCalls = [...calls.regenerate];
out.okOverlay = calls.overlay;
out.asked = calls.asked;
process.stdout.write(JSON.stringify(out));
})();
@@ -4,6 +4,7 @@
- 저장 노선 cross 차례(`refreshSavedCross`) = 카드 자료(상세 설계)를 **늘** 다시 셈 —
복합경사 · 측구 · 지반 단추마다 그 측점 설계선 · 면적이 바뀜.
- 묶음 측점 = 상세 측점이면 돌려줌 · 1 mm 넘게 다르면 null(묶음 엔진) — 그래도 카드 자료는 고침.
- 사면 이동 한 걸음도 같음 · 걸음마다 받는 쪽(카드 · 종단 · 유토 다시 그림) 한 번(441 · 요구 268-2 · 4).
- 서버 폴백(선형 저장분 없음)에도 카드 선택이 실림(열쇠 = 측점 자리) · 서버는 저장분 위에 얹음.
짝: `helper_266_card_toggle.mjs` · 진입점 `helper_266_card_toggle_entry.ts` ·
지형 · 노선 틀 = `test_346_2_pipeline_cross_page.py`.
@@ -99,7 +100,8 @@ def _check_toggles(case: dict, returned) -> None:
steps = {step["tag"]: step for step in case["steps"]}
assert steps["open"]["cut"] > 0.5 # 깎기 측점
before = steps["open"]
for tag in ("two_stage", "ditch_off", "soil"):
assert all(step["calls"] == 1 for step in case["steps"]) # 걸음마다 다시 그림 한 번(441)
for tag in ("two_stage", "ditch_off", "soil", "slope"):
step = steps[tag]
assert case["target"] in step["updated"], tag # 카드 다시 그림 대상
assert step["line"] != before["line"], tag # 카드 설계선 바뀜
@@ -264,6 +264,17 @@ def test_스냅_측점은_원래_격자_번호(r):
assert moved and all(abs(a - b) <= 10 for a, b in moved.values())
def test_초안_없으면_저장_스냅_따름(r):
"""441 — 초안 없는 저장 노선 측점 단계 = 저장 station_snap · 초안이 있으면 초안 값."""
snapped = [s["chainageM"] for s in r["cases"][1]]
plain = [s["chainageM"] for s in r["cases"][0]]
assert snapped != plain
got = r["savedSnap"]
assert got["saved"] == snapped
assert got["savedOff"] == plain and got["noPlan"] == plain
assert got["draftWins"] == plain
def test_노선_한_점_옮김_앞_구간_열쇠_그대로(r):
m = r["move"]
from_m = _first_changed_m()
@@ -82,6 +82,7 @@ def test_초안_넘김_차례(out):
"pipes", # 관 열쇠 → 새 자리(서버가 옮긴 관)
"carry:7->9:quiet",
"carryAt:7->9", # 417 — 조절 · 측점 폭 · 상세 표도 새 노선 측점으로
"uphill", # 443-2 — 상단측도 새 측점으로 옮긴 뒤 · 횡단 패치 앞
"adjusts:9",
"edits:9", # 옮긴 고침을 새 상세 설계 칸에 → 같은 [저장] 횡단 패치
"crossSave:9",
+3 -1
View File
@@ -78,7 +78,9 @@ def test_중심점_좌우_없음_위아래만(ts):
def test_배선_걷음():
order = (B06 / "B06_Section_UI_Cross_Pick_Order.ts").read_text(encoding="utf-8")
assert "layer.append(...surfaces, ...edges, ...lines, ...centers);" in order
assert (
"layer.append(...rocks, ...surfaces, ...edges, ...lines, ...centers);" in order
) # 암 = 판정면 아래(440)
for gone in (
ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Shift.ts",
ROOT / "B05_Profile" / "logic" / "B05_Profile_RouteShift.ts",
+21 -2
View File
@@ -80,7 +80,17 @@ def test_끄는_동안_MC_가_간격_배수(got, step) -> None:
drag = got["drag"][step]
assert drag["off"] == 0
assert max(drag["residues"]) <= drag["tol"]
assert drag["dragMoved"] <= float(step) / 2 + 1e-6 # 끈 IP 는 접선 따라 ±간격/2 안만
assert drag["dragMoved"] <= float(step) + 1e-6 # 끈 IP 는 접선 따라 ±간격 안만(441)
@pytest.mark.parametrize("mode", ["auto", "fixed"])
def test_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
"""441 — 측점 No. 끌기(IP 셋 × 12 방향 × 3 거리) · 놓은 뒤 MC 가 다 측점 위 · 맞춤이 늘 닿음
(반 간격 한도면 18+9 ~ 19+1 에서 못 맞춰 막힘 걸음이 생김)."""
swept = got["swept"][mode]
assert swept["drags"] == 108 and swept["released"] > 0
assert swept["offAfter"] == 0, swept["worst"]
assert swept["strayed"] == 0
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
@@ -101,6 +111,12 @@ def test_끌기_방향키_편집기_이음() -> None:
# 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림)
assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex))" in pointer
assert "now.raw.forEach((vertex, index) => (state.planned[index] = vertex))" in keys
# 미리보기 측점 밖이 누를 때보다 늘면 그 걸음 되돌림 · 막힘 검사 뒤에 옮김 표시(441 · 433-14)
assert "params.offStation() > dragOff" in pointer and "params.offStation() > now.off" in keys
move = pointer[pointer.index("const previous = state.planned[dragNode];") :]
assert move.index("shortfallCrossed(") < move.index("params.offStation() > dragOff")
assert move.index("params.offStation() > dragOff") < move.index("dragMoved = true;")
assert "offStation: () => (stationSnap ? fit.offStationCount(" in edit
assert edit.count("\n") <= 695
@@ -193,6 +209,9 @@ def test_서버_측점_셈이_저장_간격을_씀() -> None:
page = (design / "design_pipeline_page.ts").read_text(encoding="utf-8")
assert "stationSnapStep: routePlan.station_snap_step" in page
stage = (design / "logic" / "design_pipeline_stations.ts").read_text(encoding="utf-8")
assert "stationSnapStep: ctx.inputs.route?.stationSnapStep" in stage
assert "...snapOptions(ctx.inputs.route, saved?.())" in stage # 초안 · 없으면 저장 노선(441)
assert "stationSnapStep: draft.stationSnapStep" in stage
assert "stationSnapStep: saved?.station_snap_step" in stage
assert "saved: () => plan" in page and "...snapOptions(inputs.route, plan)" in page
apply = (B05 / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
assert "stationSnap: true, stationSnapStep: null" in apply # 추천 = 측점 No.
@@ -4,6 +4,7 @@
(갈아끼운 캐시)에 모든 카드 번호가 있음 · 찾기 넷(카드 고름 · 목록 → 카드 · 넘김 · 풀기)은 지금 그린 상세.
② 평면 패널 빈 곳 누름 = 평면 마커가 고른 것만 풂(`test_409` 의 deselectOther · deselectOwn).
③ 바뀜 신호에서 B06 목록도 자리가 바뀌었으면 다시 실음.
④ 암 경계선 판정(굵은 투명 선)은 구조물 판정면 아래 — 구조물 아랫부분 누름이 구조물을 고름.
TS 셈은 `helper_440_draft_card_ids.cjs`.
./venv/Scripts/python.exe -m pytest resources/tester/test_440_cross_draft_pick.py -q
@@ -77,3 +78,12 @@ def test_평면_빈_곳_누름은_마커_고름만() -> None:
marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts")
assert "deselect: () => planMarks(canvas)?.picked === ownPick && deselect()," in marks
assert "ownPick = mark.id;" in marks
def test_암_경계_판정은_구조물_판정면_아래() -> None:
order = _read("B06_Section", "B06_Section_UI_Cross_Pick_Order.ts")
assert "layer.append(...rocks, ...surfaces, ...edges, ...lines, ...centers);" in order
lines = order[order.index("const lines = kids.filter(") :]
assert "!el.classList.contains(ROCK_HIT_CLASS)" in lines[: lines.index(");")]
pick = _read("B06_Section", "B06_Section_UI_Cross_Design_Pick.ts")
assert 'export const ROCK_HIT_CLASS = "b06-chart__rock-hit";' in pick
@@ -0,0 +1,77 @@
"""PLAN 443-2 — 노선 바꾼 [저장]의 상단측(uphill).
- 옛 누가거리 열쇠 상단측 = 측점 열쇠(옮김 목록 `ROUTE_CARRY_STATES` · 415 · 417 규칙)로 새 노선 측점에.
- 새 노선에 같은 측점이 없으면 뺌 · 수를 돌려줌(「노선 밖 고침」 · 상단측 알림).
- 저장 한 줄기: 노선을 저장했으면 상단측 차례는 건너뛰고 `carryAfterRouteSave` 가 옮긴 뒤 · 횡단 패치 앞에 보냄.
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
"""
import json
import subprocess
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
@pytest.fixture(scope="module")
def out(tmp_path_factory):
work = tmp_path_factory.mktemp("b443_2")
build = subprocess.run(
[
"node",
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
"build",
"--configLoader",
"runner",
"--ssr",
"../resources/tester/helper_443_2_entry.ts",
"--outDir",
str(work),
"--logLevel",
"error",
],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=300,
)
assert build.returncode == 0, build.stderr
bundle = next(work.glob("*.js"), None) or next(work.glob("*.mjs"))
proc = subprocess.run(
["node", str(HERE / "helper_443_2_uphill_carry.mjs"), str(bundle)],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_상단측이_새_노선_같은_측점으로(out):
assert out["moved"] == {"21.50": "right", "43.00": "left"}
def test_못_맞춘_상단측은_빼고_수를_돌려줌(out):
assert out["outside"] == 1
def test_세션_없으면_그대로(out):
assert out["outsideEmpty"] == 0 and out["empty"] is None
def test_노선_저장이면_상단측_차례는_옮긴_뒤():
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
assert 'if (step === "uphill" && routeSaved) continue;' 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 # 못 맞춤 알림
+201
View File
@@ -0,0 +1,201 @@
"""PLAN 443 — 저장 누락 ① 구조물 실패 · 상단측 재전송 · 확정 하위 실패(H6 · M3 · M6).
- H6 구조물 저장 실패(409 · 그 밖) = 던짐(저장 한 줄기가 구조물 차례에서 멈춤) · 초안 남음 · 줄 안 막힘.
- M3 상단측 차례 성공 = uphill 세션 비움(다음 [저장]이 다시 안 보냄) · 실패 = 세션 그대로.
- M6 B05 확정 하위(비정규 측점 횡단 · 상단측 병합) 실패 = 응답 failed_parts · 화면이 던짐.
Node 헬퍼 = `helper_443_save_stops.cjs`.
./venv/Scripts/python.exe -m pytest resources/tester/test_443_save_stops.py -q
"""
import asyncio
import json
import subprocess
from pathlib import Path
from uuid import uuid4
import pytest
from B05_Profile import B05_Profile_Router_Lifecycle as lifecycle
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest
HERE = Path(__file__).parent
ROOT = HERE.parents[1]
@pytest.fixture(scope="module")
def out():
proc = subprocess.run(
["node", str(HERE / "helper_443_save_stops.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
# ── H6 ───────────────────────────────────────────────────────────────────────
def test_구조물_409_던짐_초안_남음(out):
assert "먼저 저장" in out["conflict"]
assert out["pendingAfterConflict"] == [{"structure_id": "s1"}]
def test_구조물_실패_뒤_줄이_안_막힘(out):
assert out["boom"] == "서버 500" and out["pendingAfterBoom"] == [{"structure_id": "s1"}]
assert out["ok"] == "ok" and out["pendingAfterOk"] is None
assert out["puts"] == [3, 3, 3] # 세 번 다 나감
def test_구조물_차례가_토스트로_삼키지_않음():
page = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Structures.ts").read_text("utf-8")
body = page.split("async function persistStructures(")[1].split("\n }\n")[0]
assert "showToast(error" not in body and "return;\n" not in body.split("catch (error)")[1]
# ── M3 · M6 화면 ─────────────────────────────────────────────────────────────
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 # 실패 · 성공 · (빈 세션은 안 보냄) · 확정 하위 하나
def test_확정_하위_실패면_던짐(out):
assert out["partsFailed"] == "비정규 측점 횡단 실패"
def test_종단_보기_상단측_차례도_같은_창구():
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
assert "await saveRouteParts(projectId, {" in actions
assert "loadUphillOverrides(projectId)" in actions # 세션에서 읽음(메모리 옛 값 안 보냄)
assert "confirmRoute(" not in actions
# ── M6 서버 ──────────────────────────────────────────────────────────────────
class _Cursor:
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
class _Connection:
def __init__(self):
self.committed = False
async def begin(self):
pass
async def commit(self):
self.committed = True
async def rollback(self):
pass
def cursor(self):
return _Cursor()
class _Pool:
def __init__(self, connection):
self.connection = connection
def acquire(self):
connection = self.connection
class _Ctx:
async def __aenter__(self):
return connection
async def __aexit__(self, *exc):
return False
return _Ctx()
def _patch(monkeypatch, connection, *, append, longitudinal, merge):
async def latest(*_a):
return {"id": 7, "status": "DRAFT"}
async def noop(*_a, **_k):
return None
async def storage(*_a):
return "p"
async def get_long(*_a):
return longitudinal
monkeypatch.setattr(lifecycle, "get_db_pool", lambda: _Pool(connection))
monkeypatch.setattr(lifecycle, "get_latest_route", latest)
monkeypatch.setattr(lifecycle, "confirm_route", noop)
monkeypatch.setattr(lifecycle, "complete_stage", noop)
monkeypatch.setattr(lifecycle, "_append_irregular_cross_sections", append)
monkeypatch.setattr(lifecycle, "get_project_storage_relative_path", storage)
monkeypatch.setattr(lifecycle, "get_longitudinal_section", get_long)
monkeypatch.setattr(lifecycle, "resolve_stored_project_path", lambda p: p)
monkeypatch.setattr(lifecycle, "_merge_uphill_overrides_into_longitudinal", merge)
monkeypatch.setattr(lifecycle, "sync_uphill_overrides_into_designs", noop)
def _boom(*_a, **_k):
raise RuntimeError("boom")
async def _aboom(*_a, **_k):
raise RuntimeError("boom")
_REQUEST = RouteConfirmRequest(
filter_key="csf",
method="tin",
surface_model_id=1,
uphill_overrides=[{"chainage_m": 40.0, "side": "right"}],
)
def test_확정_하위_실패를_응답에_실음(monkeypatch):
connection = _Connection()
_patch(
monkeypatch,
connection,
append=_aboom,
longitudinal={"longitudinal_file_path": "l.json"},
merge=_boom,
)
result = asyncio.run(lifecycle.confirm_latest_route(uuid4(), _REQUEST, False))
assert result.failed_parts == ["비정규 측점 횡단", "상단측 병합"]
assert connection.committed # 확정 자체는 진행(비치명)
def test_종단_없으면_상단측_병합_실패(monkeypatch):
async def ok(*_a, **_k):
return None
_patch(monkeypatch, _Connection(), append=ok, longitudinal=None, merge=lambda *a: None)
result = asyncio.run(lifecycle.confirm_latest_route(uuid4(), _REQUEST, False))
assert result.failed_parts == ["상단측 병합(종단 없음)"]
def test_다_되면_빈_목록(monkeypatch):
async def ok(*_a, **_k):
return None
_patch(
monkeypatch,
_Connection(),
append=ok,
longitudinal={"longitudinal_file_path": "l.json"},
merge=lambda *a: None,
)
result = asyncio.run(lifecycle.confirm_latest_route(uuid4(), _REQUEST, False))
assert result.failed_parts == []
@@ -0,0 +1,88 @@
"""444 관 구간값(H4a · H4b) — 세월교 · BOX 폼 값은 관 초안 한 벌 · 옛 예약은 관을 따라가고 못 맞추면 버림.
- 폼이 낸 세월교 · BOX 값(관경 · 수량 · 월류 폭 · 날개벽)은 관 초안(가게 pipes)으로만 나감 — 조정 예약
(culvertopt)에 또 담지 않음(test_b06_adjust_single_form 규칙 · 관 id 로 옮김 · 노선 바뀜을 따라감).
- 옛 세션 예약(culvertopt)은 관 옮김(측점 갈아끼움)을 따라 새 자리로 · [저장] 때 못 맞춘 값은 버림(지운 관
자리에 온 다른 관에 안 얹힘) · 두 번째 [저장]은 다시 안 보냄(값 그대로).
짝: `helper_444_culvertopt.mjs`(444 진입점 묶음 · 진짜 상세 가게 · 내보내기).
"""
import json
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
if str(HERE) not in sys.path:
sys.path.insert(0, str(HERE))
import test_365_page_pipeline as t365 # noqa: E402
pytestmark = t365.pytestmark
@pytest.fixture(scope="module")
def out(tmp_path_factory) -> dict:
work = tmp_path_factory.mktemp("b444")
build = subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
"build",
"--configLoader",
"runner",
"--ssr",
"../resources/tester/helper_444_entry.ts",
"--outDir",
str(work),
"--logLevel",
"error",
],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=300,
)
assert build.returncode == 0, build.stderr
bundle = work / "bundle.mjs"
shutil.copy(work / "helper_444_entry.js", bundle)
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_444_culvertopt.mjs"), str(bundle)],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_세월교_BOX_폼_값은_조정_예약에_안_쌓음() -> None:
ford = (ROOT / "B06_Section" / "B06_Section_UI_Page_Ford_Controls.ts").read_text(
encoding="utf-8"
)
controls = (ROOT / "B06_Section" / "B06_Section_UI_Page_Station_Controls.ts").read_text(
encoding="utf-8"
)
assert "queuePipeOptions" not in ford
assert "queuePipeOptions" not in controls
def test_관_옮김_예약도_새_자리로(out):
assert out["moved"] == {"50.00": {"ford_width_m": 4}}
def test_두_번째_저장은_값_그대로(out):
assert out["firstSave"] == {"puts": 1, "width": 4, "left": None}
assert out["secondSave"] == {"puts": 1, "width": 4}
def test_못_맞춘_값은_다른_관에_안_얹고_버림(out):
assert out["unmatched"] == {"puts": 1, "width": 4, "left": None}
+106
View File
@@ -0,0 +1,106 @@
"""446 카드 폭 넘김 다시 만들기 — 표본 폭을 넘겨 종 · 횡단을 다시 만들어도 사용자 값 그대로.
- 횡단 행: 측점 열쇠(0.01 m 안)로 옛 data(설계 · 메모 · CAD · 상세 표 · 조절) · 상태 · 새 요약 이김
- 종단 행: 생성 요약 · 생성 옵션은 새것 · 표준 횡단면 · 유토(풍선) · 계획 입력은 옛것
- 화면: 다시 만들기 전 확인창 · 취소 = 재생성 없음(`helper_446_widen_confirm.cjs`)
"""
import json
import subprocess
from pathlib import Path
import pytest
from B06_Section.B06_Section_Router_Widen import carried_cross_rows, carried_longitudinal_data
HERE = Path(__file__).parent
ROOT = HERE.parents[1]
DESIGN = {
"ground_type": "soil",
"section_mode": "right_cut",
"structure_note": "메모",
"structure_note_cad": {"lines": [1]},
"detail_table": {"rows": 2},
"display_half_width_m": 21,
"basin_adjust": {"d": 0.3},
}
def _old(row_id: int, at: float, design: dict | None = None, status: str = "CONFIRMED") -> dict:
data = {"sample_count": 41, "center_z": 100.0, "design": design or DESIGN}
return {"id": row_id, "chainage_m": at, "data": json.dumps(data), "status": status}
def _new(row_id: int, at: float) -> dict:
return {"id": row_id, "chainage_m": at, "data": json.dumps({"sample_count": 43})}
def test_횡단_행은_측점_열쇠로_설계_상태_그대로() -> None:
old = [_old(1, 0.0), _old(2, 20.0), _old(3, 33.456)]
new = [_new(11, 0.0), _new(12, 20.0), _new(13, 33.46), _new(14, 40.0)]
updates, dropped = carried_cross_rows(old, new)
assert dropped == 0
by_id = {row_id: (json.loads(data), status) for data, status, row_id in updates}
assert set(by_id) == {11, 12, 13} # 40 m = 새 측점 · 옛 값 없음
data, status = by_id[13] # 0.004 m 어긋남도 같은 측점
assert data["design"] == DESIGN and status == "CONFIRMED"
assert data["sample_count"] == 43 and data["center_z"] == 100.0 # 새 요약이 이김 · 옛 칸 남김
def test_같은_측점_둘이면_최신_행_못_옮긴_행은_셈() -> None:
newer = {**DESIGN, "structure_note": "새 메모"}
old = [_old(1, 20.0), _old(5, 20.0, newer, "DRAFT"), _old(6, 55.0)]
updates, dropped = carried_cross_rows(old, [_new(12, 20.0)])
data, status, _ = updates[0]
assert json.loads(data)["design"]["structure_note"] == "새 메모" and status == "DRAFT"
assert dropped == 1 # 55 m — 새 측점에 없음(로그로 알림)
def test_종단_data_는_사용자_키를_옛것으로() -> None:
old = {
"length_m": 500.0,
"options": {"cross_half_width_m": 20, "standard_cross_section": {"road_width_m": 3}},
"grade_overrides": {"max_grade_pct": 12},
"grade_summary": {"old": 1},
"mass_haul": {"balloon_offsets": {"0": [3.0, -4.0]}},
}
new = {
"length_m": 500.0,
"options": {"cross_half_width_m": 21, "station_interval_m": 20},
"grade_overrides": {},
"grade_summary": {"new": 1},
}
merged = carried_longitudinal_data(old, new)
assert merged["options"] == {
"cross_half_width_m": 21,
"station_interval_m": 20,
"standard_cross_section": {"road_width_m": 3},
}
assert merged["grade_overrides"] == {"max_grade_pct": 12}
assert merged["mass_haul"] == {"balloon_offsets": {"0": [3.0, -4.0]}}
assert merged["grade_summary"] == {"new": 1} # 계획고 옮김이 다시 씀
def test_반폭_요청은_옮겨_담는_길로() -> None:
router = (ROOT / "B06_Section" / "B06_Section_Router.py").read_text(encoding="utf-8")
at = router.index("async def regenerate_sections(")
body = router[at : router.index("async def regenerate_route_sections(", at)]
assert "widen_route_sections(" in body and "regenerate_route_sections(" not in body
assert "await carry(connection)" in router # 같은 트랜잭션 안
@pytest.mark.skipif(not (ROOT / "config" / "node_modules").is_dir(), reason="의존성 미설치")
def test_다시_만들기_전_확인창_취소면_재생성_없음() -> None:
done = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_446_widen_confirm.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
)
assert done.returncode == 0, done.stderr
out = json.loads(done.stdout)
assert out["within"] is True and out["withinAsked"] == 0
assert out["cancel"] is False and out["cancelCalls"] == 0 and out["cancelOverlay"] == 0
assert out["ok"] is True and out["okCalls"] == [21] and out["okOverlay"] == 0
assert out["asked"][0].endswith("21/20") # 요청 폭 · 표본 폭이 글에 들어감
+1 -2
View File
@@ -102,8 +102,7 @@ def test_노선이_바뀌면_노선_먼저_뒤_차례는_새_노선(out):
"plusStations",
'alignment:9:{"30.000":0.2}',
"culvert:aislo:draft:culvertopt:p1:7",
"uphill",
"carry:7->9",
"carry:7->9", # 상단측은 넘김(carryAfterRouteSave)이 새 측점으로 옮긴 뒤 보냄(443-2)
"summary:9:2",
"toast:info",
"done:false",