# -*- coding: utf-8 -*- """종단 계획고 변경이 횡단 설계에 반영되는지(2026-08-23 사용자 지시). 횡단 설계는 계산 당시 계획고(design.design_elevation_m)를 기준으로 설계선 좌표를 굳혀 둔다. 계획선이 뒤에 바뀌면 설계선은 옛 자리에 남는데 화면의 계획고 십자선· 3D 예상형상은 최신 계획선을 쓴다 — 두 기준이 어긋난다(실측: 한 노선 최대 2.21m). 어긋남 판정을 B05·B06 공용 규칙(hasStaleDesigns)으로 두고, B05 진입 때도 재계산이 돌게 한 것을 소스로 잠근다. """ 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)) COMMON = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Section_Common.ts").read_text( encoding="utf-8" ) B05_PANEL = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Panel.ts").read_text( encoding="utf-8" ) B06_PAGE = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page.ts").read_text(encoding="utf-8") def _stale(profile_samples, sections, tol=1e-3): """TS hasStaleDesigns와 같은 규칙 — 계획선 보간값과 계산 기준의 차이.""" def plan_at(ch): if not profile_samples: return None if ch <= profile_samples[0][0]: return profile_samples[0][1] if ch >= profile_samples[-1][0]: return profile_samples[-1][1] for i in range(1, len(profile_samples)): c0, z0 = profile_samples[i - 1] c1, z1 = profile_samples[i] if ch <= c1: span = c1 - c0 t = (ch - c0) / span if span > 0 else 0 return z0 + (z1 - z0) * t return profile_samples[-1][1] out = [] for ch, used in sections: if used is None: continue planned = plan_at(ch) if planned is not None and abs(planned - used) > tol: out.append(ch) return out PROFILE = [(0.0, 100.0), (20.0, 102.0), (40.0, 104.0)] def test_detects_design_computed_from_old_plan(): """계획선이 바뀌어 계산 기준과 어긋난 측점을 잡는다.""" assert _stale(PROFILE, [(20.0, 101.0)]) == [20.0] def test_clean_when_design_matches_plan(): """계획선과 계산 기준이 같으면 재계산 대상이 아니다.""" assert _stale(PROFILE, [(0.0, 100.0), (20.0, 102.0), (40.0, 104.0)]) == [] def test_interpolates_between_profile_samples(): """측점이 계획선 샘플 사이에 있으면 보간값과 비교한다(비정규 측점·배수관 자리).""" assert _stale(PROFILE, [(10.0, 101.0)]) == [] assert _stale(PROFILE, [(10.0, 100.5)]) == [10.0] def test_sections_without_design_are_skipped(): """설계가 아직 없는 측점은 판정 대상이 아니다(최초 생성 전).""" assert _stale(PROFILE, [(20.0, None)]) == [] def test_tolerance_ignores_rounding_noise(): """저장 반올림 수준(1e-3 이하)은 어긋남으로 보지 않는다 — 무한 재계산 방지.""" assert _stale(PROFILE, [(20.0, 102.0005)]) == [] def test_source_shared_rule_and_b05_entry_recompute(): """공용 판정 유틸을 B05 진입·B06 재계산이 함께 쓰는지 소스 검사.""" assert "export function hasStaleDesigns" in COMMON # B05: 진입 렌더에서 어긋남이 있으면 프리뷰 1회로 맞춘다. assert "hasStaleDesigns" in B05_PANEL # 진입 즉시 한 번 맞춘다 — 편집 반영 계획선을 넣어 부르고, 어긋나면 예약한다. assert "hasStaleDesigns({" in B05_PANEL assert "scheduleCrossPreview();" in B05_PANEL # B06: 같은 규칙 하나만 쓴다 — 옛 암 2단계 필드 누락 조건도 공용 유틸로 옮겼다 # (2026-09-03 일원화: 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다). assert "hasStaleDesigns(sectionDetail)" in B06_PAGE assert "two_stage_slope === undefined" in COMMON assert "two_stage_slope === undefined" not in B06_PAGE