Files
Aislo/resources/tester/test_272_2_station_sync.py
T
eomsangdonandClaude Opus 5.5 93e86f87c6 fix(b05,b06): 측점 한 벌 — 정수 m · 같은 m 측점 하나 · 목록 전체 다시 맞춤 (272)
- 규칙: 구조물 · 관 기준 측점 = 정수 m 사사오입(일반 측점 1 m 붙기 뺌) · 측점 심는 자리 TS 짝(plantedChainages ↔ resolve_extra_stations) · 같은 m 면 구조물 측점 하나(격자 자리 grid) · 떠나면 일반 측점(oneStationList ↔ _merge_irregular_into_longitudinal)
- 서버: B05 [저장] · B06 저장 · [측점 만들기]가 목록 전체 다시 맞춤(빈 목록도 · 옛 글 · 횡단 파일 글) · 계획선 측점 줄 다시 셈 · 정본에 없는 횡단 행은 0.5 m 안 새 측점으로 옮기고 나머지 지움 · 옛 소수 측점 꼭짓점 · 편집은 정수 자리로 · 맞춤 실패는 저장을 안 막음
- 화면: 종단 그래프 · 3D · 표 · ▲▼ 한 목록(초안 반영) · C군 구간형 · 독립 기슭막이 시작 · 끝 측점 · B06 카드는 초안에 없는 옛 구조물 측점 숨김 · 고름은 같은 m 구조물 측점 먼저 · 폼 거리 칸 정수 표기 · C군 계산 구간 · 포장 나눔 경계 정수
- 옛 자료: [정수 맞춤] 단추 없앰 · 종단 보기 열 때 한 번 자동 정수 · 알림 한 줄 · [저장]하면 남음
- 시험: test_272_1 · test_272_2 새로 · 234_1 · 238_1 · b05_extra_stations_supplier · b06_patch_station · b05_station_roundtrip · b05_b06_save_roundtrip 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
2026-10-11 19:53:24 +09:00

194 lines
8.6 KiB
Python

"""272-2 — 구조물 측점을 지금 목록 전체로 다시 맞춤(요구 167 · 측점 겹침 근본).
서버는 관 · 구조물 목록 **전체**로 종단 정본의 구조물 측점을 다시 맞춘다 — 빈 목록이어도 옛 구조물
측점을 지우고, 구조물이 떠난 격자 자리는 일반 측점으로 되돌리며(횡단 파일 글까지), 같은 m 면
측점 하나(구조물이 위 · `grid`). 종단 정본에 없는 측점의 횡단 행도 지운다(B08 이 옛 자리를 안 셈).
"""
from __future__ import annotations
import asyncio
import json
import math
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))
from B05_Profile.B05_Profile_Engine_Sections import cross_filename # noqa: E402
from B05_Profile.B05_Profile_Repository import sync_cross_section_rows # noqa: E402
from B05_Profile.B05_Profile_Router_Confirm import _merge_irregular_into_longitudinal # noqa: E402
REL = "B06_Section/longitudinal/longitudinal.json"
PIPE = "파형강관 D800"
def _write(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
def _cross(root: Path, chainage: float) -> Path:
return root / "B06_Section/cross_sections" / cross_filename(chainage)
def _setup(root: Path) -> None:
"""격자 0~100(20 m) · 옛 관 측점 95.38 · 옛 판 「regular + 글」 60 m."""
grid = [
{"chainage_m": float(c), "kind": "bp" if c == 0 else "regular"} for c in range(0, 101, 20)
]
grid[3]["structure"] = "옛 글"
stations = sorted(
[*grid, {"chainage_m": 95.38, "kind": "irregular", "structure": PIPE}],
key=lambda s: s["chainage_m"],
)
_write(root / REL, {"stations": stations})
for station in stations:
_write(_cross(root, station["chainage_m"]), {**station, "samples": []})
def _stations(root: Path) -> list[dict]:
return json.loads((root / REL).read_text(encoding="utf-8"))["stations"]
def _at(root: Path, chainage: float) -> dict:
return next(s for s in _stations(root) if s["chainage_m"] == chainage)
def test_관이_격자_위로_가면_측점_하나_구조물이_위(tmp_path: Path) -> None:
_setup(tmp_path)
moved = {"chainage_m": 100.0, "kind": "irregular", "grid": True, "structure": PIPE}
_merge_irregular_into_longitudinal(tmp_path, REL, [moved])
chainages = [s["chainage_m"] for s in _stations(tmp_path)]
assert 95.38 not in chainages and chainages.count(100.0) == 1
assert _at(tmp_path, 100.0) == moved
# 옛 판 「regular + 글」 도 글을 지움 — 횡단 파일 글까지
assert _at(tmp_path, 60.0) == {"chainage_m": 60.0, "kind": "regular"}
cross = json.loads(_cross(tmp_path, 60.0).read_text(encoding="utf-8"))
assert cross["kind"] == "regular" and "structure" not in cross
def test_구조물이_떠나면_일반_측점이_돌아옴_빈_목록도(tmp_path: Path) -> None:
_setup(tmp_path)
on_grid = {"chainage_m": 100.0, "kind": "irregular", "grid": True, "structure": PIPE}
_merge_irregular_into_longitudinal(tmp_path, REL, [on_grid])
_write(_cross(tmp_path, 100.0), {**on_grid, "samples": []}) # 측점 만들기가 쓴 파일 흉내
_merge_irregular_into_longitudinal(tmp_path, REL, [])
stations = _stations(tmp_path)
assert [s["chainage_m"] for s in stations] == [0.0, 20.0, 40.0, 60.0, 80.0, 100.0]
assert all(s["kind"] in ("bp", "regular") and "structure" not in s for s in stations)
assert all("grid" not in s for s in stations)
cross = json.loads(_cross(tmp_path, 100.0).read_text(encoding="utf-8"))
assert cross["kind"] == "regular" and "structure" not in cross and "grid" not in cross
def test_시점_위_구조물이_떠나도_시점은_시점(tmp_path: Path) -> None:
_setup(tmp_path)
at_bp = {"chainage_m": 0.0, "kind": "bp", "grid": True, "structure": PIPE}
_merge_irregular_into_longitudinal(tmp_path, REL, [at_bp])
assert _at(tmp_path, 0.0) == at_bp
_merge_irregular_into_longitudinal(tmp_path, REL, [])
assert _at(tmp_path, 0.0) == {"chainage_m": 0.0, "kind": "bp"}
def test_측점만_옮겨도_계획선_측점_줄을_다시_셈(tmp_path: Path) -> None:
"""편집 없는 관 측점 581.9 → 582 — 계획선 측점 줄(표 · ▲▼)이 옛 측점을 들고 남지 않음."""
source = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json"
path = tmp_path / REL
_write(path, json.loads(source.read_text(encoding="utf-8")))
data = json.loads(path.read_text(encoding="utf-8"))
moved = [
{**s, "chainage_m": 582.0} if s["chainage_m"] == 581.9 else s
for s in data["stations"]
if s.get("kind") == "irregular"
]
assert _merge_irregular_into_longitudinal(tmp_path, REL, moved) is not None
rows = [row["chainage_m"] for row in _stations_rows(path)]
assert 582.0 in rows and 581.9 not in rows
assert rows == [s["chainage_m"] for s in _stations(tmp_path)]
# 그대로 한 번 더 — 줄이 측점과 같으면 안 건드림
assert _merge_irregular_into_longitudinal(tmp_path, REL, moved) is None
def test_옛_소수_측점을_정수로_옮기면_꼭짓점_편집도_따라감(tmp_path: Path) -> None:
"""484.213 · 581.902 · 676.591 · 769.28 → 정수 m — 꼭짓점이 높이 그대로 옮겨 감(지우면 계획선이
그 자리에서 바뀜 · 남기면 측점 없는 꼭짓점이 1000% 넘게 꺾음 · 브라우저 실측)."""
source = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json"
data = json.loads(source.read_text(encoding="utf-8"))
_write(tmp_path / REL, data)
snapped = [
{**s, "chainage_m": float(math.floor(s["chainage_m"] + 0.5))}
for s in data["stations"]
if s.get("kind") == "irregular"
]
_merge_irregular_into_longitudinal(tmp_path, REL, snapped)
after = json.loads((tmp_path / REL).read_text(encoding="utf-8"))["profile_alignment"]
nodes = {n["chainage_m"]: n["elevation_m"] for n in after["base_pvi"]}
old_nodes = {n["chainage_m"]: n["elevation_m"] for n in data["profile_alignment"]["base_pvi"]}
for old, new in ((484.213, 484.0), (581.902, 582.0), (676.591, 677.0), (769.28, 769.0)):
assert old not in nodes and nodes[new] == old_nodes[old]
offsets = after["edits"]["station_offsets"]
assert "769.280" not in offsets and "769.000" in offsets
def _stations_rows(path: Path) -> list[dict]:
return json.loads(path.read_text(encoding="utf-8"))["profile_alignment"]["stations"]
class _Cursor:
def __init__(self, rows: list[tuple[int, float]]) -> None:
self.rows = rows
self.moved: list[tuple[float, int]] = []
self.deleted: list[int] | None = None
async def __aenter__(self) -> "_Cursor":
return self
async def __aexit__(self, *_a) -> bool:
return False
async def execute(self, sql: str, args=None) -> None:
if sql.startswith("UPDATE"):
self.moved.append(tuple(args))
elif sql.startswith("DELETE"):
self.deleted = list(args)
async def fetchall(self) -> list[tuple[int, float]]:
return self.rows
class _Connection:
def __init__(self, rows: list[tuple[int, float]]) -> None:
self.cursor_ = _Cursor(rows)
def cursor(self) -> _Cursor:
return self.cursor_
def _sync(rows: list[tuple[int, float]], stations: list[float]) -> tuple[int, _Cursor]:
connection = _Connection(rows)
return asyncio.run(sync_cross_section_rows(connection, 7, stations)), connection.cursor_
def test_옛_소수_측점_행은_새_정수_측점으로_옮김() -> None:
"""95.38 → 95 — 사용자 설계 값이 든 행을 그대로 옮김(면적은 저장 뒤 서버 재계산)."""
count, cursor = _sync([(1, 0.0), (2, 95.38), (3, 100.004)], [0.0, 20.0, 95.0, 100.0])
assert count == 1 and cursor.moved == [(95.0, 2)] and cursor.deleted is None
def test_가까운_새_측점이_없으면_지움() -> None:
count, cursor = _sync([(1, 0.0), (2, 95.38), (5, 140.0)], [0.0, 20.0, 100.0])
assert count == 2 and cursor.moved == [] and cursor.deleted == [2, 5]
def test_한_측점에_둘이_다투면_가까운_행만_옮김() -> None:
count, cursor = _sync([(2, 95.38), (6, 94.7), (7, 100.0)], [95.0, 100.0])
assert count == 2 and cursor.moved == [(95.0, 6)] and cursor.deleted == [2]
def test_측점_목록이_비면_아무것도_안_건드림() -> None:
count, cursor = _sync([(1, 0.0), (2, 95.38)], [])
assert count == 0 and cursor.moved == [] and cursor.deleted is None