- `M02_Logic_Standard` — 로직 하나를 대표 규격(고르기 값 전부 · 표 찾기 조건 수 = 표 줄마다 · 지역 = 자동값)으로 펼친 줄 · 품명 · 규격 · 단위 · 금액(엔진 계) · 출처 · 공종 · 실무 낱말 별칭(석축 → 돌쌓기) - 목록 = 서버가 뒤에서 한 번 셈(프로세스 4 · 약 90 s) → storage `_derived/m02_logic_standard/` · 열쇠 = 마스터 내용 지문 · 셈 도중 빈 줄 + 준비 false - 계산서 = `master_text.lines` 줄 · 비목 소계 · 계 · 맨 위 출처 · 노임 판 · 규격 축 아닌 입력 400 - 길 `/api/m02/logic-lib/standard` · `/logic-lib/sheet` — 로그인만(일반 사용자도) - 시험 `test_704_logic_standard.py` — 세 항목 금액 = M01 시험 계산 · 계산서 계 = 엔진 값 · 석축 찾기 · 저장본 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.7 KiB
Python
129 lines
5.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""PLAN 704-1 · 704-2 — M02 기준 목록(대표 규격 펼침 · 금액 = M01 엔진) · 계산서 보기.
|
|
|
|
① 세 항목(돌쌓기 메쌓기 · 파형강관 · 구조물터파기) 줄 금액 = M01 시험 계산(`store.calc`)
|
|
② 대표 규격 — 관경 고르기 11 · 뒷길이 표 줄 3 · 지역 = 자동값 한 벌
|
|
③ 실무 낱말 — 「석축」 찾으면 돌쌓기 · 찰쌓기(서버 찾기 맞춤 `hit` = 화면 `matchLogic` 거울 · 208-1)
|
|
④ 계산서 — 계 = 엔진 값 · 비목 소계 합 = 계 · 규격 축 아닌 입력은 400
|
|
⑤ 목록 저장본 — 임시 storage `_derived/` · 같은 지문이면 다시 안 셈
|
|
⑥ 화면 — 고르기 창이 기준 목록 표 · 계산서 [담기] = 지금 담기 동작(`done(logic)`)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M01_MasterData.M01_MasterData_Store_Find import hit
|
|
from M02_MasterTemplete import M02_Logic_Standard as st
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
THREE = ("GF000220", "GC000404", "GF000101") # 돌쌓기 메쌓기(장비) · 파형강관 부설 · 구조물터파기
|
|
|
|
|
|
def _rows(key: str) -> list[dict]:
|
|
whole = m01.loaded()[1]
|
|
return st._rows_of(whole, whole.logic(key), False)
|
|
|
|
|
|
def _given(key: str, change: dict) -> dict:
|
|
whole = m01.loaded()[1]
|
|
return st._given(whole, whole.logic(key), change)[0]
|
|
|
|
|
|
@pytest.mark.parametrize("key", THREE)
|
|
def test_줄_금액은_M01_시험_계산(key: str) -> None:
|
|
rows = _rows(key)
|
|
assert rows
|
|
for row in rows:
|
|
got = m01.calc(key, _given(key, row["바꿈"]))
|
|
assert got["ok"], got
|
|
assert row["금액"] == pytest.approx(float(got["sums"]["계"]), abs=0.01), row["규격"]
|
|
|
|
|
|
def test_대표_규격_펼침() -> None:
|
|
pipe = _rows("GC000404")
|
|
assert len({r["규격"] for r in pipe}) == len(pipe) # 줄마다 규격이 다름
|
|
assert sum(1 for r in pipe if not r["바꿈"] or "관경" in r["바꿈"]) == 11
|
|
assert pipe[0]["규격"].startswith("관경 250") and pipe[0]["단위"] == "본"
|
|
stone = _rows("GF000220")
|
|
backs = [r for r in stone if not r["바꿈"] or "뒷길이" in r["바꿈"]]
|
|
assert [r["규격"].split(" · ")[0] for r in backs] == [
|
|
"뒷길이 35㎝ 이하",
|
|
"뒷길이 55㎝ 이하",
|
|
"뒷길이 75㎝ 이하",
|
|
]
|
|
assert all("지역" not in r["바꿈"] for r in stone) # 지역 = 자동값 한 벌
|
|
hand = _rows("GF000219") # 메쌓기(인력) — 견치돌 · 뒷길이 25 같은 표 밖 조합은 0 원 → 뺌
|
|
assert all(r["금액"] != 0 for r in hand[1:])
|
|
assert stone[0]["공종"] == "돌공" and stone[0]["단위"] == "㎡"
|
|
assert _rows("GF000101")[0]["공종"] == "토공"
|
|
|
|
|
|
def test_석축_찾으면_쌓기() -> None:
|
|
hits = [
|
|
r["이름"]
|
|
for _, data in m01._logic_files(m01.loaded()[0])
|
|
for logic in data.get("줄") or []
|
|
for r in [{**logic, "별칭": ", ".join(st.words_of(logic))}]
|
|
if hit(r, "석축")
|
|
]
|
|
assert "돌쌓기 메쌓기(장비)" in hits
|
|
assert any("찰쌓기" in name for name in hits)
|
|
assert "석축" in _rows("GF000220")[0]["별칭"]
|
|
|
|
|
|
@pytest.mark.parametrize("key", THREE)
|
|
def test_계산서_계는_엔진_값(key: str) -> None:
|
|
row = _rows(key)[-1]
|
|
got = st.sheet(key, row["바꿈"])
|
|
assert got["계"] == pytest.approx(row["금액"], abs=0.01)
|
|
assert sum(g["소계"] for g in got["묶음"]) == pytest.approx(got["계"], abs=0.01)
|
|
assert got["판"].endswith("노임") and got["출처"]
|
|
assert all(line["글"] for g in got["묶음"] for line in g["줄"])
|
|
|
|
|
|
def test_계산서_규격_축_아닌_입력은_거절() -> None:
|
|
with pytest.raises(m01.StoreError) as e:
|
|
st.sheet("GF000220", {"지역": "서울"})
|
|
assert e.value.status == 400
|
|
with pytest.raises(m01.StoreError):
|
|
st.sheet("GF000220", {"뒷길이": 40}) # 대표 값 아님
|
|
|
|
|
|
def test_목록_저장본_같은_지문이면_안_셈(tmp_path, monkeypatch) -> None:
|
|
calls = []
|
|
|
|
def fake(files, workers=1):
|
|
calls.append(workers)
|
|
return {"판": "2026 상반기 노임", "공종": ["돌공"], "줄": [{"키": "GF000220"}]}
|
|
|
|
monkeypatch.setattr(st.layers, "storage_root", lambda: tmp_path)
|
|
monkeypatch.setattr(st, "build", fake)
|
|
st._MEMO.clear()
|
|
first = st.listing()
|
|
assert first["준비"] is False and first["줄"] == [] # 셈 도중 = 빈 줄
|
|
st._MEMO["job"].join()
|
|
assert st.listing()["줄"] == [{"키": "GF000220"}]
|
|
st._MEMO.clear() # 서버 다시 켬 — 저장본에서 읽음
|
|
assert st.listing()["준비"] is True and len(calls) == 1
|
|
assert list((tmp_path / "_derived" / st.KIND).glob("*.json"))
|
|
st._MEMO.clear()
|
|
|
|
|
|
def test_화면_기준_목록_계산서_담기() -> None:
|
|
pick = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts").read_text("utf-8")
|
|
assert 'from "@ui/ui_template_data_table"' in pick and "fetchStandard()" in pick
|
|
assert "openStandardSheet(row, () => {\n close();\n done(logic);" in pick
|
|
assert "(!work.select.value || x.공종 === work.select.value)" in pick
|
|
sheet = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Sheet.ts").read_text("utf-8")
|
|
assert 'from "@ui/ui_template_calc_row"' in sheet and "createSourceView(" in sheet
|
|
for name in ("Labor_Pick", "Labor_Sheet"):
|
|
text = (ROOT / f"M02_MasterTemplete/M02_MasterTemplete_{name}.ts").read_text("utf-8")
|
|
assert len(text.splitlines()) <= 700
|
|
router = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py").read_text("utf-8")
|
|
assert '@router.get("/logic-lib/standard", dependencies=[Depends(verify_session)])' in router
|
|
assert '@router.post("/logic-lib/sheet", dependencies=[Depends(verify_session)])' in router
|