"""144-1 B09 뼈대 — 탭 차례 · 저장 자리 · 길 · 빈칸 규칙 · 「연결 없음」 목록 · 단가 굳히기. - 탭 = 기번3 공통 엑셀 보이는 탭 차례(목차 · 안내 = 「연결 점검」 한 탭 · INITIAL 안 옮김) - 저장 = 프로젝트 안 `B09_Estimation/v1/`(사용자 값 · 굳힌 단가) · 결과는 저장 안 함 - 줄 = 수요 따라(사용자 승인 ①) — 연결된 사본 없으면 노무 · 경비 · 중기 빈 표 · B08 자재만 줄 · 출처 빈칸 """ import asyncio import json import re import sys from pathlib import Path 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_Prices as prices # noqa: E402 from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402 from B09_Estimation import B09_Estimation_Store as store # noqa: E402 #: 엑셀 보이는 탭(목차 · 안내 빼고) — 조사 F 1절 차례 EXCEL_TABS = [ "공사원가계산서", "총괄설계내역서", "설계내역서", "일위대가목록표", "일위대가표", "단가산출근거목록표", "단가산출근거", "환율및기초자료", "중기목록표", "중기사용료", "재료비목록표", "노무비목록표", "경비목록표", "자재단가대비표", "재료비수량금액집계표", "노무비수량금액집계표", "경비수량금액집계표", "중기시간금액집계표", ] # fmt: skip B08_SHEETS = { "material": { "rows": [ {"key": "sand", "group": "골재", "spec": "모래", "unit": "㎥", "amount": 16}, {"key": "rubble", "group": "골재", "spec": "막자갈", "unit": "㎥", "amount": None}, {"key": "cement", "group": "시멘트", "spec": "40kg/대", "unit": "대", "amount": 244}, ] } } def test_탭_차례가_엑셀과_같다(): source = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs.ts").read_text(encoding="utf-8") block = source[ source.index("export const TABS") : source.index("];", source.index("export const TABS")) ] names = re.findall(r'\["\w+", "([^"]+)"\]', block) assert names[0] == "연결 점검" assert names[1:] == EXCEL_TABS def test_길_셋(): routes = {(r.path, tuple(sorted(r.methods))) for r in router_mod.router.routes} assert ("/api/projects/{project_id}/estimation", ("GET",)) in routes assert ("/api/projects/{project_id}/estimation", ("PUT",)) in routes assert ("/api/projects/{project_id}/estimation/prices", ("POST",)) in routes main = (ROOT / "main.py").read_text(encoding="utf-8") assert "app.include_router(b09_estimation_router" in main def test_사용자_값_다듬기_저장_자리(tmp_path): raw = { "materials": {"MT1": {"applied": 1200, "supply": "관급", "x": 1}, "MT2": {"applied": None}, "MT3": {"supply": "엉뚱"}}, "estimate": {"estimated_price": "abc", "profit_adjust": 905980, "work_type": "토목", "rates": {"RT000001": 3.56, "RT000002": None}, "zzz": 1}, "other": 1, } # fmt: skip clean = store.write_user(tmp_path, raw) assert clean == { "materials": {"MT1": {"applied": 1200.0, "supply": "관급"}, "MT2": {"applied": None}}, "estimate": {"estimated_price": None, "profit_adjust": 905980.0, "work_type": "토목", "rates": {"RT000001": 3.56, "RT000002": None}}, } # fmt: skip path = tmp_path / "B09_Estimation" / "v1" / "user_values.json" assert json.loads(path.read_text(encoding="utf-8")) == clean assert store.read_user(tmp_path) == clean assert store.read_book(tmp_path) is None def test_단가_굳히기_운전사_연료_따라옴(): book = prices.refresh_book(["EQ000004"]) # 불도저(무한궤도) 19ton · 조종원 1048 · 경유 rows = book["rows"] assert set(rows) == {"EQ000004", "LB000048", "MO000002"} assert rows["EQ000004"]["loss"] == 1811 and rows["EQ000004"]["crew_code"] == "1048" assert rows["LB000048"]["code"] == "1048" and rows["MO000002"]["prices"]["유가"] > 0 assert book["driver_factor"] == {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"} # [저장] = 책에 없는 새 줄만 더함 — 있는 줄 값은 안 바꿈 stale = {**book, "rows": {**rows, "LB000048": {**rows["LB000048"], "price": 1}}} grown = prices.extend_book(stale, ["EQ000004", "LB000002"]) assert grown["rows"]["LB000048"]["price"] == 1 and "LB000002" in grown["rows"] # 책 밖 줄 = 지금 M01 값 + fresh 표시 inputs = prices.build_inputs(stale, ["EQ000004", "LB000002"]) fresh = {row["key"]: row.get("fresh", False) for row in inputs["labor"]} assert fresh == {"LB000002": True, "LB000048": False} assert inputs["fuel_keys"] == {"경유": "MO000002"} def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch): async def fake_materials(_project_id): return B08_SHEETS monkeypatch.setattr(router_mod, "material_sheets", fake_materials) body = asyncio.run(router_mod.payload("p", tmp_path)) sheets = body["sheets"] assert sheets["labor"] == [] and sheets["expense"] == [] and sheets["machine"] == [] assert [row["key"] for row in sheets["compare"]] == ["b08:sand", "b08:cement"] # 수량 있는 줄만 assert all(row["applied"] is None for row in sheets["compare"]) # 출처 없음 = 빈칸(0 아님) assert [(g["tab"], g["key"]) for g in sheets["gaps"] if g["tab"] == "compare"] == [ ("compare", "b08:sand"), ("compare", "b08:cement"), ] assert any("연결된 구조물 조합 사본 없음" in n for n in body["notes"]) assert body["book"] is None # GET 은 아무것도 쓰지 않음 assert not (tmp_path / "B09_Estimation").exists() def test_연결된_사본이_부른_자원만(tmp_path): """사본 단가 묶음 통째(고르기 후보 기계 100여)가 아니라 로직이 실제로 부른 줄만.""" from B09_Estimation.B09_Estimation_Linked import used_keys from M02_MasterTemplete import M02_Item_Prices as item_prices bundle = item_prices.to_json(item_prices.collect(["GC000268"])) # 기계 시간당 기계경비 files = { "단가.json": bundle, "일위대가.json": {"줄": {"종류": "일위대가", "담은로직": [{"로직": "GC000268"}]}}, "입력값.json": {"GC000268": {"기계": {"값": "0101-0019"}, "지역": {"값": "전국평균"}}}, } for name, data in files.items(): (tmp_path / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") assert used_keys(tmp_path) == ["EQ000004", "MO000002", "LB000048"] (tmp_path / "입력값.json").write_text("{}", encoding="utf-8") # 빈 입력 = 자동값(142-1 사본 셈 한 벌 · 기계 고르기 첫 값 1730-0015) — 그 로직이 부른 줄 assert used_keys(tmp_path) == ["EQ000112", "MO000001", "LB000050"] assert used_keys(tmp_path / "없음") == []