"""구조물도 일위대가 줄 고치기 창구 (2026-09-13, PLAN 3장 ③). 겨누는 것 ① 줄 더하기·빼기를 저장하면 표가 그 조합으로 서고, 양식대로 보내면 저장본이 지워짐 ② 수동 단가는 막힌 줄을 세우고 「미확정」으로 셈 · 넣은 날짜는 서버가 붙임 ③ ⛔ 저장 자리는 둘 — [내 라이브러리에 저장]은 줄 조합만 싣고 수동 단가는 안 실음 ④ 다른 양식을 가져오면 그 종류의 줄 조합·수동 단가가 비워짐 ⑤ 고르개가 실제 단가표에서 품셈·자원을 찾음 · 차례가 겹치면 거절 """ from __future__ import annotations import json import sys from pathlib import Path import pytest from fastapi import FastAPI from fastapi.testclient import TestClient ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402 import B08_Quantity.B08_Quantity_Router_Material as material_module # noqa: E402 import B08_Quantity.B08_Quantity_Router_StructureSheet as router_module # noqa: E402 from B05_Profile.B05_Profile_Structures_Repository import save_structures # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402 from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402 from common_util.common_util_auth import verify_session # noqa: E402 from common_util.common_util_project_settings import quantity_settings # noqa: E402 PROJECT_ID = "55555555-5555-5555-5555-555555555555" SHEETS = f"/api/projects/{PROJECT_ID}/quantity/structure-sheets" @pytest.fixture() def project(tmp_path: Path) -> Path: root = tmp_path / "project" root.mkdir() wall = StructureInstance.model_validate( { "type_id": "masonry_wet", "placement": "interval", "start_m": 0.0, "end_m": 10.0, "options": {"height_m": 2.5, "back_len_cm": 45}, } ) save_structures(str(root), [wall], base_revision=0) return root @pytest.fixture() def client(project: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient: async def fake_root(project_id): return str(project) async def no_route(project_id): return {} async def real_build(project_id): from B09_Estimation.B09_Estimation_UnitPrice import cached_build return cached_build() monkeypatch.setattr(library_module, "STORAGE_BASE_DIR", str(tmp_path / "storage")) monkeypatch.setattr(router_module, "_project_root", fake_root) monkeypatch.setattr(router_module, "_price_build", real_build) monkeypatch.setattr(material_module, "_section_modes", no_route) monkeypatch.setattr(material_module, "_ground_types", no_route) app = FastAPI() app.include_router(router_module.router) app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 42} return TestClient(app) def _key(client: TestClient) -> str: return client.get(SHEETS).json()["sheets"][0]["key"] def _table(client: TestClient, key: str) -> dict: response = client.get(f"{SHEETS}/unit-price", params={"sheet_key": key}) assert response.status_code == 200, response.text return response.json() def test_줄을_고치고_수동_단가를_넣으면_저장_자리가_둘로_갈린다( client: TestClient, project: Path, tmp_path: Path ) -> None: key = _key(client) before = _table(client, key) assert before["editor"]["edited"] is False and before["unit_price"]["unconfirmed"] == 0 rows = before["editor"]["rows"] # 기초잡석(3) 빼고 · 고정 수량 1.5 줄 더하기 · 모르터(2)에 수동 단가. edited = [ rows[0], rows[1], {"seq": 4, "name": "추가 잡석", "quantity": 1.5, "ref_code": "B-FP-12-25"}, ] saved = client.put( f"{SHEETS}/unit-price", json={ "sheet_key": key, "rows": edited, "manual_prices": { "2": {"material": 80000, "source": "견적 3곳 평균"}, "3": {"labor": 1}, }, }, ) assert saved.status_code == 200, saved.text assert saved.json()["edited"] is True and saved.json()["manual_prices"] == 1 after = _table(client, key) table = after["unit_price"] assert [row["seq"] for row in table["rows"]] == [1, 2, 4] mortar = table["rows"][1] assert mortar["manual"] is True and mortar["manual_entered_at"] assert table["unconfirmed"] == 1 and table["complete"] is True assert table["rows"][2]["quantity"] == pytest.approx(1.5) and table["rows"][2]["total"] > 0 # 저장 자리 둘 — 줄 조합은 종류별, 수동 단가는 프로젝트 칸. settings = quantity_settings(str(project)) assert [row["seq"] for row in settings["structure_unit_price_rows"]["masonry_wet"]] == [1, 2, 4] assert settings["structure_manual_prices"]["masonry_wet"]["2"]["source"] == "견적 3곳 평균" # ⛔ 내 라이브러리에는 줄 조합만 — 수동 단가 흔적이 없음. mine = client.put(f"{SHEETS}/library/personal", json={"sheet_key": key}) assert mine.status_code == 200, mine.text folder = tmp_path / "storage" / "7" / "42" / "library" item = json.loads((folder / f"{mine.json()['code']}.json").read_text(encoding="utf-8")) assert [row["seq"] for row in item["unit_price"]["rows"]] == [1, 2, 4] assert "80000" not in json.dumps(item) and "견적" not in json.dumps(item) # 양식대로 되돌리면 줄 조합 저장본이 지워짐. back = client.put( f"{SHEETS}/unit-price", json={"sheet_key": key, "rows": after["editor"]["default_rows"], "manual_prices": {}}, ) assert back.json()["edited"] is False assert "masonry_wet" not in quantity_settings(str(project))["structure_unit_price_rows"] def test_다른_양식을_가져오면_줄_조합과_수동_단가가_비워진다( client: TestClient, project: Path ) -> None: key = _key(client) rows = load_template("masonry_wet")["unit_price"]["rows"][:2] client.put( f"{SHEETS}/unit-price", json={"sheet_key": key, "rows": rows, "manual_prices": {"2": {"material": 1}}}, ) taken = client.put( f"{SHEETS}/library/import", json={ "type_id": "masonry_wet", "tier": "program", "code": load_template("masonry_wet")["code"], }, ) assert taken.status_code == 200, taken.text assert ( taken.json()["cleared_unit_price_rows"] is True and taken.json()["cleared_manual_prices"] == 1 ) settings = quantity_settings(str(project)) assert "masonry_wet" not in settings["structure_unit_price_rows"] assert "masonry_wet" not in settings["structure_manual_prices"] def test_고르개와_차례_겹침_거절(client: TestClient) -> None: found = client.get(f"{SHEETS}/price-search", params={"q": "FP-12-25", "kind": "work"}) assert found.status_code == 200, found.text assert any(item["code"] == "B-FP-12-25" for item in found.json()["items"]) resource = client.get(f"{SHEETS}/price-search", params={"q": "보통인부", "kind": "resource"}) assert resource.json()["items"], "노임 보통인부가 자원 갈래에 있어야 함" twice = [{"seq": 1, "name": "a", "quantity": 1}, {"seq": 1, "name": "b", "quantity": 1}] bad = client.put(f"{SHEETS}/unit-price", json={"sheet_key": _key(client), "rows": twice}) assert bad.status_code == 422