- 마스터 층 구조물 항목 51개 지움 — 나무 지우기(tree.apply delete) → 집계표 저장(store.write) 한 줄기 · 집계표 구조물 열 · SS 장부 줄 · structure/area/material 파일 153개 · 남김 = no · sta · 비고 열 · 변수 · 쪽줄 · 합계줄 · 바인딩규칙 · 자재조합 · 구조물옵션표시 · drawing · M01 · 회사 · 개인 층 · 프로젝트 복사본 · 지운 열의 열너비만 뺌 - 키 · 번호 다시 안 씀 — 장부 다음 52(새 항목 SS000052) · 지운번호 51개 기록(새 번호는 SD-000001 모양이라 옛 8자리와 안 겹침) - 지우기 전 사본 tmp/마스터_집계표_초기화전_0930/(git 밖 · 156 파일 = 지우기 전과 같음) - 시험 안 견본 fixtures/m02_master_sample(옛 집계표 · 장부 · 자재조합 · rv · c4 · pp_len · ms 세 파일 · 도면 줄임) + helper_m02_master_sample — 실제 마스터를 읽던 시험 일곱(table_fill · template_layers · structures_summary_snapshot · item_copy_complete · m02_tree · m02_item_inputs · spreadsheet_saved_values)을 견본으로 · M01 쓰는 곳 찾기(users_of) 시험은 이미 임시 표(combo_inputs)라 그대로 - 새 시험 test_m02_empty_master — 빈 마스터에서 항목 0 · 빈 나무 · 첫 화면 길 · 새 폴더 · 새 항목(SS000052 · SD-000001) · 지우기 · 고르기 모달 마스터 0 · 프로젝트 복사본 calc 그대로 · 원본없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
543 lines
25 KiB
Python
543 lines
25 KiB
Python
"""프로젝트 집계 항목 복사본 완전체 (PLAN 25-2b · 25-2e · 26-2b · 임시 저장소 · 임시 마스터 사본).
|
|
|
|
복사본은 구조물마다(`templates/structures/{구조물 id}/` · 관은 `pipe@측점`).
|
|
|
|
- 지정 → [저장] → 폴더에 새 파일(입력연결 · 입력값 · 고른자재 · 단가) · manifest 가져온날 · 원본판
|
|
- 단가 묶음이 닫혀 있음 — 조합 37개 · 로직 170개를 마스터와 복사본으로 돌려 같은 값
|
|
- 마스터 단가 한 줄을 바꿔도 프로젝트 계산 그대로 · 목록에 「마스터 바뀜」 · 새로 가져오기 뒤 바뀜
|
|
(입력값 · 고른자재는 같은 키면 살림)
|
|
- 쓰기 뒤 다시 읽기 같음 · 판 다르면 409 · 모양 틀리면 400 · 고친 복사본은 안 지움
|
|
- 옛 복사본은 읽을 때 채움 · 미리 가져오기 · 남의 프로젝트 403
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from common_util.common_util_auth import verify_session
|
|
from config import config_system
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Item_Copy as copies
|
|
from M02_MasterTemplete import M02_Item_Prices as prices
|
|
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
|
from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_routes
|
|
from M02_MasterTemplete import M02_Project_Items_Router as items_router
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
from resources.tester.helper_m02_master_sample import SAMPLE
|
|
|
|
REAL = SAMPLE # 시험 안 견본(PLAN 26-5a) — 실제 마스터 안 읽음
|
|
MASTER = config_system.PROJECT_ROOT / "resources/master_data"
|
|
PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"}
|
|
FOLDER = "pipe@40.0" # 관 지점 복사본 폴더 = 구조물 id(26-2b)
|
|
PIPE_PATH = ["관공", "(관종·관경마다)", "관매설"]
|
|
LABOR_PRICE = "LB000002" # 보통인부 — 되메우기 · 파형강관 부설 호표가 씀
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def master_copy(tmp_path_factory) -> Path:
|
|
target = tmp_path_factory.mktemp("master_data")
|
|
for path in MASTER.glob("*.json"):
|
|
shutil.copy2(path, target / path.name)
|
|
return target
|
|
|
|
|
|
@pytest.fixture
|
|
def world(tmp_path: Path, monkeypatch, master_copy: Path) -> dict[str, Any]:
|
|
system = tmp_path / "master"
|
|
system.mkdir()
|
|
for name in (layers.COMBO_FILE, "자재조합.json"):
|
|
shutil.copy2(REAL / name, system / name)
|
|
for kind in ("table", "structure", "area", "material"):
|
|
shutil.copytree(REAL / kind, system / kind)
|
|
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
|
monkeypatch.setattr(m01, "FOLDER", master_copy)
|
|
storage = tmp_path / "storage"
|
|
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
|
|
root = storage / "3" / "12" / "proj"
|
|
(root / "B05_Profile/route").mkdir(parents=True)
|
|
(root / "B04_PreProcess/drainage/edits").mkdir(parents=True)
|
|
return {"system": system, "root": root, "master": master_copy}
|
|
|
|
|
|
def _save(root: Path, pipes: list[dict]) -> dict[str, list[str]]:
|
|
(root / "B05_Profile/route/structures.json").write_text(
|
|
json.dumps({"revision": 1, "structures": []}), encoding="utf-8"
|
|
)
|
|
(root / "B04_PreProcess/drainage/edits/pipe_points.json").write_text(
|
|
json.dumps({"points": pipes}, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
return snap.sync_snapshots(root)
|
|
|
|
|
|
def _pipe(summary: dict | None = PIPE) -> dict[str, Any]:
|
|
return {"chainage_m": 40.0, "source": "user", "summary_item": summary}
|
|
|
|
|
|
def _read(path: Path) -> Any:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _auto_values(root: Path) -> dict[str, Any]:
|
|
"""담은 로직마다 자동값을 확인한 값으로 — 사용자가 다 채운 것과 같게."""
|
|
labor = _read(root / "templates/structures" / FOLDER / "일위대가.json")
|
|
_, whole = m01.loaded()
|
|
out: dict[str, Any] = {}
|
|
for row in labor["로직"]:
|
|
values = m01.mau.values(whole, whole.logic(row["키"]))
|
|
out[row["키"]] = {k: {"값": prices.to_json(v), "확인": True} for k, v in values.items()}
|
|
return out
|
|
|
|
|
|
def _fill_values(root: Path) -> dict[str, Any]:
|
|
current = copies.read_file(root, FOLDER, "입력값")
|
|
values = _auto_values(root)
|
|
copies.write_file(root, FOLDER, "입력값", current["판"], values)
|
|
return values
|
|
|
|
|
|
# ── 새 파일 ──────────────────────────────────────────
|
|
|
|
|
|
def test_지정하면_새_파일까지_복사(world):
|
|
root = world["root"]
|
|
assert _save(root, [_pipe()])["만듦"] == [FOLDER]
|
|
folder = root / "templates/structures" / FOLDER
|
|
assert _read(folder / "입력연결.json") == {"판": None, "줄": []} # 층 연결이 아직 없음
|
|
assert _read(folder / "입력값.json") == {} and _read(folder / "고른자재.json") == {}
|
|
price = _read(folder / "단가.json")
|
|
rows = {k: len(v.get("줄") or v.get("표")) for k, v in price.items()}
|
|
assert rows["인력.json"] >= 2 and rows["재료_자재품목.json"] > 0 and "_빠진" not in price
|
|
keys = {row["키"] for data in price.values() for row in data.get("줄") or data.get("표")}
|
|
assert {"GF000176", "GC000088", "GF000113", "GC000267", LABOR_PRICE} <= keys
|
|
manifest = _read(folder / "manifest.json")
|
|
assert manifest["가져온날"] and len(manifest["원본판"]) == 16
|
|
assert copies.list_items(root) == [
|
|
{
|
|
"폴더": FOLDER,
|
|
"이름": "관공 관매설",
|
|
"원본": {**PIPE, "경로": PIPE_PATH},
|
|
"원본판": manifest["원본판"],
|
|
"가져온날": manifest["가져온날"],
|
|
"마스터바뀜": False,
|
|
"원본없음": False,
|
|
"구조물": ["pipe@40.0"],
|
|
"고침": False,
|
|
"값있음": False,
|
|
}
|
|
]
|
|
|
|
|
|
def test_층_입력연결을_복사(world):
|
|
root, system = world["root"], world["system"]
|
|
(system / "inputs").mkdir()
|
|
link = {"판": "a1", "줄": [{"로직": "GF000176", "입력": "관경", "자리": "면적", "알약": "x"}]}
|
|
column = next(
|
|
r["열"] for r in _read(system / layers.COMBO_FILE)["줄"] if r["키"] == PIPE["key"]
|
|
)
|
|
(system / "inputs" / f"{column}.json").write_text(json.dumps(link, ensure_ascii=False), "utf-8")
|
|
_save(root, [_pipe()])
|
|
assert copies.read_file(root, FOLDER, "입력연결")["내용"] == link
|
|
|
|
|
|
def test_단가_묶음은_닫혀_있음(world):
|
|
"""조합마다 복사본 단가 묶음만으로 돌린 값 = 마스터로 돌린 값(자동값 · 고르기 끝값)."""
|
|
files, whole = prices.master()
|
|
combos = [r for d in files.values() if d.get("그룹") == "일위대가조합" for r in d["줄"]]
|
|
runs = 0
|
|
for ua in combos:
|
|
keys = prices.mcb.logic_keys(ua)
|
|
snapshot = prices.load_prices(prices.to_json(prices.collect(keys)))
|
|
for key in keys:
|
|
try:
|
|
row = whole.logic(key)
|
|
except prices.mf.FormulaError:
|
|
continue
|
|
for pick in (0, -1):
|
|
values = m01.mau.values(whole, row)
|
|
for spec in row.get("입력", []):
|
|
if spec.get("고르기"):
|
|
values[spec["이름"]] = spec["고르기"][pick]
|
|
assert _run(whole, key, values) == _run(snapshot, key, values), (ua["키"], key)
|
|
runs += 1
|
|
assert len(combos) >= 30 and runs >= 300
|
|
|
|
|
|
def _run(master: Any, key: str, values: dict) -> str:
|
|
try:
|
|
got = prices.mf.run(master, key, dict(values))
|
|
except Exception as error: # 까닭도 같아야 함
|
|
return f"멈춤 {error}"
|
|
return str(got.get("계", got.get("결과")))
|
|
|
|
|
|
# ── 계산 · 마스터 바뀜 · 새로 가져오기 ───────────────────
|
|
|
|
|
|
def test_입력이_없으면_빠진_입력_목록(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
got = copies.calc(root, FOLDER)
|
|
assert got["계"] == 0 and len(got["멈춤"]) == 3
|
|
assert {"로직": "GF000176", "입력": "관경"} in got["빠진입력"]
|
|
|
|
|
|
def test_마스터_단가를_바꿔도_계산_그대로_새로_가져오면_바뀜(world):
|
|
root, master_dir = world["root"], world["master"]
|
|
_save(root, [_pipe()])
|
|
values = _fill_values(root)
|
|
before = copies.calc(root, FOLDER)
|
|
assert before["빠진입력"] == [] and before["멈춤"] == [] and before["계"] > 0
|
|
files, whole = m01.loaded()
|
|
ua = next(
|
|
r
|
|
for d in files.values()
|
|
if d.get("그룹") == "일위대가조합"
|
|
for r in d["줄"]
|
|
if r["키"] == "UA000036"
|
|
)
|
|
given = {k: {n: v["값"] for n, v in one.items()} for k, one in values.items()}
|
|
expected = prices.to_json(prices.mcb.run(whole, ua, given))
|
|
assert before["계"] == pytest.approx(expected["계"]) and before["노무비"] > 0
|
|
labor_file = master_dir / "인력.json"
|
|
original = labor_file.read_bytes()
|
|
try:
|
|
data = _read(labor_file)
|
|
row = next(r for r in data["줄"] if r["키"] == LABOR_PRICE)
|
|
row["값"] = row["값"] * 2
|
|
labor_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
assert copies.calc(root, FOLDER) == before # 복사본만 씀
|
|
assert copies.list_items(root)[0]["마스터바뀜"] is True
|
|
copies.refresh(root, FOLDER)
|
|
after = copies.calc(root, FOLDER)
|
|
assert after["노무비"] > before["노무비"] and after["재료비"] == before["재료비"]
|
|
assert copies.read_file(root, FOLDER, "입력값")["내용"] == values # 같은 키 살림
|
|
assert copies.list_items(root)[0]["마스터바뀜"] is False
|
|
finally:
|
|
labor_file.write_bytes(original)
|
|
|
|
|
|
def test_새로_가져오면_없는_키는_버림(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
folder = root / "templates/structures" / FOLDER
|
|
(folder / "입력값.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"GF000176": {"관경": {"값": 600, "확인": True}, "옛입력": {"값": 1}},
|
|
"GX999999": {"a": {"값": 1}},
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
"utf-8",
|
|
)
|
|
(folder / "고른자재.json").write_text(
|
|
json.dumps({"GF000176": {"0": "MT000001", "99": "MT000002"}}), "utf-8"
|
|
)
|
|
copies.refresh(root, FOLDER)
|
|
assert _read(folder / "입력값.json") == {"GF000176": {"관경": {"값": 600, "확인": True}}}
|
|
assert _read(folder / "고른자재.json") == {"GF000176": {"0": "MT000001"}}
|
|
|
|
|
|
# ── 쓰기 · 판 · 모양 · 지우기 규칙 ─────────────────────────
|
|
|
|
|
|
def test_쓰기_뒤_다시_읽기_같음_판_다르면_409(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
first = copies.read_file(root, FOLDER, "고른자재")
|
|
wrote = copies.write_file(
|
|
root, FOLDER, "고른자재", first["판"], {"GF000176": {"0": "MT000010"}}
|
|
)
|
|
again = copies.read_file(root, FOLDER, "고른자재")
|
|
assert again["내용"] == {"GF000176": {"0": "MT000010"}} and again["판"] == wrote["판"]
|
|
with pytest.raises(copies.Stale):
|
|
copies.write_file(root, FOLDER, "고른자재", first["판"], {})
|
|
for kind, bad in (
|
|
("입력값", {"GF000176": {"관경": 600}}),
|
|
("입력값", {"GF000176": {"관경": {"값": 1, "확인": "네"}}}),
|
|
("고른자재", {"GF000176": {"첫": "MT000001"}}),
|
|
("입력연결", {"줄": [{"로직": "GF000176", "입력": "관경", "자리": "어딘가"}]}),
|
|
("도면", {}),
|
|
):
|
|
with pytest.raises(ValueError):
|
|
copies.write_file(root, FOLDER, kind, again["판"], bad)
|
|
with pytest.raises(LookupError):
|
|
copies.read_file(root, "../../etc", "입력값")
|
|
|
|
|
|
def test_고친_복사본은_구조물에서_빠지면_지난에_남김(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
assert _save(root, [_pipe(None)])["지움"] == [FOLDER] # 안 고쳤으면 지움
|
|
assert not (root / "templates/structures/_지난").exists()
|
|
_save(root, [_pipe()])
|
|
current = copies.read_file(root, FOLDER, "입력값")
|
|
copies.write_file(root, FOLDER, "입력값", current["판"], {"GF000176": {"관경": {"값": 600}}})
|
|
assert _save(root, [_pipe(None)])["지움"] == [FOLDER]
|
|
assert copies.list_items(root) == []
|
|
past = list((root / "templates/structures/_지난").iterdir())
|
|
assert len(past) == 1 and past[0].name.startswith("pipe@40.0_")
|
|
assert _read(past[0] / "입력값.json") == {"GF000176": {"관경": {"값": 600}}}
|
|
|
|
|
|
def test_미리_가져오기(world):
|
|
root = world["root"]
|
|
_save(root, [])
|
|
made = copies.prefetch(root, FOLDER, PIPE)
|
|
assert made["폴더"] == FOLDER and made["만듦"] is True
|
|
assert copies.prefetch(root, FOLDER, PIPE)["만듦"] is False # 같은 항목이면 그대로
|
|
assert copies.list_items(root)[0]["구조물"] == [FOLDER]
|
|
assert _save(root, [])["지움"] == [FOLDER] # [저장] 뒤에도 안 쓰면 지움
|
|
with pytest.raises(ValueError):
|
|
copies.prefetch(root, "../x", PIPE)
|
|
|
|
|
|
def test_옛_복사본은_읽을_때_채움(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
folder = root / "templates/structures" / FOLDER
|
|
for name in ("입력연결.json", "입력값.json", "고른자재.json", "단가.json"):
|
|
(folder / name).unlink()
|
|
manifest = _read(folder / "manifest.json")
|
|
for key in ("원본판", "가져온날"):
|
|
manifest.pop(key)
|
|
(folder / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False), "utf-8")
|
|
assert copies.read_file(root, FOLDER, "입력값")["내용"] == {}
|
|
filled = _read(folder / "manifest.json")
|
|
assert filled["가져온날"] == filled["적용일"] and filled["옮겨받음"]
|
|
assert (folder / "단가.json").is_file() and copies.calc(root, FOLDER)["멈춤"]
|
|
assert copies.list_items(root)[0]["마스터바뀜"] is False # 원본이 안 바뀐 옛 복사본
|
|
|
|
|
|
# ── 길 · 권한 ────────────────────────────────────────
|
|
|
|
|
|
def test_길_권한_남의_프로젝트는_403(world, monkeypatch):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
|
|
async def fake_row(project_id: str) -> dict[str, Any]:
|
|
return {"company_id": 3, "storage_path": str(root)}
|
|
|
|
monkeypatch.setattr(layer_routes, "_project_row", fake_row)
|
|
monkeypatch.setattr(layer_routes, "resolve_stored_project_path", lambda path: path)
|
|
app = FastAPI()
|
|
app.include_router(items_router.router)
|
|
session = {"role": "USER", "company_id": 3}
|
|
app.dependency_overrides[verify_session] = lambda: session
|
|
client = TestClient(app)
|
|
base = "/api/projects/p1/items"
|
|
assert client.get(base).json()["항목"][0]["폴더"] == FOLDER
|
|
got = client.get(f"{base}/{FOLDER}/입력값").json()
|
|
put = client.put(f"{base}/{FOLDER}/입력값", json={"판": got["판"], "내용": {}})
|
|
assert put.status_code == 200
|
|
stale = client.put(f"{base}/{FOLDER}/입력값", json={"판": got["판"] + "x", "내용": {}})
|
|
assert stale.status_code == 409 and stale.json()["detail"]["판"] == put.json()["판"]
|
|
assert client.get(f"{base}/{FOLDER}/모름").status_code == 422
|
|
assert client.post(f"{base}/{FOLDER}/calc").json()["멈춤"]
|
|
wall = {"structure": "st9", "layer": "system", "owner": None}
|
|
assert client.post(base, json={**wall, "summary_item": {"layer": "x"}}).status_code == 400
|
|
assert client.post(base, json={"layer": "system", "key": "SS000022"}).status_code == 400
|
|
made = client.post(base, json={**wall, "key": "SS000022"}).json()
|
|
assert made["폴더"] == "st9" and made["만듦"] is True
|
|
names = {i["폴더"]: i["이름"] for i in client.get(base).json()["항목"]}
|
|
assert names["st9"] == "돌쌓기 (찰·메마다) H=(높이마다)" # 층 SS 줄 이름(경로 이음)
|
|
assert client.post(base, json={**wall, "key": "SS999999"}).status_code == 404
|
|
assert client.post(f"{base}/{FOLDER}/reapply").json()["덮임"] == []
|
|
session["company_id"] = 4
|
|
assert client.get(base).status_code == 403
|
|
assert client.post(f"{base}/{FOLDER}/refresh").status_code == 403
|
|
|
|
|
|
# ── 구조물마다 복사본 (26-2b) ─────────────────────────
|
|
|
|
|
|
MS = {"layer": "system", "owner": None, "key": "SS000022", "name": "돌쌓기"}
|
|
|
|
|
|
def _wall(sid: str, summary: dict | None = MS) -> dict[str, Any]:
|
|
return {
|
|
"structure_id": sid,
|
|
"type_id": "masonry_dry",
|
|
"placement": "interval",
|
|
"chainage_m": 85,
|
|
"start_m": 80,
|
|
"end_m": 90,
|
|
"options": {"height_m": 2.0, "length_m": 10},
|
|
"summary_item": summary,
|
|
}
|
|
|
|
|
|
def _save_all(root: Path, walls: list[dict], pipes: list[dict]) -> dict[str, list[str]]:
|
|
(root / "B05_Profile/route/structures.json").write_text(
|
|
json.dumps({"revision": 1, "structures": walls}, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
(root / "B04_PreProcess/drainage/edits/pipe_points.json").write_text(
|
|
json.dumps({"points": pipes}, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
return snap.sync_snapshots(root)
|
|
|
|
|
|
def _memo(root: Path, folder: str, text: str) -> None:
|
|
"""면적 시트에 표식 한 칸 — 한쪽만 고쳤는지 보려는 것(계산값 풀이는 건너뜀)."""
|
|
area = copies.read_file(root, folder, "면적")
|
|
body = {**area["내용"], "메모": text}
|
|
copies.write_file(root, folder, "면적", area["판"], body, recalc=lambda doc: doc)
|
|
|
|
|
|
def test_같은_항목_구조물_둘은_따로(world):
|
|
root = world["root"]
|
|
second = {**_pipe(), "chainage_m": 60.0}
|
|
made = _save_all(root, [_wall("a1"), _wall("a2")], [_pipe(), second])["만듦"]
|
|
assert made == ["a1", "a2", FOLDER, "pipe@60.0"]
|
|
_memo(root, "a1", "a1 만 고침")
|
|
assert copies.read_file(root, "a1", "면적")["내용"]["메모"] == "a1 만 고침"
|
|
assert "메모" not in copies.read_file(root, "a2", "면적")["내용"] # 다른 쪽 그대로
|
|
_fill_values(root) # pipe@40.0 에만 값
|
|
assert copies.calc(root, FOLDER)["멈춤"] == [] and copies.calc(root, FOLDER)["계"] > 0
|
|
assert len(copies.calc(root, "pipe@60.0")["멈춤"]) == 3 # 계산도 각자
|
|
flags = {i["폴더"]: i["고침"] for i in copies.list_items(root)}
|
|
assert flags == {"a1": True, "a2": False, FOLDER: True, "pipe@60.0": False}
|
|
|
|
|
|
def test_원본_항목을_지워도_복사본_계산_그대로(world):
|
|
root, system = world["root"], world["system"]
|
|
_save(root, [_pipe()])
|
|
_fill_values(root)
|
|
before = copies.calc(root, FOLDER)
|
|
combo = _read(system / layers.COMBO_FILE)
|
|
combo["줄"] = [r for r in combo["줄"] if r["키"] != PIPE["key"]]
|
|
(system / layers.COMBO_FILE).write_text(json.dumps(combo, ensure_ascii=False), "utf-8")
|
|
assert copies.calc(root, FOLDER) == before
|
|
row = copies.list_items(root)[0]
|
|
assert row["원본없음"] is True and row["마스터바뀜"] is False
|
|
assert _save(root, [_pipe()])["만듦"] == [] # 다시 저장해도 복사본 그대로
|
|
with pytest.raises(LookupError):
|
|
copies.reapply(root, FOLDER) # 원본이 없으면 재적용 못 함(404)
|
|
|
|
|
|
def test_재적용은_시트_고침을_덮고_알림_입력값은_살림(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
values = _fill_values(root)
|
|
_memo(root, FOLDER, "x")
|
|
assert copies.reapply(root, FOLDER)["덮임"] == ["면적"]
|
|
assert "메모" not in copies.read_file(root, FOLDER, "면적")["내용"]
|
|
assert copies.read_file(root, FOLDER, "입력값")["내용"] == values
|
|
assert copies.reapply(root, FOLDER)["덮임"] == []
|
|
|
|
|
|
def test_개인에_저장하면_개인_층에_항목(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
rows = [{"로직": "GF000176", "입력": "관경", "자리": "선택"}]
|
|
current = copies.read_file(root, FOLDER, "입력연결")
|
|
copies.write_file(root, FOLDER, "입력연결", current["판"], {"판": 3, "줄": rows})
|
|
made = copies.save_as(root, FOLDER, 3, 12, ["관공", "우리 현장"], "파형강관 매설")
|
|
assert made["summary_item"] == {
|
|
"layer": "personal",
|
|
"owner": "3/12",
|
|
"key": made["key"],
|
|
"name": "관공 우리 현장 파형강관 매설",
|
|
}
|
|
folder = layers.personal_dir(3, 12)
|
|
row = next(r for r in _read(folder / layers.COMBO_FILE)["줄"] if r["키"] == made["key"])
|
|
assert [row["종류"], row["분류1"], row["분류2"]] == ["관공", "우리 현장", "파형강관 매설"]
|
|
assert row["일위대가"] == "UA000036" and row["단위"] == "m"
|
|
copy = root / "templates/structures" / FOLDER
|
|
drawing = _read(folder / "structure" / f"{made['열']}.json")
|
|
assert drawing["도면"] == _read(copy / "도면.json")["도면"]
|
|
assert _read(folder / "inputs" / f"{made['열']}.json") == {"판": 1, "줄": rows}
|
|
assert not list(folder.rglob("단가.json")) # 단가 묶음은 안 가져감
|
|
# 개인 층 항목을 다른 구조물에 지정하면 그 내용으로 복사본이 섬
|
|
assert _save_all(root, [_wall("b1", made["summary_item"])], [])["만듦"] == ["b1"]
|
|
|
|
|
|
def test_프로젝트에서_새_항목(world):
|
|
root = world["root"]
|
|
_save_all(root, [_wall("c1")], [])
|
|
made = copies.new_item(root, "c1", 3, 12, ["신규 공사"], "돌붙임", "㎡")
|
|
assert made["폴더"] == "c1" and made["summary_item"]["layer"] == "personal"
|
|
origin = _read(root / "templates/structures/c1/manifest.json")["원본"]
|
|
assert origin["key"] == made["key"] and origin["경로"] == ["신규 공사", "돌붙임"]
|
|
assert copies.list_items(root)[0]["이름"] == "신규 공사 돌붙임"
|
|
# 화면이 새 항목을 지정해 [저장] 하면 그대로 남음
|
|
assert _save_all(root, [_wall("c1", made["summary_item"])], [])["만듦"] == []
|
|
with pytest.raises(ValueError):
|
|
copies.new_item(root, "c2", 3, 12, [], "", None) # 경로 · 이름 빈 값
|
|
|
|
|
|
def test_옛_항목_폴더는_쓰는_구조물마다_옮겨_받음(world):
|
|
root = world["root"]
|
|
_save_all(root, [_wall("d1"), _wall("d2")], [])
|
|
base = root / "templates/structures"
|
|
old = root / "templates/items/system_none_SS000022"
|
|
old.parent.mkdir(parents=True)
|
|
shutil.move(str(base / "d1"), str(old))
|
|
shutil.rmtree(base / "d2")
|
|
manifest = _read(old / "manifest.json")
|
|
manifest.update({"구조물": ["d1", "d2"], "고침": True})
|
|
(old / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False), "utf-8")
|
|
(old / "입력값.json").write_text(json.dumps({"GF": {"a": {"값": 1}}}), "utf-8")
|
|
assert [i["폴더"] for i in copies.list_items(root)] == ["d1", "d2"]
|
|
assert not (root / "templates/items").exists()
|
|
for sid in ("d1", "d2"):
|
|
assert _read(base / sid / "입력값.json") == {"GF": {"a": {"값": 1}}} # 고친 값 유지
|
|
assert _read(base / sid / "manifest.json")["구조물"] == [sid]
|
|
assert _save_all(root, [_wall("d1"), _wall("d2")], [])["만듦"] == []
|
|
|
|
|
|
def test_지정을_바꾸면_고친_옛_복사본은_지난에(world):
|
|
root = world["root"]
|
|
_save_all(root, [_wall("e1")], [])
|
|
_memo(root, "e1", "y")
|
|
rv = {"layer": "system", "owner": None, "key": "SS000001", "name": "돌기슭막이"}
|
|
result = _save_all(root, [_wall("e1", rv)], [])
|
|
assert result["만듦"] == ["e1"] and result["지움"] == ["e1"]
|
|
assert _read(root / "templates/structures/e1/manifest.json")["원본"]["key"] == "SS000001"
|
|
past = list((root / "templates/structures/_지난").iterdir())
|
|
assert len(past) == 1 and _read(past[0] / "면적.json")["메모"] == "y"
|
|
|
|
|
|
def test_새_항목_길_회사_없으면_409(world, monkeypatch):
|
|
root = world["root"]
|
|
_save_all(root, [_wall("f1")], [])
|
|
|
|
async def fake_row(project_id: str) -> dict[str, Any]:
|
|
return {"company_id": None, "storage_path": str(root)}
|
|
|
|
monkeypatch.setattr(layer_routes, "_project_row", fake_row)
|
|
monkeypatch.setattr(layer_routes, "resolve_stored_project_path", lambda path: path)
|
|
app = FastAPI()
|
|
app.include_router(items_router.router)
|
|
session = {"role": "SYSTEM_ADMIN", "company_id": None, "user_id": 12}
|
|
app.dependency_overrides[verify_session] = lambda: session
|
|
client = TestClient(app)
|
|
body = {"structure": "f1", "경로": ["새"], "이름": "것"}
|
|
assert client.post("/api/projects/p1/items/new", json=body).status_code == 409
|
|
save_as = client.post("/api/projects/p1/items/f1/save-as", json={"경로": ["a"], "이름": "b"})
|
|
assert save_as.status_code == 409
|
|
|
|
|
|
def test_재적용_뒤_고침은_꺼지고_값있음만_남음(world):
|
|
root = world["root"]
|
|
_save(root, [_pipe()])
|
|
_memo(root, FOLDER, "x")
|
|
assert copies.list_items(root)[0]["고침"] is True
|
|
got = copies.reapply(root, FOLDER)
|
|
assert got["값있음"] is False
|
|
row = copies.list_items(root)[0]
|
|
assert row["고침"] is False and row["값있음"] is False
|
|
_fill_values(root) # 입력값 씀 — 고침 켜짐
|
|
assert copies.list_items(root)[0]["고침"] is True
|
|
assert copies.reapply(root, FOLDER)["값있음"] is True
|
|
row = copies.list_items(root)[0]
|
|
assert row["고침"] is False and row["값있음"] is True
|