Files
Aislo/resources/tester/test_144_b09_skeleton.py
T
eomsangdonandClaude Opus 5.5 d9db1dfb1d feat(B09): 단가산출근거 · 목록표 — 승격 줄 · 옛 SP → 산근 블록 · B09 굳힌 단가 · 엑셀 끝수(145-1)
연결된 사본 일위대가 로직 줄 중 승격 + 옛 SP 참조 줄 · 같은 로직 + 같은 입력값 = 산근 하나 · 서버 단독 풀이 ·
대가 · 내역서가 읽는 길 sangeun_key · price_of · Linked.used_keys 빈 입력 = 자동값.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
2026-10-09 15:19:31 +09:00

136 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"]] == [
("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")
auto = used_keys(tmp_path) # 빈 입력 = 자동값(145-1 · 142-1) — 자동 기계 한 대 자원만
assert auto and len(auto) <= 3 and auto[0].startswith("EQ")
assert used_keys(tmp_path / "없음") == []