Files
Aislo/resources/tester/test_154_2_b08_direct_link.py
T
eomsangdonandClaude Opus 5.5 266d003ce7 feat(B08): 미연결 구조물 바로 연결 — 고르기 모달 → 그 칸만 저장 → 다시 셈(154-2)
- 새 서버 길 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
2026-10-09 19:11:16 +09:00

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