B05 와 B06 은 한 페이지인데 구조물을 넣고 빼는 자리가 B05 뿐이라 화면을 오가야 했음. 종단 그래프 렌더러·알약 레인·좌측 배치 폼은 이미 공용이었으므로, 빠져 있던 조작층만 B05 와 같은 부품(mountStructureMenu)으로 B06 에도 붙임. 관 추가·삭제는 이동과 같은 예약 경로를 씀 — 세션에 쌓고 [저장]·[확정]에서 정본 (pipe_points.json)으로 나가며, 세부 배수유역 재분할은 그때 서버가 처리함. 넣었다 바로 빼거나 뺐다 다시 넣으면 예약만 걷어 내 정본이 흔들리지 않음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
136 lines
5.0 KiB
Python
136 lines
5.0 KiB
Python
"""B06 종단에서 넣고 뺀 **계곡 통과 시설**이 세션에 제대로 쌓이는지(2026-09-12 사용자 지시).
|
|
|
|
B05·B06 은 한 페이지이므로 관 추가·삭제도 양쪽에서 되어야 한다. 다만 정본에 바로 쓰지
|
|
않고 **세션에 예약**했다가 [저장]·[확정]에서 한 번에 내보낸다(CLAUDE.md 5장). 넣었다 바로
|
|
빼면 예약만 걷어 내야 하고, 뺐다 다시 넣어도 마찬가지다 — 그러지 않으면 저장 때 있지도
|
|
않은 관을 지우거나 같은 자리에 둘이 선다.
|
|
|
|
실제 화면 코드를 그대로 컴파일해 Node 로 돌린다(다단 하한 시험과 같은 방식).
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
|
MODULE = PROJECT_ROOT / "B06_Section" / "B06_Section_Api_Culvert_Options.ts"
|
|
|
|
_RUNNER = """
|
|
const { writeFileSync } = require("node:fs");
|
|
const Module = require("node:module");
|
|
|
|
// 화면 코드가 번들러 별칭(`@config/…`)을 쓴다 — Node 에는 없으니 빈 껍데기로 돌려준다.
|
|
// 이 시험이 보는 것은 세션 예약 로직뿐이라 값은 안 쓴다.
|
|
const load = Module._load;
|
|
Module._load = function (request, parent, isMain) {
|
|
if (request.startsWith("@config/") || request.startsWith("@util/") || request.startsWith("@ui/"))
|
|
return new Proxy({}, { get: () => () => undefined });
|
|
return load.call(this, request, parent, isMain);
|
|
};
|
|
|
|
// 세션 저장소 흉내 — 화면 코드는 window.sessionStorage 만 쓴다.
|
|
const store = new Map();
|
|
global.window = {
|
|
sessionStorage: {
|
|
getItem: (key) => (store.has(key) ? store.get(key) : null),
|
|
setItem: (key, value) => store.set(key, String(value)),
|
|
removeItem: (key) => store.delete(key),
|
|
},
|
|
};
|
|
|
|
const { createCulvertOptionWriter } = require(process.argv[3]);
|
|
const EDIT_KEY = "edit";
|
|
const writer = createCulvertOptionWriter(
|
|
() => "p1",
|
|
() => "opt",
|
|
undefined,
|
|
() => "move",
|
|
() => EDIT_KEY,
|
|
);
|
|
const read = () => JSON.parse(store.get(EDIT_KEY) ?? '{"adds":[],"removes":[]}');
|
|
const steps = {};
|
|
|
|
writer.queueAdd(120, "pipe");
|
|
writer.queueAdd(140);
|
|
steps.afterTwoAdds = read();
|
|
|
|
// 방금 넣은 자리를 다시 빼면 **추가 예약만** 사라진다(정본에는 아직 없다).
|
|
writer.queueRemove(140);
|
|
steps.afterUndoAdd = read();
|
|
|
|
// 정본에 있던 관을 빼면 삭제 예약으로 남는다.
|
|
writer.queueRemove(200);
|
|
steps.afterRemove = read();
|
|
|
|
// 뺀 자리를 다시 넣으면 삭제 예약만 걷어 낸다(새로 만들지 않는다).
|
|
writer.queueAdd(200);
|
|
steps.afterUndoRemove = read();
|
|
|
|
// 같은 자리를 두 번 넣어도 하나만 남는다.
|
|
writer.queueAdd(120);
|
|
steps.afterDuplicateAdd = read();
|
|
|
|
writeFileSync(process.argv[2], JSON.stringify(steps));
|
|
"""
|
|
|
|
|
|
def _run(tmp_path: Path) -> dict:
|
|
out = tmp_path / "out"
|
|
subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
[
|
|
"node",
|
|
str(TSC),
|
|
"--ignoreConfig",
|
|
"--target",
|
|
"es2022",
|
|
"--module",
|
|
"commonjs",
|
|
"--skipLibCheck",
|
|
"--outDir",
|
|
str(out),
|
|
str(MODULE),
|
|
],
|
|
cwd=str(PROJECT_ROOT),
|
|
check=False,
|
|
capture_output=True,
|
|
)
|
|
compiled = next(out.rglob("B06_Section_Api_Culvert_Options.js"), None)
|
|
assert compiled is not None, "화면 코드가 JS 로 안 나옴 — tsc 실패"
|
|
(out / "runner.cjs").write_text(_RUNNER, encoding="utf-8")
|
|
result = tmp_path / "result.json"
|
|
subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(out / "runner.cjs"), str(result), str(compiled)],
|
|
cwd=str(PROJECT_ROOT),
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return json.loads(result.read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_추가와_삭제_예약이_서로를_걷어_낸다(tmp_path: Path) -> None:
|
|
steps = _run(tmp_path)
|
|
|
|
# 넣은 둘이 그대로 쌓인다.
|
|
assert [entry["chainage_m"] for entry in steps["afterTwoAdds"]["adds"]] == [120.0, 140.0]
|
|
assert steps["afterTwoAdds"]["adds"][0]["facility"] == "pipe"
|
|
assert steps["afterTwoAdds"]["removes"] == []
|
|
|
|
# 방금 넣은 자리를 빼면 추가 예약만 사라지고 삭제 예약은 안 생긴다.
|
|
assert [entry["chainage_m"] for entry in steps["afterUndoAdd"]["adds"]] == [120.0]
|
|
assert steps["afterUndoAdd"]["removes"] == []
|
|
|
|
# 정본에 있던 관은 삭제 예약으로 남는다.
|
|
assert steps["afterRemove"]["removes"] == ["200.00"]
|
|
|
|
# 뺀 자리를 다시 넣으면 삭제 예약만 걷힌다 — 새 관을 만들지 않는다.
|
|
assert steps["afterUndoRemove"]["removes"] == []
|
|
assert [entry["chainage_m"] for entry in steps["afterUndoRemove"]["adds"]] == [120.0]
|
|
|
|
# 같은 자리를 두 번 넣어도 하나만 남는다.
|
|
assert [entry["chainage_m"] for entry in steps["afterDuplicateAdd"]["adds"]] == [120.0]
|