# -*- coding: utf-8 -*- """B08 미연결 구조물 바로 연결(PLAN 154-2) — 임시 저장소. - 서버 `link` = 그 시설 조합 칸 하나만 정본에 씀(구조물 판번호 +1 · 관 지점 노선 지문 그대로) - 같은 측점 박스 + 관 = `facility` 로 가름 · 그룹 칸 = `group_items[gid]` · None = 풀기 - 저장 뒤 사본 맞춤 → 사본 폴더 생김 → 집계 응답 `unlinked` 에서 빠짐 - 길 차례(`{폴더}/{종류}` 보다 먼저) · 화면 = 초안 있으면 막음 · 고르기 모달 · 다시 그림 """ from __future__ import annotations import json import shutil from pathlib import Path from typing import Any import pytest from B08_Quantity import B08_Quantity_Router_Summary as summary from config import config_system from M02_MasterTemplete import M02_Item_Copy as copies from M02_MasterTemplete import M02_Item_Snapshot as snap from M02_MasterTemplete import M02_Template_Layers as layers from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition from resources.tester.helper_m02_master_sample import SAMPLE ROOT = Path(__file__).resolve().parents[2] PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"} RV = {"layer": "system", "owner": None, "key": "SS000001", "name": "돌기슭막이 메쌓기 1.2m"} MS = {"layer": "system", "owner": None, "key": "SS000022", "name": "돌쌓기"} POINTS = [ {"chainage_m": 100.0, "source": "user", "options": {"pipe_diameter_mm": 800}, "group_items": {"grv6lny": PIPE}}, {"chainage_m": 100.0, "source": "user", "facility": "box_culvert", "options": {}}, {"chainage_m": 200.0, "source": "user", "options": {"pipe_diameter_mm": 800}}, ] # fmt: skip STRUCTURES = [ {"structure_id": "st0", "type_id": "open_ditch", "placement": "point", "chainage_m": 300}, {"structure_id": "st1", "type_id": "open_ditch", "placement": "point", "chainage_m": 400, "summary_item": MS}, ] # fmt: skip @pytest.fixture def root(tmp_path: Path, monkeypatch) -> Path: system = tmp_path / "master" system.mkdir() shutil.copy2(SAMPLE / layers.COMBO_FILE, system / layers.COMBO_FILE) for kind in ("table", "structure", "area"): shutil.copytree(SAMPLE / kind, system / kind) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) storage = tmp_path / "storage" monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) found = storage / "3" / "12" / "proj" edits = found / "B04_PreProcess/drainage/edits" edits.mkdir(parents=True) (edits / "pipe_points.json").write_text( json.dumps({"route_signature": "sig-1", "points": POINTS}, ensure_ascii=False), encoding="utf-8", ) route = found / "B05_Profile/route" route.mkdir(parents=True) (route / "structures.json").write_text( json.dumps({"revision": 4, "structures": STRUCTURES}, ensure_ascii=False), encoding="utf-8", ) snap.sync_snapshots(found) return found def _points(root: Path) -> dict[str, Any]: path = root / "B04_PreProcess/drainage/edits/pipe_points.json" return json.loads(path.read_text(encoding="utf-8")) def _structures(root: Path) -> dict[str, Any]: path = root / "B05_Profile/route/structures.json" return json.loads(path.read_text(encoding="utf-8")) def test_관_지점_그_칸만_박스_관_가름(root): before = _points(root) copies.link(root, "pipe@100.0", "box_culvert", RV) after = _points(root) assert after["route_signature"] == "sig-1" assert after["points"][1]["summary_item"] == RV # 박스만 assert after["points"][0] == before["points"][0] # 같은 측점 관 그대로 assert after["points"][2] == before["points"][2] copies.link(root, "pipe@100.0__g9lq9uk", "pipe", RV) pipe = _points(root)["points"][0] assert pipe["group_items"] == {"grv6lny": PIPE, "g9lq9uk": RV} # 다른 그룹 그대로 assert "summary_item" not in pipe copies.link(root, "pipe@100.0__g9lq9uk", "pipe", None) # 풀기 assert _points(root)["points"][0]["group_items"] == {"grv6lny": PIPE} copies.link(root, "pipe@100.0", "box_culvert", None) assert "summary_item" not in _points(root)["points"][1] def test_구조물_그_칸만_판번호(root): copies.link(root, "st0", None, RV) got = _structures(root) assert got["revision"] == 5 st0, st1 = got["structures"] assert st0["summary_item"] == RV and st0["chainage_m"] == 300 assert st1["summary_item"] == MS # 다른 구조물 그대로 copies.link(root, "st1__gA", None, RV) got = _structures(root) assert got["revision"] == 6 assert got["structures"][1]["group_items"] == {"gA": RV} assert got["structures"][1]["summary_item"] == MS def test_못_잇는_것(root): with pytest.raises(LookupError): copies.link(root, "pipe@999.0", "pipe", RV) with pytest.raises(LookupError): copies.link(root, "pipe@100.0", "ford_bridge", RV) with pytest.raises(LookupError): copies.link(root, "nope", None, RV) with pytest.raises(ValueError): copies.link(root, "#0", None, RV) def test_저장_뒤_사본_생기고_미연결에서_빠짐(root): names = definition.read()["종류"]["pipe"]["이름"] designs = [{"chainage_m": 200.0, "design": {"pipe_length_m": 12}}] first = summary.pipe_summary(root, designs, names, 6.0) assert any(u["id"] == "st0" for u in first["unlinked"]) assert any(u["id"] == "pipe@200.0" for u in first["unlinked"]) copies.link(root, "st0", None, RV) copies.link(root, "pipe@200.0", "pipe", PIPE) assert snap.safe_sync(root) == [] assert (snap.structures_dir(root) / snap.folder_name("st0") / snap.MANIFEST_NAME).is_file() linked = snap.linked_snapshots(root) assert {"st0", "pipe@200.0"} <= set(linked) after = summary.pipe_summary(root, designs, names, 6.0) assert not any(u["id"] == "st0" for u in after["unlinked"]) pipe200 = next((u for u in after["unlinked"] if u["id"] == "pipe@200.0"), None) assert pipe200 is None or pipe200["slots"][0]["linked"] # 본체는 연결됨 def test_길_차례_링크가_먼저(): from M02_MasterTemplete.M02_Project_Items_Router import router paths = [(route.path, sorted(route.methods)) for route in router.routes] link = paths.index(("/api/projects/{project_id}/items/{copy}/link", ["PUT"])) write = paths.index(("/api/projects/{project_id}/items/{folder}/{kind}", ["PUT"])) assert link < write def test_화면_초안이면_막고_고르기_모달_다시_그림(): ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8") assert "readPendingStructures(projectId) !== null" in ui assert "readPendingPipes(projectId) !== null" in ui assert 'import("@ui/ui_template_item_picker")' in ui assert ".link(slot.copy, facility.facility, item)" in ui api = (ROOT / "ui_template/ui_template_item_picker_project.ts").read_text(encoding="utf-8") assert "/link`" in api page = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") assert "host.replaceWith(fresh)" in page and "unlinkedAt(unlinked, station)" in page