# -*- 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)