# -*- coding: utf-8 -*- """280-5 · 280-6 · 281 B07 좌측 ⚙ 설정 · 확정 단추 지움 · 저장 후 이동. 못박는 것 - ⚙ = 카드 제목(평면도 · 횡단도 · 구조물 · 전개도) · 한 장 단추 안(종단 1장 · 토적) · 표지 없음. - ⚙ 누름 = 카드 접힘 그대로 · 도면 안 엶(Enter 도) · 제목 누름 · 단추 누름은 그대로. - 모달 — 횡단 = 배치 칸(지금 장 값) + 표 배율 + 안내선 · 평면도 = 평면도 관리 모달 · 구조물 = 축척 없음 줄 · 머리 [원본에서 다시 그리기] = 그 묶음 도면을 연 때만 켬(표준 횡단면도 = 횡단 묶음). - 저장 묶음 — 고친 탭(●) + 지금 탭만 confirm · 고친 것 없으면 요청 0 · 실패 / 도각 편집 = 거짓(머묾) · 원본 되돌림 = 물음 → invalidate → 다시 실음 · [취소] = 그대로. - 페이지 — [현재 도면 확정] · [수정] · 자동 B08 이동 · 「모든 횡단 확정」 잠금 없음 · 읽기 전용 안 묶음 · ☰ · 단계 판 · [<] [>] = 저장 → (B08 · B09) 단계 4 완료 → 이동 · 탭 줄 겹판. - 서버 `design-drawings-complete` = 단계 4 완료. Node 헬퍼 `helper_280_5_b07_settings.cjs` 가 화면 그릇 흉내로 돌림. """ from __future__ import annotations import asyncio import json import os import re import subprocess from pathlib import Path from uuid import UUID import pytest import B07_DesignDetail.B07_DesignDetail_Router as router HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = Path(HERE).parents[1] B07 = ROOT / "B07_DesignDetail" def _read(path: Path) -> str: return path.read_text(encoding="utf-8").replace("\r\n", "\n") @pytest.fixture(scope="module") def out(): proc = subprocess.run( ["node", os.path.join(HERE, "helper_280_5_b07_settings.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_설정_단추_자리(out): assert out["cardGears"] == ["plan", "cross", "structure", "development"] assert out["cardGearInTitle"] is True assert out["buttonGears"] == ["longitudinal", "mass_haul"] assert out["buttonGearRole"] == [["SPAN", "button", 0]] * 2 # 단추 안 단추 금지 assert out["coverGear"] == 0 def test_설정_누름은_접힘_도면_열기_안_일으킴(out): assert out["collapsedSame"] is True assert out["selectedAfterGear"] == [] assert out["enterStops"] is True assert out["settingsCalls"] == [ "plan", "cross", "structure", "development", "longitudinal", "mass_haul", ] assert out["titleClickToggles"] is True and out["buttonClickOpens"] is True def test_횡단_모달은_배치_칸_표_배율_안내선(out): cross = out["crossModal"] assert cross["title"] == "횡단도 설정" assert cross["labels"] == ["배치 열", "행", "표 배율"] assert cross["values"] == ["3", "2", "1.2"] # 지금 장(1장) 값 · 표 배율 assert [b[0] for b in cross["buttons"]] == ["이 장", "모든 장", "배율 적용", "안내선 숨기기"] assert cross["guideLast"] is True assert cross["head"] == [["원본에서 다시 그리기", False]] assert cross["notes"] == ["지금 도면: cross_s1"] assert cross["scaleTitle"] == "축척 — 횡단도 장(표준 횡단면도 뺌)" saved = out["crossSave"] assert saved["closed"] is True assert saved["saved"] == [[{"default": [4, 4], "sheets": {"1": [2, 2]}, "table_scale": 1.2}, 1]] def test_구조물_종단_평면도_모달(out): structure = out["structureModal"] assert structure["title"] == "구조물 도면 설정" assert structure["notes"] == ["M02 그림 — 축척 없음"] assert structure["head"][0][1] is True # 다른 묶음 도면을 연 중 = 꺼짐 assert out["longModal"] == {"title": "종단도 설정", "bodyKids": 1} # 축척 칸(485-2) assert out["planModal"] == {"viaManager": True, "head": [["원본에서 다시 그리기", False]]} assert out["planRedraw"] == {"redraws": 1, "closed": True} assert out["standardInCross"] is True def test_저장은_고친_탭만_실패면_머묾(out): assert out["noEdits"] == {"ok": True, "calls": []} saved = out["saveEdited"] assert saved["ok"] is True assert saved["confirms"] == [ ["confirm", "cross_s1", "live", {"q": 1}], # 지금 탭 = CAD 에서 · 수량표 같이 ["confirm", "longitudinal", "stash", None], # 다른 고친 탭 = 받아 둔 편집본 ] assert saved["clean"] == [False, False, False] assert sorted(saved["confirmed"]) == [ "cross_s1", "longitudinal", ] # 안 고친 mass_haul 은 안 보냄 assert out["saveFail"] == {"ok": False, "toasts": ["error"]} assert out["frameEditing"] == {"ok": False, "calls": 0} def test_원본에서_다시_그리기(out): assert out["redrawable"] is True assert out["redraw"] == { "ok": True, "calls": ["ask", "invalidate", "forget", "reload"], "confirmed": False, } assert out["pristine"] is False # 원본 그대로 = 꺼짐 assert out["redrawCancel"] == {"ok": False, "invalidates": 0} def test_페이지_확정_단추_잠금_자동이동_없음(): page = _read(B07 / "B07_DesignDetail_UI_Page.ts") for gone in ( "현재 도면 확정", '"수정"', "all_confirmed", "allDrawingsConfirmed", "confirmDesignDrawing", "invalidateDesignDrawing", "crossLayoutSlot", ): assert gone not in page, gone assert "confirmed: false, // 잠금 없음(요구 281)" in page assert "actionRow.append(frameEditor.button);" in page assert "onStepClick: (stepIndex) => void moveToStage(stepIndex)," in page move = page[ page.index("const moveToStage = async") : page.index("const layout = createWorkflowLayout") ] assert move.index("saveEditedDrawings(store)") < move.index("completeDesignStage(projectId)") assert move.index("completeDesignStage(projectId)") < move.index("goToWorkflowStage(") assert "if (stepIndex > 4 && drawings.length > 0)" in move assert "currentStage: 5," in page assert len(page.splitlines()) <= 700 def test_탭_줄_겹판_약속(): page = _read(B07 / "B07_DesignDetail_UI_Page.ts") assert 'cad.element.style.getPropertyValue("--cad-tab-slot-left") !== ""' in page assert "if (slotted) cad.element.append(tabs.element);" in page assert "else cadStack.prepend(tabs.element);" in page css = _read(B07 / "B07_DesignDetail_UI_Style.css") block = css[css.index(".b07-cad-stack .cad-host > .cad-tabs {") :] block = block[: block.index("}")] assert "right: var(--cad-tab-slot-right, 0px);" in block assert "left: var(--cad-tab-slot-left, 0px);" in block assert re.search(r"height:\s*26px", block) class _Cursor: async def __aenter__(self) -> _Cursor: return self async def __aexit__(self, *_: object) -> bool: return False class _Connection: async def begin(self) -> None: ... async def commit(self) -> None: ... async def rollback(self) -> None: ... def cursor(self) -> _Cursor: return _Cursor() class _Acquire: async def __aenter__(self) -> _Connection: return _Connection() async def __aexit__(self, *_: object) -> bool: return False def test_서버_단계_완료(monkeypatch, tmp_path: Path) -> None: stages: list[tuple[str, int]] = [] async def source(_project_id: UUID) -> tuple[int, Path, Path, bool]: return 1, tmp_path, tmp_path / "longitudinal.json", True async def complete(_cursor: object, project_id: str, stage_no: int) -> None: stages.append((project_id, stage_no)) monkeypatch.setattr(router, "_confirmed_source", source) monkeypatch.setattr(router, "complete_stage", complete) monkeypatch.setattr( router, "get_db_pool", lambda: type("P", (), {"acquire": lambda self: _Acquire()})() ) project = UUID("8b98ca75-c572-4002-8468-2adae4323414") result = asyncio.run(router.complete_design_stage(project)) assert result == {"status": "success", "project_id": str(project)} assert stages == [(str(project), 4)] async def blocked(_project_id: UUID) -> tuple[int, Path, Path, bool]: raise PermissionError("B06 종·횡단 확정 후 상세 설계를 진행할 수 있습니다.") monkeypatch.setattr(router, "_confirmed_source", blocked) assert asyncio.run(router.complete_design_stage(project)).status_code == 409 RANGE = "1/100~1/10,000 사이 정수" def test_485_2_축척_칸_판정(out): assert out["readAxis"] == [1000, RANGE, RANGE, RANGE, RANGE, RANGE, None] # 자동 = null assert out["toSend"] == [None, 500, None] # 기본과 같으면 null def test_485_2_종단_축척_저장(out): scale = out["scale"] assert scale["longLabels"] == ["X(가로) 1/", "Y(세로) 1/"] assert scale["longStart"]["values"] == ["1000", "200"] assert scale["longStart"]["autos"] == [] # 종단 = 자동 없음 for bad in ("longLow", "longFrac"): # 범위 밖 · 소수 = 붉음 + 저장 꺼짐 assert scale[bad]["errors"] == [RANGE, None] assert scale[bad]["save"] is False assert scale["longOk"]["errors"] == [None, None] and scale["longOk"]["save"] is True # 저장본 2장 — 물음 [취소] = 요청 없음 · 모달 그대로 assert scale["cancel"] == {"calls": ["ask"], "open": True, "closed": 0} save = scale["save"] assert "저장본 2장" in save["ask"] assert save["sent"] == {"longitudinal": {"x": 500, "y": None}} # Y = 기본 → null assert save["open"] is False assert save["closed"] == [[True, "longitudinal"]] # 화면 다시 세움 · 같은 도면 assert scale["reset"]["values"] == ["1000", "200"] # [기본값] = 칸만 assert scale["same"] == {"calls": 0, "toasts": ["바뀐 축척이 없습니다."]} def test_485_2_토적_전개도_자동(out): scale = out["scale"] assert scale["massStart"] == { "values": ["", "50000"], "errors": [None, None], "disabled": [True, False], "autos": [True], "save": True, } assert scale["massManual"]["errors"][0] and scale["massManual"]["save"] is False # 빈 칸 assert scale["massTyped"]["save"] is True assert scale["massSent"] == {"ask": 0, "sent": {"mass_haul": {"x": 2000, "y": None}}} assert scale["massBack"]["autos"] == [True] and scale["massBack"]["save"] is True assert scale["dev"]["autos"] == [True, True] and scale["dev"]["disabled"] == [True, True] assert scale["structure"] == 0 # 구조물 = 축척 칸 없음