fix(467 덧 고름 되돌림): 횡단 다시 셈 입력 물러섬 = 저장 정본 설계 · 고름 단추가 메모리 상세를 안 덮음

- Cross_Refresh refreshLocally — 세션 고름(단추 · 노견 확장 · 소단 깊이 · 암 경계 · 암 경사) ?? 저장 정본(savedDesigns) · 정본에 없는 측점만 메모리 설계 · 소단 깊이 · 소단 제원은 이번 원천 그대로(↶ 뒤 앞 값이 안 남음)
- Design_Sync handleDesignChange — target.design 덮기 셋 뺌(세션만 · 카드는 다시 셈 알림이 그림)
- 새 시험 test_272_choice_undo(옛 코드 = 넷 다 ↶ 뒤 앞 고름에 머묾)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 07:32:17 +09:00
co-authored by Claude Opus 5.5
parent 9f05c9ffcb
commit 57db7a0608
5 changed files with 280 additions and 25 deletions
+134
View File
@@ -0,0 +1,134 @@
/* 467 덧 헬퍼 — 횡단 고름(단추 · 노견 확장 · 소단 깊이 · 암 경계)을 세션에 쓰고 다시 셈 → 세션 칸을 지움(↶) →
* 다시 셈 = 고르기 전(정본) 설계와 같은가 · 다시 씀(↷) = 고른 값과 같은가.
* node helper_272_choice_undo.mjs <묶음.mjs> <job.json>
* job = { standard, rockDefault, ground } · 짝 = `test_272_choice_undo.py` · 표준출력 JSON 한 벌.
*/
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
const memory = () => {
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 = memory();
globalThis.localStorage = memory();
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.fetch = async () => new Response("{}", { status: 404 });
const m = await import(pathToFileURL(process.argv[2]).href);
const job = JSON.parse(readFileSync(process.argv[3], "utf8"));
const P = "p272undo";
const R = 6;
const AT = 40;
m.writeState("std-cross", job.standard, P);
m.rememberRockBoundaryDefault(P, job.rockDefault);
// 소단 사본 = 측점을 덮는 구간(소단 깊이가 먹게)
m.writeState(
"berm",
[{ start_m: 0, end_m: 100, width_m: 1, interval_m: 3, slope_deg: 0, depth_m: 1 }],
P,
R,
);
const canonDetail = () => ({
longitudinal: { length_m: 100, stations: [], profile_alignment: { policy: {} } },
cross_sections: [
{
station_id: "station_000000040000",
chainage_m: AT,
label: "",
kind: "regular",
center_z: 100,
azimuth_deg: 90,
center_x: AT,
center_y: 0,
frame: { left_xy: [0, 1] },
uphill_side: "left",
samples: job.ground,
design: {
design_elevation_m: 100,
ground_type: "soil",
section_mode: "left_cut",
ditch_type: "standard",
ditch_side: "left",
ditch_choice: null,
two_stage_slope: false,
},
},
],
});
m.writeState("section-detail", canonDetail(), P, R); // 저장 정본(세션 상세)
const alignment = {
stations: [{ chainage_m: AT, plan_elevation_m: 100.4 }],
samples: [
{ chainage_m: 0, elevation_m: 100.4 },
{ chainage_m: 100000, elevation_m: 100.4 },
],
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
};
const detail = canonDetail(); // 메모리 상세(가게가 쥔 객체)
const refresh = async (target = detail) => {
await m.refreshCrossDesigns({
projectId: P,
routeId: R,
detail: target,
edits: { station_offsets: {}, curve_radii: {} },
alignment,
});
return JSON.stringify(target.cross_sections[0].design);
};
const clear = (name) => sessionStorage.removeItem(m.stateKey(name, P, R));
const pristine = await refresh(canonDetail()); // 고르기 전 = 정본으로 셈
await refresh(); // 메모리 상세 한 번(측점 열쇠 세움)
const out = { pristineSame: (await refresh()) === pristine, cases: {} };
const CASES = {
choice: [
() =>
m.writeCrossDesignChoice(P, R, AT, {
ground_type: "blasting_rock",
section_mode: "right_cut",
ditch_side: "right",
ditch_type: "standard",
paved: false,
two_stage_slope: true,
ditch_choice: false,
}),
"crossdesign",
],
shoulder: [() => m.writeShoulderExtend(P, R, AT, "left", 1.2), "shoulderext"],
bermDepth: [() => m.writeBermDepth(P, R, AT, "left", 2.5), "bermdepth"],
rock: [() => m.writeKeyed("rockb", P, R, new Map([["40.00", 3.5]]), ["40.00"]), "rockb"],
};
for (const [name, [write, state]] of Object.entries(CASES)) {
write();
const picked = await refresh();
clear(state); // ↶ = 세션 칸 지움
const undone = await refresh();
write(); // ↷ = 다시 고름
const redone = await refresh();
clear(state);
const again = await refresh();
out.cases[name] = {
changed: picked !== pristine,
undoIsCanon: undone === pristine,
redoIsPicked: redone === picked,
undoAgain: again === pristine,
};
}
process.stdout.write(JSON.stringify(out));
@@ -0,0 +1,10 @@
/* 467 덧 시험 묶음 진입점 — 횡단 고름 ↶ = 정본(진짜 모듈 · vite ssr). */
export { refreshCrossDesigns } from "../../B06_Section/B06_Section_Cross_Refresh";
export {
writeBermDepth,
writeCrossDesignChoice,
writeShoulderExtend,
} from "../../B06_Section/B06_Section_Cross_Design_Session";
export { writeKeyed } from "../../B06_Section/B06_Section_Station_Keys";
export { rememberRockBoundaryDefault } from "../../B06_Section/B06_Section_UI_Standard_Panel";
export { stateKey, writeState } from "../../A00_Common/b_page_state";
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""467 덧 (요구 272) — 횡단 고름을 되돌리면(↶) 다시 셈이 정본 값으로 돌아옴.
못박는 것
- 카드 단추(지반 · 단면 갈래 · 측구) · 노견 확장 · 소단 깊이 · 암 경계를 세션에 쓰고 다시 셈 → 세션 칸 지움(↶) →
설계 = 고르기 전(정본으로 셈) 설계와 글자까지 같음 · 다시 씀(↷) = 고른 값과 같음.
- 다시 셈 입력 물러섬 = 저장 정본 설계(메모리 설계에 남은 앞 고름 · 결과 아님).
- 고름 단추가 메모리 상세 설계를 덮지 않음(`handleDesignChange`).
Node 헬퍼 `helper_272_choice_undo.mjs` 가 vite ssr 로 묶은 진짜 모듈을 부름.
"""
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(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from config.config_system import ( # noqa: E402
STANDARD_CROSS_SECTION,
STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
)
GROUND = [
{"offset_m": o / 2, "elevation_m": round(100.0 + 0.35 * o / 2, 4), "valid": True}
for o in range(-24, 25)
]
@pytest.fixture(scope="module")
def out(tmp_path_factory):
work = tmp_path_factory.mktemp("b272undo")
build = subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
"build",
"--configLoader",
"runner",
"--ssr",
"../resources/tester/helper_272_choice_undo_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_272_choice_undo_entry.js", bundle)
job = work / "job.json"
job.write_text(
json.dumps(
{
"standard": STANDARD_CROSS_SECTION,
"rockDefault": STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
"ground": GROUND,
}
),
encoding="utf-8",
)
proc = subprocess.run( # noqa: S603
["node", str(HERE / "helper_272_choice_undo.mjs"), str(bundle), str(job)],
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_고르기_전_메모리_다시_셈_같음(out):
assert out["pristineSame"]
@pytest.mark.parametrize("name", ["choice", "shoulder", "bermDepth"])
def test_고름_되돌림_정본_다시_고름_같음(out, name):
case = out["cases"][name]
assert case["changed"], name # 고름이 실제로 설계를 바꿈
assert case["undoIsCanon"], name # ↶ = 정본
assert case["redoIsPicked"], name # ↷ = 고른 값
assert case["undoAgain"], name
def test_암_경계_되돌림_정본(out):
case = out["cases"]["rock"]
assert case["undoIsCanon"] and case["redoIsPicked"] and case["undoAgain"]
def test_단추가_메모리_설계를_안_덮음():
sync = (ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts").read_text(encoding="utf-8")
body = sync[
sync.index("async function handleDesignChange") : sync.index("function recomputeIfRock")
]
assert "target.design" not in body
refresh = (ROOT / "B06_Section" / "B06_Section_Cross_Refresh.ts").read_text(encoding="utf-8")
assert "savedDesigns(projectId, input.routeId)" in refresh