- 맞춤 규칙 한 벌 — common_util_structure_station(파이썬 · TS 짝 · 거울 시험) - 입력 — 좌측 폼 칸(1 m 단계 · 친 값만) · 끌기 · 우클릭 · 방향키(1 m 걸음) · 배수유역도 관 · B06 관 옮김 · 추가 - 구간형 시작 · 끝도 정수 m · 일반 측점 = 횡단 상세를 읽을 때 등록 - 서버 자동 배치 · 노선 재투영 자리도 정수 m(확정본은 단추로만) - [정수 맞춤] — 구조물 목록 머리 · 확인 창 · 관은 하나씩 폼 [수정] 길 · 계획고 편집 따라감 · 결과 n건 알림 - 찾는 폭 한 값 — 손으로 적은 0.51 을 PIPE_MATCH_M 으로 - 시험 — test_234_1 새로 · 1 m 걸음 · 정수 입력에 맞춘 기대값 고침(219_1 · station_roundtrip) · 헬퍼 대역 보탬 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
232 lines
9.1 KiB
Python
232 lines
9.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""234-1 구조물 · 관 기준 측점 = 정수 m · 일반 측점 1 m 안 붙기 · [정수 맞춤] 단추.
|
|
|
|
9-29 「저장값 소수 · 표기만 정수」(20장)를 「저장값도 정수 m」로 바꿈
|
|
(사용자 확정 10-10 · 조사 AF 7절 A+B+C).
|
|
맞춤 규칙 한 벌 = 파이썬 `common_util_structure_station.py` ↔
|
|
TS `common_util_structure_station.ts`(거울).
|
|
화면 쪽 진짜 TS(폼 칸 · 구조물 옮김 · 단추 · 배수유역도 관 · 계획고 편집 따라감)는
|
|
`helper_234_1_structure_station.cjs` 가 Node 로 돌린다. 서버 자동 배치 · 노선 재투영은 파이썬으로.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
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 common_util.common_util_drainage_pipes import PipePoint, project_pipe_points # noqa: E402
|
|
from common_util.common_util_route_geometry import RouteVertex, StructureCandidate # noqa: E402
|
|
from common_util.common_util_structure_station import ( # noqa: E402
|
|
general_stations,
|
|
snap_candidates,
|
|
snap_structure_station,
|
|
)
|
|
|
|
TOTAL = 279.8
|
|
STATIONS = general_stations(TOTAL, 20.0)
|
|
# (입력, 기대) — 소수 → 정수 · 일반 측점 1 m 안 붙기 · 끝 측점(소수) 붙기
|
|
CASES = [
|
|
(80.17, 80.0), # 관 80.17 = 카드 80 한 자리
|
|
(80.6, 80.0), # 1 m 안 → 일반 측점
|
|
(80.99, 80.0),
|
|
(81.0, 81.0), # 꼭 1 m 는 밖 — 81 에 둘 수 있음(방향키가 갇히지 않음)
|
|
(50.5, 51.0), # .5 는 올림(파이썬 · TS 같은 꼴)
|
|
(50.4, 50.0),
|
|
(263.31, 263.0),
|
|
(263.206, 263.0),
|
|
(127.86, 128.0),
|
|
(0.3, 0.0),
|
|
(279.3, 279.8), # 끝 측점 1 m 안 → 끝 측점(정수가 아닌 일반 측점)
|
|
(278.6, 279.8), # 반올림 279 가 끝 측점 1 m 안
|
|
(277.4, 277.0),
|
|
]
|
|
STEPS = [
|
|
((80.0, 1), 81.0),
|
|
((80.17, 1), 81.0),
|
|
((80.17, -1), 80.0), # 79.17 은 일반 측점 80 의 1 m 안
|
|
((279.8, -1), 278.0), # 279 는 끝 측점에 붙어 제자리 → 한 걸음 더
|
|
((279.8, 1), 279.8), # 노선 밖은 제자리
|
|
((0.0, -1), 0.0),
|
|
]
|
|
STRUCTURES = [
|
|
{"structure_id": "sign", "placement": "point", "chainage_m": 127.86},
|
|
{
|
|
"structure_id": "wall",
|
|
"placement": "interval",
|
|
"chainage_m": 80.17,
|
|
"start_m": 75.17,
|
|
"end_m": 95.67,
|
|
},
|
|
{"structure_id": "ok", "placement": "point", "chainage_m": 40.0},
|
|
{"structure_id": "near", "placement": "point", "chainage_m": 20.4},
|
|
]
|
|
PIPES = [80.17, 120.0, 120.3, 263.31, 140.0]
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
|
reason="프론트엔드 의존성 미설치",
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def ts(tmp_path_factory) -> dict:
|
|
source = tmp_path_factory.mktemp("s234") / "in.json"
|
|
source.write_text(
|
|
json.dumps(
|
|
{
|
|
"stations": STATIONS,
|
|
"samples": [value for value, _ in CASES],
|
|
"steps": [list(args) for args, _ in STEPS],
|
|
"structures": STRUCTURES,
|
|
"pipes": PIPES,
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(HERE / "helper_234_1_structure_station.cjs"), str(source)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
cwd=str(ROOT),
|
|
check=False,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_general_stations() -> None:
|
|
assert STATIONS == [float(index * 20) for index in range(14)] + [279.8]
|
|
|
|
|
|
@pytest.mark.parametrize(("value", "expected"), CASES)
|
|
def test_snap_python(value: float, expected: float) -> None:
|
|
assert snap_structure_station(value, STATIONS) == expected
|
|
# 두 번 맞춰도 같음
|
|
assert snap_structure_station(expected, STATIONS) == expected
|
|
|
|
|
|
def test_mirror_python_ts(ts) -> None:
|
|
assert ts["snap"] == [snap_structure_station(value, STATIONS) for value, _ in CASES]
|
|
assert ts["snapTwice"] is True
|
|
|
|
|
|
def test_arrow_step_one_metre(ts) -> None:
|
|
assert ts["step"] == [expected for _, expected in STEPS]
|
|
|
|
|
|
def test_general_registry_skips_irregular(ts) -> None:
|
|
assert ts["general"] == STATIONS # 구조물 측점 80.17(irregular)은 일반 측점 아님
|
|
assert ts["defaultSnap"] == 80
|
|
|
|
|
|
def test_form_input_integer(ts) -> None:
|
|
assert ts["remainderStep"] == "1"
|
|
assert ts["typed"] == 80 # 입력 4+0.17 = 80.17 → 80
|
|
assert ts["typedNear"] == 80 # 4+0.6 → 일반 측점 80 에 붙음
|
|
assert ts["typedOne"] == 81
|
|
assert ts["untouched"] == 80.17 # 안 고친 옛 값은 그대로(단추 몫)
|
|
|
|
|
|
def test_moved_structure_range_ends_integer(ts) -> None:
|
|
assert ts["movedPoint"]["chainage_m"] == 128
|
|
wall = ts["movedWall"]
|
|
assert (wall["chainage_m"], wall["start_m"], wall["end_m"]) == (80, 75, 96)
|
|
drag = ts["movedDrag"]
|
|
assert (drag["chainage_m"], drag["start_m"], drag["end_m"]) == (133, 133, 145)
|
|
|
|
|
|
def test_snap_button_plan_and_run(ts) -> None:
|
|
plan = ts["plan"]
|
|
assert [entry["index"] for entry in plan["structures"]] == [0, 1, 3]
|
|
assert plan["pipes"] == [[80.17, 80], [263.31, 263]]
|
|
assert plan["skipped"] == 1 # 120.3 → 120 자리에 관이 이미 있음
|
|
run = ts["run"]
|
|
assert run["count"] == 5 and run["emits"] == 1 and run["closed"] == 1
|
|
assert [s["chainage_m"] for s in run["structures"]] == [128, 80, 40, 20]
|
|
assert (run["structures"][1]["start_m"], run["structures"][1]["end_m"]) == (75, 96)
|
|
# 관은 하나씩 폼 [수정]과 같은 길 — 시설 속성이 그대로 실림
|
|
assert [(m["from"], m["to"]) for m in run["moves"]] == [(80.17, 80), (263.31, 263)]
|
|
assert run["moves"][0]["attributes"]["options"] == {"pipe_kind": "흄관"}
|
|
assert run["moves"][0]["attributes"]["facility"] == "pipe"
|
|
assert "5건" in run["confirmText"]
|
|
assert run["toasts"][-1]["message"].startswith("정수 맞춤 5건")
|
|
assert "1건은 그대로" in run["toasts"][-1]["message"]
|
|
# 두 번째 누름 — 옮길 것 없음(겹친 관만 알림)
|
|
assert ts["second"]["count"] == 0
|
|
assert "1건은 맞출 자리에 다른 관" in ts["second"]["toasts"][0]["message"]
|
|
|
|
|
|
def test_plan_edit_follows_batch_move(ts) -> None:
|
|
# 213-2 규칙만으로는 여럿을 한꺼번에 옮기면 못 따라감(null) → 맞춘 자리로 따라감
|
|
assert ts["follow"] == [{"old": None, "next": 80}, {"old": None, "next": 263}]
|
|
|
|
|
|
def test_drainage_map_pipe_integer(ts) -> None:
|
|
assert ts["editor"] == [141, 200] # 옮김 141.37 → 141 · 추가 200.6 → 일반 측점 200
|
|
|
|
|
|
def _vertices() -> list[RouteVertex]:
|
|
return [RouteVertex(x=0.0, y=0.0, z=0.0, chainage_m=0.0), RouteVertex(TOTAL, 0.0, 0.0, TOTAL)]
|
|
|
|
|
|
def test_server_auto_placement_integer() -> None:
|
|
vertices = _vertices()
|
|
placed = [
|
|
StructureCandidate(chainage_m=80.374, x=80.374, y=0.0, reason="stream"),
|
|
StructureCandidate(chainage_m=150.6, x=150.6, y=0.0, reason="spacing"),
|
|
StructureCandidate(chainage_m=150.9, x=150.9, y=0.0, reason="spacing"),
|
|
]
|
|
snapped = snap_candidates(placed, vertices)
|
|
assert [(p.chainage_m, p.x, p.reason) for p in snapped] == [
|
|
(80.0, 80.0, "stream"),
|
|
(151.0, 151.0, "spacing"), # 같은 자리로 모인 둘째는 버림
|
|
]
|
|
|
|
|
|
def test_server_reprojection_integer() -> None:
|
|
points = [PipePoint(chainage_m=70.0, x=80.374, y=3.0, start_m=75.0, end_m=85.0)]
|
|
moved = project_pipe_points(points, _vertices())
|
|
assert moved[0].chainage_m == 80.0
|
|
assert (moved[0].start_m, moved[0].end_m) == (85.0, 95.0) # 구간은 같은 만큼 밀림
|
|
|
|
|
|
def test_wiring_source() -> None:
|
|
"""받는 곳마다 맞춤이 걸렸는지(짝 머리 · 폼 · 끌기 · 방향키 · 관 · 단추)."""
|
|
|
|
def read(rel: str) -> str:
|
|
return (ROOT / rel).read_text(encoding="utf-8")
|
|
|
|
assert "common_util_structure_station.py" in read(
|
|
"common_util/common_util_structure_station.ts"
|
|
)
|
|
assert "common_util_structure_station.ts" in read(
|
|
"common_util/common_util_structure_station.py"
|
|
)
|
|
assert "movedStructure(structures[index], toChainageM)" in read(
|
|
"B05_Profile/B05_Profile_UI_Structures_Panel.ts"
|
|
)
|
|
assert "snapDock(list" in read("B05_Profile/B05_Profile_UI_Structures_Panel.ts")
|
|
assert "stepStructureStation(fromM" in read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
|
b06 = read("B06_Section/B06_Section_UI_Page_Structures_Panel.ts")
|
|
assert "const to = snapStructureStation(toChainageM)" in b06
|
|
assert "Math.abs(fromChainageM - toChainageM) > 0.005" in b06
|
|
assert "setGeneralStations" in read("B06_Section/B06_Section_Section_Store.ts")
|
|
assert "snap_candidates(" in read("common_util/common_util_drainage_detail.py")
|
|
for rel in (
|
|
"B05_Profile/B05_Profile_UI_Drainage_Panel.ts",
|
|
"B05_Profile/B05_Profile_UI_IrregularStations.ts",
|
|
"B05_Profile/B05_Profile_UI_Drainage_Parts.ts",
|
|
"B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts",
|
|
):
|
|
assert "< 0.51" not in read(rel), rel # 찾는 폭 = PIPE_MATCH_M 한 값
|