- 좌측 패널 · 아래 단추 넷 · 종단도 · 구조물 배치 폼을 페이지에 한 번만 만듦(design_shared) — 두 보기가 같은 노드 - 보기는 가운데 위 칸만: 종단 보기 = 3D(design_view_profile) · 횡단 보기 = 횡단 카드(design_view_cross · 처음 보일 때 그림) - 옛 B05_Profile_UI_Page · B06_Section_UI_Page 삭제 · 한 벌이 받은 조작은 지금 보기로, 관 목록 · 횡단 재계산 알림은 두 보기 모두로 - 진행 단계 종단설계 · 횡단설계 두 칸이 보기를 따라 켜짐 · 누르면 주소만 바꿈 · 제목도 따라감(setTitle · setActiveStep) - [저장] · [확정] 한 벌 — 두 보기 몫을 모아 saveDesign 한 번 · 종단 보기 [확정] 켬(횡단 보기를 숨긴 채 세워 횡단 자료를 얻음) - [초기화] · 계획노선 편집은 같은 페이지라도 다시 세움(remountRoute) - 덧붙임: 방향키는 초점 있는 쪽만(조정창 ↔ 종단도 계획고) · 횡단 보기 계획고 편집에도 [3D 업데이트] 대기 표시 · 종단 보기 폼 관 옵션을 횡단 캐시에도 얹음 · 종단 보기에서 횡단 조정 칸 가림 - 나눔: Cross_View 756→658(_Cross_View_Frame) · Page_Structures_Panel 700→547(_Structures_Spec) · 옛 B05 Page 786 → design_view_profile 638(_Page_Latest · _Page_Stations) - 시험: test_design_view_parity 한 벌 기준(한 패널 · 보기 오가도 같은 노드) · page_single · profile_single · save_channels · save_recompute 등 옮긴 자리로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
197 lines
8.3 KiB
Python
197 lines
8.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""측점 · 구조물 고름 한 곳 회귀검증 — PLAN 24-2d (2026-09-29).
|
|
|
|
• 관 찾는 거리 한 값(`PIPE_MATCH_M = 0.51`) — 정의는 한 곳, 옛 0.05 는 없음
|
|
• 횡단 카드 · 종단 알약 · 목록 · 3D 표지 어디서 골라도 같은 순서(관 → 구조물) · 같은 거리로 폼이 열 항목을 풂
|
|
• 세션 값(`structure-pick`) 읽기 · 쓰기 · 알림 — 부재키 지킴 · 같은 값 조용 · 되울림 막음
|
|
• B06 진입 때 옹벽 · 기슭막이 카드를 구조물 id 로 찾음
|
|
화면 그릇 없이 Node 헬퍼가 TS 를 트랜스파일해 돌린다(`helper_b05_b06_selection.cjs`).
|
|
실제 화면 조작은 ORCA(자체 확인)로 따로 본다.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
|
|
|
|
|
def _read(*parts: str) -> str:
|
|
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
|
|
return handle.read()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def out():
|
|
proc = subprocess.run(
|
|
["node", os.path.join(HERE, "helper_b05_b06_selection.cjs")],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
# ── 거리 한 값 ─────────────────────────────────────────────────────────────
|
|
def test_거리_값은_0_51_하나(out):
|
|
assert out["matchM"] == 0.51
|
|
|
|
|
|
def test_거리_정의는_한_곳뿐이고_옛_0_05는_없다():
|
|
definitions = []
|
|
for folder in ("A00_Common", "B05_Profile", "B06_Section"):
|
|
for current, _dirs, names in os.walk(os.path.join(ROOT, folder)):
|
|
if "node_modules" in current:
|
|
continue
|
|
for name in names:
|
|
if not name.endswith(".ts"):
|
|
continue
|
|
text = _read(os.path.relpath(os.path.join(current, name), ROOT))
|
|
if re.search(r"\b(const|let)\s+PIPE_MATCH_M\b", text):
|
|
definitions.append(name)
|
|
assert definitions == ["design_selection.ts"]
|
|
for name, folder in (
|
|
("B05_Profile_UI_Structures_Panel.ts", "B05_Profile"),
|
|
("design_view_profile.ts", "A00_Common/design"),
|
|
("design_view_cross.ts", "A00_Common/design"),
|
|
("B05_Profile_UI_Selection.ts", "B05_Profile"),
|
|
("B06_Section_UI_Page_Structures_Panel.ts", "B06_Section"),
|
|
("B06_Section_UI_Page_Structure_Pick.ts", "B06_Section"),
|
|
("B05_Profile_UI_Structure_Pick_Session.ts", "B05_Profile"),
|
|
):
|
|
text = _read(folder, name)
|
|
assert not re.search(r"<\s*0\.05\b", text), name
|
|
assert not re.search(r"[-+]\s*0\.51\b", text), name
|
|
|
|
|
|
# ── 카드 누름 → 폼이 열 항목 ────────────────────────────────────────────
|
|
def test_카드_측점이_관에서_0_05보다_어긋나도_관을_찾는다(out):
|
|
assert out["card"]["pipeNear"] == {"pipeChainageM": 173.09, "structureId": None}
|
|
assert out["card"]["pipeFar"]["pipeChainageM"] is None # 0.51 m 밖은 다른 자리
|
|
|
|
|
|
def test_옹벽_기슭막이_카드는_구조물_폼을_연다(out):
|
|
for key in ("wallStart", "wallMiddle", "wallEdge"):
|
|
assert out["card"][key] == {"pipeChainageM": None, "structureId": "wall"}, key
|
|
assert out["card"]["revet"]["structureId"] == "revet"
|
|
|
|
|
|
def test_같은_측점에_관과_구조물이_겹치면_관이_먼저(out):
|
|
assert out["card"]["pipeBeatsStructure"]["pipeChainageM"] == 205.2
|
|
|
|
|
|
def test_구조물_id가_있으면_id가_이기고_없는_id는_측점으로_되돌아간다(out):
|
|
assert out["card"]["byId"]["structureId"] == "revet"
|
|
assert out["card"]["staleId"]["structureId"] == "wall"
|
|
|
|
|
|
def test_고를_것이_없으면_해제(out):
|
|
assert out["card"]["none"] == {"pipeChainageM": None, "structureId": None}
|
|
assert out["card"]["cleared"] == {"pipeChainageM": None, "structureId": None}
|
|
|
|
|
|
# ── 세션 값 + 알림 ─────────────────────────────────────────────────────
|
|
def test_세션에_적고_읽는다(out):
|
|
assert out["store1"] == {"chainageM": 205, "structureId": "wall", "partKey": "own"}
|
|
assert out["handoff"] == {"at": 205, "key": "right", "structureId": "wall"}
|
|
assert out["handoffNone"] is None
|
|
assert out["noProject"] is None
|
|
|
|
|
|
def test_부재키를_안_주면_지키고_해제하면_비운다(out):
|
|
assert out["keyKept"] == "own"
|
|
assert out["cleared"] is None
|
|
|
|
|
|
def test_같은_값은_알리지_않고_출처를_함께_알린다(out):
|
|
assert out["sameQuiet"] == 1
|
|
assert out["heardFrom"] == [[205, "wall", "b06"], [173.09, None, "b05"], [None, None, "b05"]]
|
|
assert out["afterOff"] == 3 # 구독을 끊으면 더는 안 들림
|
|
|
|
|
|
def test_알림_도중_다시_낸_값은_적기만_한다(out):
|
|
assert out["echoes"] == 1
|
|
assert out["lastWritten"] == 2
|
|
|
|
|
|
# ── B06 진입 때 카드 찾기 ──────────────────────────────────────────────
|
|
def test_관_카드는_거리_안에서_바로_찾는다(out):
|
|
assert out["apply"]["pipeOffsetCard"] == [["focus", "s173.09"]]
|
|
|
|
|
|
def test_옹벽은_구조물_id로_시작_카드를_찾는다(out):
|
|
assert out["apply"]["wallByAnchor"] == [["focus", "s200"]]
|
|
assert out["apply"]["wallNoId"] == [["focus", "s200"]]
|
|
|
|
|
|
def test_기슭막이_부재키는_그_카드의_조정창을_연다(out):
|
|
assert out["apply"]["revetCardFirst"] == [
|
|
["focus", "s80"],
|
|
["revet", 80, "own"],
|
|
["refresh", 80],
|
|
]
|
|
|
|
|
|
def test_카드를_못_찾으면_false로_알려_나중에_다시_부르게_한다(out):
|
|
assert out["retry"] == {
|
|
"before": False,
|
|
"after": True,
|
|
"notFound": False,
|
|
"emptyHandoff": False,
|
|
}
|
|
|
|
|
|
def test_모르는_자리_또는_값_없음은_아무것도_안_한다(out):
|
|
assert out["apply"]["unknown"] == []
|
|
assert out["apply"]["none"] == []
|
|
|
|
|
|
# ── B05 진입 때 되살리기 ───────────────────────────────────────────────
|
|
def test_B05_되살리기_관은_측점만으로_구조물을_짐작하지_않는다(out):
|
|
pipe = out["restore"]["pipeOnly"]
|
|
assert pipe["calls"] == [["3d", 85.05, None], ["station", 85.05]]
|
|
assert pipe["done"] is False # 관 목록이 실리면 다시 불린다
|
|
|
|
|
|
def test_B05_되살리기_구조물_id가_있으면_그_구조물을_연다(out):
|
|
structure = out["restore"]["structure"]
|
|
assert structure["calls"] == [
|
|
["3d", 205, "own"],
|
|
["station", 205],
|
|
["selectAt", 205, "wall"],
|
|
]
|
|
assert structure["done"] is True
|
|
|
|
|
|
def test_B05_되살리기_적힌_것이_없으면_끝난_것(out):
|
|
assert out["restore"]["nothing"] == {"calls": [], "done": True}
|
|
|
|
|
|
# ── 배선 ───────────────────────────────────────────────────────────────
|
|
def test_횡단도_벽_구체_눌러도_폼은_같은_길_하나():
|
|
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
|
assert "showPipeAt" not in panel # 관 전용 길을 없앰
|
|
assert panel.count("panel.showAtChainage(") == 3 # 기슭막이 · 세월교 · BOX
|
|
assert panel.count("panel.releasePipe()") == 3 # 벽 선택을 풀 때는 관 폼만 비움
|
|
assert "section.selectAt(" in panel
|
|
page = _read("A00_Common/design", "design_view_cross.ts")
|
|
assert "writeStructurePick" not in page
|
|
assert "selectDesign(" in page
|
|
|
|
|
|
def test_고름은_selectDesign_한_길로_적는다():
|
|
for folder, name in (
|
|
("A00_Common/design", "design_view_profile.ts"),
|
|
("B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts"),
|
|
("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts"),
|
|
):
|
|
text = _read(folder, name)
|
|
assert "writeStructurePick" not in text, name
|
|
assert "rememberStructurePick" not in text, name
|
|
assert "selectDesign(" in _read("B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts")
|