"""703-2 B09 펼친 일위대가 우클릭 — 복사해 새 일위대가 · 내 자료집에 저장. - 복사본 = 덮개 `K:…` 의 `@copy` {from · name · spec} — 셈 짝(`Calc_Labor` 파이썬 · TS 거울)이 원본 블록을 본떠 끝 번호 새 호표 · 덮개 = 제 것 · 원본 없음 = 짝 잃음 · 저장 다듬기 · 서고 사본 지킴(prune) - 내 자료집 = 개인 층 사전조합(임시 storage) — 줄 수량 덮개 = 문서 손(쓸 때 걷힘 · 723-1) · 품 · 단가 덮개 = 안 담음 수 · 같은 이름 409 → 덮어쓰기 · 새 이름 """ import copy import importlib.util import json import sys from pathlib import Path from typing import Any import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B09_Estimation import B09_Estimation_Bill_Library as library # noqa: E402 from B09_Estimation import B09_Estimation_Daega_Personal as personal # noqa: E402 from B09_Estimation import B09_Estimation_Store as store # noqa: E402 from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402 from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402 from config import config_system # noqa: E402 from M02_MasterTemplete import M02_MasterTemplete_Items as items_store # noqa: E402 from M02_MasterTemplete import M02_MasterTemplete_Store as m02 # noqa: E402 from M02_MasterTemplete import M02_Template_Layers as layers # noqa: E402 def _load(name: str): spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(f"{name}.py")) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod b145 = _load("test_145_2_b09_labor_calc") b702 = _load("test_702_b09_daega_cover") #: DG9 찰붙임 복사 — 석공 품 0.1 → 0.2(복사본만) · 원본 DG9 덮개 0.12 는 원본에만 COPY = { "T:DG9": {"labor:L00005#0": {"qty": 0.12}}, "K:aa": { "@copy": {"from": "T:DG9", "name": "찰붙임 복사", "spec": "규격 B"}, "labor:L00005#0": {"qty": 0.2}, }, "K:bb": {"@copy": {"from": "K:aa", "name": "복사의 복사"}}, "K:cc": {"@copy": {"from": "T:없음"}, "x#0": {"qty": 1}}, } def _block(out, bid): return next(b for b in out["daega_blocks"] if b["id"] == bid) def test_복사본_새_호표_끝_번호_덮개_제것(): daega = b702._covered_excel() out = compute_labor(daega, b145.SHEETS, b145.SANGEUN, COPY) src, mine = _block(out, "DG9"), _block(out, "DG:K:aa") n = len(daega["blocks"]) assert [mine["no"], _block(out, "DG:K:bb")["no"]] == [n + 1, n + 2] # 번호 새로(끝) assert (mine["name"], mine["spec"], mine["cover"]) == ("찰붙임 복사", "규격 B", "K:aa") assert src["lines"][0]["qty"] == 0.12 and mine["lines"][0]["qty"] == 0.2 assert mine["lines"][0]["base"]["qty"] == 0.1 # 품셈 값 = 원본 기본값 assert [ln["amount"] for ln in mine["lines"][1:]] == [ln["amount"] for ln in src["lines"][1:]] twice = _block(out, "DG:K:bb") assert twice["spec"] == "규격 B" and twice["lines"][0]["qty"] == 0.1 # 덮개 안 물려받음 assert any(r["key"] == "DG:K:aa" for r in out["daega"]) # 목록표 = 내역 단가연결 자리 assert out["daega_orphans"] == [ {"cover": "K:cc", "cid": "@copy"}, {"cover": "K:cc", "cid": "x#0"}, ] def test_거울_파이썬_TS_복사본(): daega = b702._covered_excel() py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, b145.SANGEUN, COPY))) ts = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COPY) assert b145._same(py, ts["out"]) def test_저장_다듬기_복사본_표지(): raw = {"daega_cover": {**COPY, "K:dd": {"@copy": {"name": "원본 없음"}}}} got = store.clean_user(raw)["daega_cover"] assert got["K:aa"]["@copy"] == {"from": "T:DG9", "name": "찰붙임 복사", "spec": "규격 B"} assert got["K:bb"] == {"@copy": {"from": "K:aa", "name": "복사의 복사"}} assert "K:dd" not in got def test_서고_사본_원본_지킴(): user = {"daega_cover": {"K:aa": {"@copy": {"from": "L:combo@system@-@SS1"}}}} assert library.used_folders(user) == {"combo@system@-@SS1"} def test_사전조합_원본_복사본_사본_지킴(tmp_path): """통합 점검 — 사전조합(`C:참조`) 복사본 · 복사본의 복사본 = 원본 사본 폴더 지킴(업데이트 수요 · 저장 정리).""" base = library.library_dir(tmp_path) for name, ref in ( ("precombo@personal@1-3@SC-000001", {"층": "personal", "주인": "1/3", "키": "SC-000001"}), ("precombo@personal@1-3@SC-000002", {"층": "personal", "주인": "1/3", "키": "SC-000002"}), ): (base / name).mkdir(parents=True) manifest = {"종류": "precombo", "원본": {"참조": ref}} (base / name / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") src = "C:참조|personal|1/3|SC-000001" user = { "daega_cover": { "K:aa": {"@copy": {"from": src, "name": "복사"}}, "K:bb": {"@copy": {"from": "K:aa", "name": "복사의 복사"}}, } } assert library.used_folders(user) == set() # 폴더 모름 = 옛 꼴 assert library.used_folders(user, tmp_path) == {"precombo@personal@1-3@SC-000001"} assert library.prune(tmp_path, user) == ["precombo@personal@1-3@SC-000002"] # ── 내 자료집(개인 층 사전조합 · 임시 storage) ───────────────────────────── @pytest.fixture() def world(tmp_path, monkeypatch) -> dict[str, Any]: (tmp_path / "storage").mkdir() monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage")) root = b145._project(tmp_path / "proj", [(100, "SS000901")]) b145._snap(root, "pipe@100.0", "SS000901", b145.LINES, b145._values(b145.STONE)) folder = layers.personal_dir(7, 42) folder.mkdir(parents=True) items_store.ensure(folder) got = daega_inputs(root) block = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@100.0"]) return {"root": root, "folder": folder, "block": block} def test_자료집_문서_덮개_손_안담음_수(world): block = world["block"] each = block["lines"][1]["qty"] # GF000119 줄 수량(산근 = 손 그대로 0.5) cover = { block["cover"]: { "sangeun:SG:GF000119#0": {"qty": 0.8, "labor": 1000}, # 줄 수량 = 담음 · 단가 = 안 담음 } } made = personal.build(world["root"], block["cover"], cover, "내 돌붙임") doc = made["문서"] assert each == 0.5 and doc["이름"] == "내 돌붙임" hands = [ln["수량"]["손"] for ln in doc["담은로직"]] assert hands == [2, 0.8, 1] # 덮개 줄만 바뀜 assert doc["담은로직"][0]["입력값"] == b145.STONE # 넣을 값 얹음 assert made["담음"] == {"수량": 1, "넣을값": 1, "고른자재": 0} assert made["안담음"] == {"품": 0, "단가": 1} # 복사본 = 원본까지 따라감 · 이름 기본 = 복사본 이름 · 덮개 = 복사본 것 cover2 = {"K:aa": {"@copy": {"from": block["cover"], "name": "복사"}}} cover2["K:aa"]["sangeun:SG:GF000219#0"] = {"qty": 3} made2 = personal.build(world["root"], "K:aa", cover2) assert made2["문서"]["이름"] == "복사" assert [ln["수량"]["손"] for ln in made2["문서"]["담은로직"]] == [3, 0.5, 1] with pytest.raises(LookupError): personal.build(world["root"], "S:없음", {}) def test_자료집_같은_이름_물음(world): block = world["block"] doc = personal.build(world["root"], block["cover"], {}, "내 돌붙임")["문서"] with m02.at(world["folder"]): first = personal.write(doc) with pytest.raises(m02.StoreError) as e: personal.write(doc) assert e.value.status == 409 and e.value.detail["같은이름"] == first["이름"] changed = copy.deepcopy(doc) changed["담은로직"][1]["수량"] = {"손": 9} again = personal.write(changed, first["이름"]) # 덮어쓰기 assert again["이름"] == first["이름"] with pytest.raises(m02.StoreError) as e: # 733-2 — 새 이름이라도 같은 내용 = 409 물음 personal.write({**doc, "이름": "내 돌붙임 2"}) assert ( e.value.detail["같은것"][0]["키"] == first["이름"] and "같은이름" not in e.value.detail ) other = personal.write({**doc, "이름": "내 돌붙임 2"}, new=True) # 그래도 새로 used = personal.write({**doc, "이름": "내 돌붙임 3"}, use=first["이름"]) # 그것을 씀 assert used == {"이름": first["이름"], "씀": True} assert other["이름"] != first["이름"] saved = json.loads( (world["folder"] / "precombo" / f"{first['이름']}.json").read_text(encoding="utf-8") ) # 742-1 = M02 SC 안 줄 수량 저장(덮어쓴 손 9 · 쓰는 곳 알약 · 이 공사 입력이 이어지면 그 값이 앞섬) assert saved["이름"] == "내 돌붙임" and saved["담은로직"][1]["수량"] == {"손": 9} assert saved["담은로직"][0]["입력값"] == b145.STONE assert len(list((world["folder"] / "precombo").glob("SC-*.json"))) == 2 def test_자료집_길_같은_이름_409_덮어쓰기(world, monkeypatch): """서버 길 — 화면 캐시 덮개째 · 개인 층(임시 storage) · 같은 이름 409 {같은이름} → 덮어쓰기.""" from fastapi import FastAPI from fastapi.testclient import TestClient from B09_Estimation import B09_Estimation_Router as b09 from common_util.common_util_auth import verify_session async def root_of(_pid): return world["root"] monkeypatch.setattr(b09, "_project_root", root_of) app = FastAPI() app.include_router(b09.router) session = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False} app.dependency_overrides[verify_session] = lambda: session client = TestClient(app) block = world["block"] url = "/api/projects/8b98ca75-c572-4002-8468-2adae4323414/estimation/daega/personal" user = {"daega_cover": {block["cover"]: {"sangeun:SG:GF000219#0": {"qty": 4}}}} body = {"cover": block["cover"], "name": "길 시험", "user": user} first = client.post(url, json=body) assert first.status_code == 200, first.text assert first.json()["담음"]["수량"] == 1 again = client.post(url, json=body) assert again.status_code == 409 and again.json()["detail"]["같은이름"] == first.json()["키"] over = client.post(url, json={**body, "overwrite": first.json()["키"]}) assert over.status_code == 200 and over.json()["키"] == first.json()["키"] saved = world["folder"] / "precombo" / f"{first.json()['키']}.json" assert json.loads(saved.read_text(encoding="utf-8"))["담은로직"][0]["수량"] == { "손": 4 } # 742-1 assert client.post(url, json={**body, "cover": "S:없음"}).status_code == 404