# -*- coding: utf-8 -*- """475-3(요구 275-3) — 노선이 바뀌면 자리 칸은 같은 자리 측점만 들고 감. 자리 칸 = 암 경계 · 2단 경사 · 손 상단측(TS `SEAT_KEYS` 하나 · 파이썬은 그 글을 읽음). 못박는 것 - 자리 판정 거울 — TS `sameSeat` ↔ 파이썬 `same_seat`(중심 0.01 m · 왼쪽 축 1e-3 · 모르면 같은 자리). - 브라우저 · Node 넘김(`carryKeyedStates`) — 같은 자리 = 자리 칸도 · 옮긴 자리 = 자리 칸 버림(새 자리 기본값) · 지반 · 측구 종류 · 사면 쉬프트는 그대로 · 값의 쓴 때 자리(`@seat` · 되돌리기로 되살린 값)가 새 자리와 같으면 유지 · 자리 모르는 옛 목록 = 옛 동작. - 미리보기 입력(`crossAtSeat`) — 자리 칸 묶음이 측점 자리와 다르면 뺌 · 같으면 그대로. - 서버 노선 저장 넘김(`carry_cross_inputs` · `seats`) — 같은 규칙 · `seats` 없으면 옛 동작. - 기본 설계가 넘긴 손 단면 유형으로 셈(`compute_default_designs` · 없으면 상단측 깎기). ./venv/Scripts/python.exe -m pytest resources/tester/test_475_3_seat_carry.py -q """ import json import re 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, station_seats # noqa: E402 from B06_Section.B06_Section_Router_Design import ( # noqa: E402 CARRY_INPUT_KEYS, SEAT_KEYS, compute_default_designs, ) from common_util.common_util_station_key import same_seat, station_seat # noqa: E402 SEATS = [ [{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0.009, "y": 0, "left": [1, 0]}], [{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0.011, "y": 0, "left": [1, 0]}], [{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0, "y": 0, "left": [0.998, 0.002]}], [{"x": 0, "y": 0, "left": [1, 0]}, {"x": 0, "y": 0, "left": [0.9995, 0.0009]}], [{"x": 0, "y": 0, "left": None}, {"x": 0, "y": 0.005, "left": [0, 1]}], [None, {"x": 9, "y": 9, "left": [0, 1]}], ] def _py_seat(seat): if seat is None: return None left = seat.get("left") return seat["x"], seat["y"], tuple(left) if left else None @pytest.fixture(scope="module") def out(tmp_path_factory): work = tmp_path_factory.mktemp("b475_3") build = subprocess.run( [ "node", str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"), "build", "--configLoader", "runner", "--ssr", "../resources/tester/helper_475_3_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")) inputs = work / "input.json" inputs.write_text(json.dumps({"seats": SEATS}), "utf-8") proc = subprocess.run( ["node", str(HERE / "helper_475_3_seat_carry.mjs"), str(bundle), str(inputs)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_자리_칸_목록_정본은_TS_하나(): swap = (ROOT / "B06_Section" / "logic" / "B06_Section_Station_Swap.ts").read_text("utf-8") body = re.search(r"export const SEAT_KEYS = \[(.*?)\] as const;", swap, re.S) assert tuple(re.findall(r'"([a-z_0-9]+)"', body.group(1))) == SEAT_KEYS assert SEAT_KEYS == ( "two_stage_slope", "rock_boundary_offset_m", "cut_slope_ratio_user", "section_mode", "ditch_side", ) def test_자리_판정_거울(out): assert out["sameSeat"] == [same_seat(_py_seat(a), _py_seat(b)) for a, b in SEATS] assert out["sameSeat"] == [True, False, False, True, True, True] def test_같은_자리는_자리_칸도_옮긴_자리는_기본값(out): carried = out["carried"] assert carried["rockb"] == { "grid:1": 1.2, "grid:3": 0.9, } # grid:2 옮김 = 버림 · grid:3 = 쓴 때 자리 assert carried["cutslope"] == {"grid:1": 0.8} assert carried["crossdesign"]["grid:1"] == { # 포장은 안 넘김(138-1) "ground_type": "soil", "ditch_type": "l_type", "two_stage_slope": True, "section_mode": "right_cut", "ditch_side": "left", } assert carried["crossdesign"]["grid:2"] == {"ground_type": "soil", "ditch_type": "l_type"} assert carried["shoulderext"] == {"grid:2": {"left": 0.5}} # 자리 칸 아님 = 그대로 assert out["carriedSeats"]["grid:3"] == {"x": 3, "y": 60, "left": [-1, 0]} assert out["outside"] == 0 def test_자리_모르는_옛_목록은_옛_동작(out): unknown = out["unknown"] assert unknown["rockb"] == {"grid:1": 1.2, "grid:2": 1.5, "grid:3": 0.9} assert unknown["crossdesign"]["grid:2"]["two_stage_slope"] is True def test_미리보기_입력_자리_칸(out): at = out["atSeat"] assert at["grid:1"] == { "groundType": "soil", "rockBoundaryOffsetM": 1, "sectionMode": "right_cut", } # grid:2 = 저장 자리(0, 40)에서 온 2단 경사는 뺌 · 새 자리(5, 40)에서 쓴 암 경사는 그대로 assert at["grid:2"] == {"groundType": "soil", "cutSlopeRatio": 0.7} def test_B06_갈아끼움_자리_바뀐_측점은_자리_칸_기본값(out): same, moved = out["swapDraft"] assert same["rock_boundary_offset_m"] == -1.2 and same["section_mode"] == "both_cut" assert moved == { "ground_type": "soil", "section_mode": "left_cut", "shoulder_extend_left_m": 0.5, } # 노선 초안 아닌 차례(관 · 구조물 · 추가 측점만) = 이어 받은 설계 그대로 assert out["swapNoDraft"][1]["two_stage_slope"] is True def _row(chainage, x, left=(-1.0, 0.0)): return { "chainage_m": chainage, "center_x": x, "center_y": chainage, "frame": {"left_xy": list(left)}, } def test_서버_노선_저장_넘김_같은_규칙(): old_keys = {"grid:1": 20.0, "grid:2": 40.0} new_keys = {"grid:1": 20.0, "grid:2": 40.5} old_rows = [_row(20.0, 0.0), _row(40.0, 0.0)] new_rows = [_row(20.0, 0.0), _row(40.5, 5.0)] seats = (station_seats(old_rows, old_keys), station_seats(new_rows, new_keys)) hand = { "ground_type": "soil", "two_stage_slope": True, "rock_boundary_offset_m": 1.5, "cut_slope_ratio_user": 0.8, "section_mode": "right_cut", "ditch_side": "left", "shoulder_extend_left_m": 0.5, } designs = {20.0: dict(hand), 40.0: dict(hand)} keys = (*CARRY_INPUT_KEYS, "shoulder_extend_left_m") inputs, _, dropped = carry_cross_inputs(old_keys, new_keys, designs, keys, (), SEAT_KEYS, seats) assert inputs[20.0] == hand assert inputs[40.5] == {"ground_type": "soil", "shoulder_extend_left_m": 0.5} assert dropped == 0 # seats 없음 = 옛 동작(손 상단측은 안 넘김 · 자리 칸 셋은 넘김) old, _, _ = carry_cross_inputs(old_keys, new_keys, designs, keys, ()) assert old[40.5] == {k: v for k, v in hand.items() if k not in ("section_mode", "ditch_side")} assert station_seat({"chainage_m": 1}) is None def test_기본_설계가_넘긴_손_단면_유형으로_셈(tmp_path, monkeypatch): """엔진 입력 = 넘긴 단면 유형 · 측구 방향(결과 단면 유형은 엔진이 지형으로 다시 판정).""" import B06_Section.B06_Section_Router_Design as design_module seen: list[tuple[str, object]] = [] real = design_module.compute_cross_designs # 511-B B-4 — 측점 묶음 Node 한 번 def spy(jobs, *args, **kwargs): seen.extend((options["section_mode"], options.get("ditch_side")) for *_, options in jobs) return real(jobs, *args, **kwargs) monkeypatch.setattr(design_module, "compute_cross_designs", spy) 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" stations = [{"chainage_m": 20.0, "uphill_side": "left"}] (tmp_path / relative).write_text( json.dumps({"stations": stations, "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]) hand = compute_default_designs( tmp_path, relative, [20.0], carried={20.0: {"section_mode": "both_cut", "ditch_side": "right"}}, ) assert plain and hand assert seen == [("left_cut", None), ("both_cut", "right")]