Files
Aislo/resources/tester/test_318_cross_card.py
T
eomsangdonandClaude Opus 5.5 2641c15e29 fix(b05·b06): 정본 다른 종류(관 ↔ 맹암거 · 개거 등)로 바꿈 = 옛 것 지우고 같은 측점에 새 종류 · 일반 측점 자리 종류 바꿈 · 지움 = 구조물만 뺀 사본에 새 종류 · 중심고 상자에 겹친 Y 눈금 숨기지 않음(상자 위 · 안 비치는 바탕) (319 · 요구 217)
- 공용 구조물 패널 종류 바꿈: 관 지점 ↔ 구조물이면 새로 놓기 폼으로 빠지던 것 → 옛 것 지움 → 우클릭 추가 길(addAt)로 같은 측점 · 옛 측점 조정값 버림(318-4)
- 횡단 초안 맞춤: 일반 측점 자리(grid)는 종류가 바뀌어도 저장분(옛 관 그림)이 남던 것 → 구조물만 뺀 사본(지형 쪽 고친 값 그대로) + 새 종류
- 시험: test_319_kind_swap 8 · test_318 중심고 겹침 기대값 바꿈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
2026-10-14 06:52:02 +09:00

296 lines
12 KiB
Python

"""PLAN 318 — B06 횡단 카드(요구 215 · 216).
- 318-1 단추 줄: 빈자리 누름 = 카드 고름 · 단추 · ⋯ 펼침 누름 = 카드로 안 넘김
- 318-2 중심고: 아래 띠에서 빼고 Y축 라벨 둥근 상자(눈금과 같은 글자 · 소수 첫째 자리 ·
축선 안 침범) · 암 경계 = 띠 왼쪽
- 318-3 아래 띠 아래 여유: 카드 = 내용 높이(같은 줄 큰 카드에 안 늘어남) · 빈 띠도 높이 그대로
- 318-4 관 종류 바꿈: 그 측점 옛 조정값(세션 · 미리 뜬 횡단 사본)을 버리고
[저장] 때 정본에서도 비움
화면 TS 는 `helper_318_cross_card.cjs` 가 Node 로 돌린다(무거운 화면 사슬 · 서버 부름은 흉내).
"""
from __future__ import annotations
import asyncio
import json
import os
import re
import subprocess
import pytest
from B06_Section import B06_Section_Router_Confirm as confirm
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
from B06_Section.B06_Section_Router_Design import STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS
from B06_Section.B06_Section_Schema import SectionConfirmRequest
HERE = os.path.dirname(__file__)
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
NODE_READY = os.path.isdir(os.path.join(ROOT, "config", "node_modules"))
def _src(*parts: str) -> str:
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
return handle.read()
@pytest.fixture(scope="module")
def result() -> dict:
if not NODE_READY:
pytest.skip("프론트엔드 의존성 미설치")
out = subprocess.run(
["node", os.path.join(HERE, "helper_318_cross_card.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
cwd=ROOT,
timeout=120,
)
assert out.returncode == 0, out.stderr
return json.loads(out.stdout)
# ── 318-1 단추 줄 ─────────────────────────────────────────────────────────
def test_bar_empty_area_picks_card(result: dict) -> None:
bar = result["bar"]
assert bar["empty"] == 1 # 단추 줄 빈자리
assert bar["legend"] == 1 # 단추 묶음 바탕(단추 아닌 자리)
def test_bar_controls_do_not_pick_card(result: dict) -> None:
assert result["bar"]["button"] == 0
assert result["bar"]["summary"] == 0 # ⋯ 펼침
# ── 318-2 중심고 Y축 상자 ──────────────────────────────────────────────────
def test_center_tag_on_y_axis(result: dict) -> None:
center = result["center"]
box = center["box"]
assert center["text"] == "876.6" # 소수 첫째 자리
assert "b06-chart__tick" in center["text_classes"] # 다른 Y 라벨과 같은 글자 크기
assert box["rx"] > 0 # 둥근 상자
assert box["x"] + box["width"] < center["axis_x"] # Y 축선 안 침범
assert box["x"] > 0
assert abs(box["y"] + box["height"] / 2 - center["center_y"]) < 0.01 # 중심고 높이에
assert center["text_x"] == center["tick_x"] # 글 오른끝 = 다른 눈금 자리
assert center["tick_near_center"] >= 1 # 겹친 눈금 글도 그대로(319-1)
assert center["on_top"] == [ # 상자 · 글을 맨 뒤에 붙여 위에서 가림(319-1)
"b06-chart__center-box",
"b06-chart__tick b06-chart__center-text",
]
assert center["tick_count"] > 3
assert center["hidden_when_out_of_view"]
assert center["absent_without_center"]
def test_footer_without_center_text(result: dict) -> None:
footer = result["footer"]
assert "중심고" not in footer["rock_text"]
assert "876" not in footer["rock_text"]
assert footer["rock_first"].split()[-1] == "b06-cross-card__rockb" # 암 경계 = 왼쪽 첫 칸
assert footer["rock_classes"][-1].split()[-1] == "b06-cross-card__cutslope"
assert footer["soil_exists"] and footer["soil_children"] == 0 # 토사도 띠는 남음(빈 띠)
def test_center_wired_into_card_and_modal() -> None:
assert "centerZ: section.center_z ?? null" in _src(
"B06_Section", "B06_Section_UI_Cross_View.ts"
)
assert "centerZ: section.center_z ?? null" in _src(
"B06_Section", "B06_Section_UI_Section_View.ts"
)
# ── 318-2 · 318-3 양식 ──────────────────────────────────────────────────────
def _rule(css: str, selector: str) -> str:
match = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css)
assert match, selector
return match.group(1)
def test_css_layout() -> None:
css = _src("B06_Section", "B06_Section_UI_Style_Cross.css")
assert "align-items: start" in _rule(css, ".b06-section__grid") # 카드 = 내용 높이
rock = _rule(css, ".b06-cross-card__rockb")
assert "position: absolute" not in rock and "translateX" not in rock # 왼쪽 맞춤
assert 'content: "\\200b"' in _rule(css, ".b06-cross-card > footer:empty::before")
box = _rule(css, ".b06-chart__center-box")
assert "var(--color-accent)" in box # 테마 색
assert "font-size" not in _rule(css, ".b06-chart__center-text") # 글자 크기 = 눈금 그대로
# ── 318-4 관 종류 바꿈 ─────────────────────────────────────────────────────
def test_forget_session_adjusts_at_station(result: dict) -> None:
forget = result["forget"]
assert forget["culvertopt"] == {"140.00": {"a": 1}}
assert forget["culvertopt_other_route"] == {} # 노선 모두
assert forget["culvertopt_other_project"] == {"100.00": {"a": 1}} # 다른 프로젝트 그대로
assert forget["revetx"] == {"140.00:inlet": {"x": 3}}
assert forget["extrawall"] == {"140.00": 1} # `b` 머리(집수정 단)까지
assert forget["extraspan"] == {}
assert forget["revetlink"] == {"detached": {"140.00": True}, "grade": {}}
assert forget["fordadjust"] == {} and forget["boxadjust"] == {} # 100.30 = 같은 관(0.51 안)
assert forget["basinadjust"] == {} and forget["inletstruct"] == {}
assert forget["crossw"] == {"100.00": 5} # 구조물 아닌 값(표시 반폭)은 그대로
assert forget["reloads"] == 1 # 횡단 보기 메모리 사본 다시 읽음
assert forget["preview_calls"][0] == ["P", 100, list(STRUCTURE_ADJUST_KEYS)]
def test_kind_reset_marks(result: dict) -> None:
forget = result["forget"]
assert forget["kindreset"] == {"100.00": "pipe"}
assert forget["changed"] == [100]
assert forget["second"] == {"100.00": "pipe"} # 처음 종류를 기억
assert forget["reverted"] is None # 처음 종류로 되돌림 = 저장분 그대로
assert forget["same_kind_untouched"]
def test_preview_copy_stripped(result: dict) -> None:
assert result["preview"]["had"] == ["ford_adjust", "ground_type", "revet_adjust"]
assert result["preview"]["after"] == ["ground_type"]
def test_b05_save_path_patches(result: dict) -> None:
assert result["cache_patches"] == [
{"chainage_m": 100, "structure_reset": True},
{"chainage_m": 100, "display_half_width_m": 6}, # 옛 ford_adjust 안 실음
{"chainage_m": 140, "ford_adjust": {"w": 2}},
]
assert result["cache_patches_plain"] == [{"chainage_m": 140, "ford_adjust": {"w": 2}}]
def test_wiring_sources() -> None:
events = _src("B05_Profile", "B05_Profile_UI_Structures_Panel_Events.ts")
live = events.index("ctx.liveCommit();\n // 관 지점 종류 바꿈")
assert "forgetStationAdjusts(ctx.projectId, pipeItem.chainage_m, prev.type_id" in events[live:]
persist = _src("B06_Section", "B06_Section_UI_Page_Persist.ts")
assert "patchFor(at).structure_reset = true" in persist
assert 'clearState("kindreset", ctx.projectId)' in persist
store = _src("B06_Section", "B06_Section_Section_Store.ts")
assert "crossPatchesFromCache(detail, kindChangedChainages(projectId))" in store
view = _src("A00_Common", "design", "design_view_cross.ts")
assert "setStationAdjustReload(() => stationControls.load())" in view
def test_structure_keys_mirror() -> None:
"""서버 · 브라우저 열쇠 목록이 같음 · 모두 승계 목록 안."""
ts = _src("B06_Section", "B06_Section_Api_Culvert_Options.ts")
block = ts[ts.index("export const STRUCTURE_ADJUST_KEYS = [") :]
block = block[: block.index("] as const")]
assert re.findall(r'"([a-z_]+)"', block) == list(STRUCTURE_ADJUST_KEYS)
assert set(STRUCTURE_ADJUST_KEYS) <= set(USER_TOUCHED_KEYS)
assert "structure_note" not in STRUCTURE_ADJUST_KEYS # 사용자 글 · 그림은 안 버림
# ── 318-4 서버 — 비움이 먼저 · 같은 행은 이어 얹음 · None = 지움 ───────────────
class _Cursor:
def __init__(self, rows):
self.rows = rows
self.calls = []
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
async def execute(self, sql, params=None):
self.calls.append((" ".join(sql.split()), list(params) if params else []))
async def fetchall(self):
return self.rows
class _Connection:
def __init__(self, rows):
self.cursor_obj = _Cursor(rows)
def cursor(self):
return self.cursor_obj
def _written(connection: _Connection) -> dict:
for sql, params in connection.cursor_obj.calls:
if sql.startswith("UPDATE cross_sections SET data = CASE"):
count = len(params) // 3
return {params[i * 2]: json.loads(params[i * 2 + 1]) for i in range(count)}
return {}
def test_merge_none_deletes_and_same_row_accumulates() -> None:
stored = {
"design": {
"ground_type": "soil",
"ford_adjust": {"w": 1},
"revet_adjust": {"inlet": {"x": 1}},
"display_half_width_m": 6,
}
}
connection = _Connection([(7, 100.0, json.dumps(stored))])
asyncio.run(
merge_cross_section_designs(
connection,
route_id=1,
entries=[
(100.0, dict.fromkeys(STRUCTURE_ADJUST_KEYS)),
(100.0, {"box_adjust": {"w": 2}}),
],
replace=False,
)
)
written = _written(connection)
assert list(written) == [7] # 한 행 한 벌
assert written[7]["design"] == {
"ground_type": "soil",
"display_half_width_m": 6,
"box_adjust": {"w": 2},
}
def test_merge_none_only_entry_for_missing_row_inserts_nothing() -> None:
connection = _Connection([])
asyncio.run(
merge_cross_section_designs(
connection,
route_id=1,
entries=[(50.0, dict.fromkeys(STRUCTURE_ADJUST_KEYS))],
replace=False,
project_id="00000000-0000-0000-0000-000000000001",
)
)
assert not any("INSERT" in sql for sql, _ in connection.cursor_obj.calls)
def test_apply_edits_resets_before_patches(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[list, bool]] = []
async def fake_merge(connection, *, route_id, entries, replace, project_id=None):
calls.append((list(entries), replace))
return len(entries)
monkeypatch.setattr(confirm, "merge_cross_section_designs", fake_merge)
request = SectionConfirmRequest(
cross_patches=[
{"chainage_m": 100, "structure_reset": True, "box_adjust": {"left": {"lengthM": 1}}},
{"chainage_m": 140, "display_half_width_m": 6},
]
)
asyncio.run(confirm._apply_section_edits(None, 1, request, []))
entries = [entries for entries, replace in calls if not replace][-1]
assert entries[0] == (100, dict.fromkeys(STRUCTURE_ADJUST_KEYS)) # 비움이 먼저
rest = dict(entries[1:])
assert "structure_reset" not in rest[100] # design 에 안 씀
assert rest[100]["box_adjust"]["left"]["lengthM"] == 1 # 새 값은 뒤에 얹힘
assert rest[140] == {"display_half_width_m": 6}