- 노선 저장 직전 저장 관(id · 옛 누가거리)을 떠 두고 저장 뒤 같은 id 새 자리로 — 초안으로 안 따라간 관 조절(첫 초안 측점 짝 · 관 id 규칙 갈림) - carryChainageStates 시설 풀이 = 옛 자리 → 새 자리 · 관 열쇠 → 새 자리 - 시험 test_417 옛 자리 짝 · 346-3 저장 차례(관 받기) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
180 lines
7.6 KiB
Python
180 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""417 — 노선 바뀐 [저장] 때 B06 조절 · 사면 쉬프트 · 소단 깊이 옮김(M2) · 평면 초안 횡단 입력
|
||
(M3 · L4).
|
||
|
||
못박는 것
|
||
- 브라우저: 측점 열쇠 칸(사면 쉬프트 · 소단 깊이) · 누가거리 열쇠 칸(옹벽 · BOX · 측점 폭 ·
|
||
상세 표 · 연동)이 새 노선 측점으로 옮겨 감 · 구조물 조절은 관 · 구조물 열쇠만(일반 측점 옹벽
|
||
조절은 버림).
|
||
- 옮긴 값이 새 상세 설계 칸에 얹힘(같은 [저장] 횡단 패치가 실음).
|
||
- 평면 초안 횡단 입력 = 세션 사면 쉬프트 · 소단 깊이 + 저장분(세션 없음)도.
|
||
- 서버: `carry_cross_inputs` 가 `ROUTE_CARRY_KEYS` 를 측점 열쇠마다 넘김 · 구조물 조절은 관
|
||
열쇠만 · `compute_default_designs` 가 넘긴 사면 쉬프트로 설계선을 셈.
|
||
- 거울: 브라우저 `ROUTE_CARRY_STATES` 설계 칸 = 서버 `ROUTE_CARRY_KEYS` +
|
||
`STRUCTURE_ADJUST_KEYS` = `USER_TOUCHED_KEYS` − 소단 제원 · 암 경사(둘은 `CARRY_INPUT_KEYS`).
|
||
|
||
Node 헬퍼 `helper_417_route_carry.mjs` 가 vite ssr 로 묶은 진짜 모듈(`helper_417_entry.ts`)을 부름.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
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 B05_Profile.B05_Profile_Engine_Sections import find_cross_file # noqa: E402
|
||
from B05_Profile.B05_Profile_Route_Save_Parts import carry_cross_inputs # noqa: E402
|
||
from B06_Section.B06_Section_Engine_Design import ROUTE_CARRY_KEYS # noqa: E402
|
||
from B06_Section.B06_Section_Router_Design import ( # noqa: E402
|
||
CARRY_INPUT_KEYS,
|
||
STRUCTURE_ADJUST_KEYS,
|
||
USER_TOUCHED_KEYS,
|
||
compute_default_designs,
|
||
)
|
||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||
|
||
TS_KEYS = ROOT / "B06_Section" / "B06_Section_Station_Keys.ts"
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def out(tmp_path_factory):
|
||
work = tmp_path_factory.mktemp("b417")
|
||
build = subprocess.run(
|
||
[
|
||
"node",
|
||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||
"build",
|
||
"--configLoader",
|
||
"runner",
|
||
"--ssr",
|
||
"../resources/tester/helper_417_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_417_entry.js", bundle)
|
||
source = work / "input.json"
|
||
source.write_text(json.dumps({"standard": STANDARD_CROSS_SECTION}, default=str), "utf-8")
|
||
proc = subprocess.run(
|
||
["node", str(HERE / "helper_417_route_carry.mjs"), str(bundle), str(source)],
|
||
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["shoulderKeys"] == ["grid:2"]
|
||
assert out["bermKeys"] == ["grid:2"]
|
||
|
||
|
||
def test_조절은_관_따라_옮김_일반_측점_옹벽은_버림(out):
|
||
assert out["revet"] == {"47.00:outlet": {"x": 0.3, "d": None, "h": 1.5, "m": None}}
|
||
assert out["box"] == {"47.00": {"left": {"x": 0.2}}}
|
||
assert out["link"] == {"detached": {"47.00": True}, "grade": {}}
|
||
assert out["width"] == {"40.00": 6}
|
||
assert out["table"] == {"47.00": {"rows": 2}}
|
||
assert out["chainageOutside"] == 1 # 40 m 옹벽 조절(일반 측점)
|
||
|
||
|
||
def test_관_id_규칙이_갈려도_시설_자리로_옮김(out):
|
||
assert out["splitResolved"] == {"52.00:outlet": {"x": 0.3, "d": None, "h": 1.5, "m": None}}
|
||
assert out["splitDropped"] == {} # 풀이 없음 = 같은 자리 측점 없음 → 버림
|
||
# 초안으로 안 따라간 옛 저장 시설 측점 고침 = 좌표 짝 시설 측점으로
|
||
assert out["movedByPlace"] == {"52.00:outlet": {"x": 0.3, "d": None, "h": 1.5, "m": None}}
|
||
|
||
|
||
def test_새_상세_설계_칸에_얹힘(out):
|
||
d40, d47 = out["design40"], out["design47"]
|
||
assert d40["shoulder_extend_left_m"] == 0.5 and d40["berm_depth_right_m"] == 1.2
|
||
assert d40["display_half_width_m"] == 6 and "revet_adjust" not in d40
|
||
assert d47["revet_adjust"]["outlet"]["h"] == 1.5
|
||
assert d47["box_adjust"]["left"]["x"] == 0.2
|
||
assert d47["revet_link_detached"] is True and d47["detail_table"] == {"rows": 2}
|
||
|
||
|
||
def test_평면_초안_횡단_입력에_사면_쉬프트_소단_깊이(out):
|
||
assert out["input40"] == {"shoulderExtendLeftM": 0.5, "bermDepthRightM": 1.2}
|
||
# 세션 없음 = 저장분 그대로(저장 뒤 사라지지 않음 · L4)
|
||
assert out["input20"] == {"shoulderExtendRightM": 0.7, "bermDepthLeftM": 0.4}
|
||
|
||
|
||
def test_서버_이월_측점_손질_넘김_구조물_조절은_관만():
|
||
old = {"grid:2": 40.0, "pipe:p1": 45.0, "grid:3": 60.0}
|
||
new = {"grid:2": 40.0, "pipe:p1": 47.0}
|
||
designs = {
|
||
40.0: {"shoulder_extend_left_m": 0.5, "structure_note": "메모", "revet_adjust": {"h": 1}},
|
||
45.0: {"berm_depth_right_m": 1.2, "revet_adjust": {"h": 2}, "detail_table": {"r": 1}},
|
||
60.0: {"shoulder_extend_right_m": 0.3},
|
||
}
|
||
inputs, adjusts, dropped = carry_cross_inputs(
|
||
old, new, designs, (*CARRY_INPUT_KEYS, *ROUTE_CARRY_KEYS), STRUCTURE_ADJUST_KEYS
|
||
)
|
||
assert inputs == {
|
||
40.0: {"shoulder_extend_left_m": 0.5, "structure_note": "메모"},
|
||
47.0: {"berm_depth_right_m": 1.2, "detail_table": {"r": 1}},
|
||
}
|
||
assert adjusts == {47.0: {"revet_adjust": {"h": 2}}}
|
||
assert dropped == 1
|
||
|
||
|
||
def test_서버_기본_설계가_넘긴_사면_쉬프트로_셈(tmp_path):
|
||
route = tmp_path / "routes" / "r1"
|
||
(route / "longitudinal").mkdir(parents=True)
|
||
(route / "cross_sections").mkdir()
|
||
profile = [
|
||
{"chainage_m": 0.0, "elevation_m": 100.0},
|
||
{"chainage_m": 40.0, "elevation_m": 100.0},
|
||
]
|
||
relative = "routes/r1/longitudinal/longitudinal.json"
|
||
(tmp_path / relative).write_text(
|
||
json.dumps({"stations": [{"chainage_m": 20.0}], "design_profiles": [{"samples": profile}]}),
|
||
"utf-8",
|
||
)
|
||
samples = [
|
||
{"offset_m": float(o), "elevation_m": 100.0 + 0.3 * o, "valid": True}
|
||
for o in range(-15, 16)
|
||
]
|
||
cross = {"chainage_m": 20.0, "samples": samples, "uphill_side": "left"}
|
||
find_cross_file(route / "cross_sections", 20.0).write_text(json.dumps(cross), "utf-8")
|
||
plain = compute_default_designs(tmp_path, relative, [20.0])
|
||
carried = {20.0: {"shoulder_extend_right_m": 0.8, "structure_note": "메모"}}
|
||
shifted = compute_default_designs(tmp_path, relative, [20.0], carried=carried)
|
||
assert plain and shifted
|
||
design = shifted[0][1]
|
||
assert design["shoulder_extend_right_m"] == pytest.approx(0.8)
|
||
assert design["structure_note"] == "메모"
|
||
assert "shoulder_extend_right_m" not in plain[0][1]
|
||
assert design["design_line"] != plain[0][1]["design_line"] # 설계선이 쉬프트로 바뀜
|
||
|
||
|
||
def test_거울_브라우저_옮김_목록_서버_이월_열쇠():
|
||
text = TS_KEYS.read_text(encoding="utf-8")
|
||
block = re.search(r"ROUTE_CARRY_STATES = \{(.*?)\} as const", text, re.S).group(1)
|
||
rows = re.findall(r":\s*\[([^\]]*)\]", block) # 칸 이름 → [설계 칸 …]
|
||
ts_keys = {key for row in rows for key in re.findall(r'"([a-z_]+)"', row)}
|
||
assert ts_keys == set(ROUTE_CARRY_KEYS) | set(STRUCTURE_ADJUST_KEYS)
|
||
assert ts_keys == set(USER_TOUCHED_KEYS) - {"berm", "cut_slope_ratio_user"}
|
||
assert {"berm", "cut_slope_ratio_user"} <= set(CARRY_INPUT_KEYS)
|