- 새 서버 길 PUT /api/projects/{id}/items/{사본 id}/link — 그 시설 조합 칸 하나만 정본에
(관 지점 = pipe_points.json 노선 지문 그대로 · 박스/관 facility 로 가름 · 구조물 = 판번호 +1) → safe_sync
- 구조물도 모달 맨 위 「조합 연결」 칸(새 B08_Quantity_UI_Link) — 칸마다 연결 여부 · [선택]
- B05/B06 세션 초안(구조물 · 관) 있으면 막고 「B05 · B06 [저장] 먼저」 알림 · 저장 뒤 탭 몸통 새로 그림
- 공용 창구 projectItems().link 추가 · 새 시험 test_154_2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
162 lines
7.0 KiB
Python
162 lines
7.0 KiB
Python
# -*- 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
|