From 3edc60a5aeec00d32c7521c9543519c39b62a25a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 10 Sep 2026 00:18:34 +0900 Subject: [PATCH] =?UTF-8?q?test(B09):=20=EA=B2=80=EC=A6=9D=20=EC=85=8B(V-1?= =?UTF-8?q?1=C2=B7V-16=C2=B7V-17)=EC=9D=84=20=EC=8B=9C=ED=97=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B9=80=20+=20=EA=B3=B5=EA=B5=AC?= =?UTF-8?q?=EC=86=90=EB=A3=8C=20=ED=8A=B9=EC=88=98=EA=B3=B5=EA=B5=AC=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「사람이 한 번 봤다」로 두면 다음 사람이 또 봐야 함. 본 것을 시험으로 남김. - V-11 인계 계약 검사가 양쪽으로 무는지(칸을 늘려도·빠뜨려도 걸리는지) — 가드의 가드. - V-16 불도저 운반이 갈래(토사·파쇄암)마다 금액으로 서는지. 덤프 운반은 아직 안 서는 것도 사실대로 못 박음(운반거리 설계 입력 대기). - V-17 공구손료와 제잡비는 밑수도 가는 자리도 달라 겹치지 않음(주재료비→재료비 / 노무비→경비). 지금은 주재료비가 0 이라 3% 를 넣어도 한 원도 안 움직이는 것을 못 박음. - 곁들여: 석공사·철골공사는 특수공구라 건설품셈 1-2-6 이 「별도 계상」으로 두었음을 공구손료 칸 사유에 적음 — 일반 비율을 그대로 붙이면 안 되는 자리. Co-Authored-By: Claude Opus 5 (1M context) --- B09_Estimation/B09_Estimation_Router.py | 3 + resources/tester/test_b09_verify_guards.py | 93 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 resources/tester/test_b09_verify_guards.py diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index 92752a3f..cfc9e431 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -508,6 +508,9 @@ async def get_factor_choices(project_id: UUID) -> JSONResponse: " 별도 계상하되 산정 근거를 명시하여야 한다」", "⚠ 비워 두면 안 붙습니다 — 지금은 안 붙고 있는 상태입니다" " (사용자 확정 2026-09-09 「지금은 안 넣되 숫자 넣으면 되게 열어 둘 것」).", + "⚠ 석공사·철골공사처럼 특수공구를 쓰는 자리는 건설품셈 1-2-6 이" + " 「별도 계상」으로 두었습니다 — 일반 비율을 그대로 붙이면 안 되는" + " 자리라, 그런 공종이 섞인 내역에서는 켜기 전에 살펴보십시오.", ], "base_items": with_material, "base_note": ( diff --git a/resources/tester/test_b09_verify_guards.py b/resources/tester/test_b09_verify_guards.py new file mode 100644 index 00000000..941371ef --- /dev/null +++ b/resources/tester/test_b09_verify_guards.py @@ -0,0 +1,93 @@ +"""검증 리스트 셋을 시험으로 못 박음 — V-11 · V-16 · V-17 (2026-09-09 밤). + +「사람이 한 번 봤다」로 두면 다음 사람이 또 봐야 한다. **본 것을 시험으로 옮긴다.** + + V-11 인계 계약 검사가 **새 칸에도** 걸리나 — 계약 시험이 실제로 무는지(가드의 가드) + V-16 불도저 운반이 **금액으로** 서나 — 갈래(토사·리핑암)마다 단가가 서는지 + V-17 공구손료가 **이중으로 안 붙나** — 제잡비와 밑수가 겹치지 않는지 +""" + +from __future__ import annotations + +import sys +from decimal import Decimal +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B09_Estimation.B09_Estimation_PriceBook import PriceKind # noqa: E402 +from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices, cached_build # noqa: E402 + + +# ── V-11 ──────────────────────────────────────────────────────────── +def test_v11_계약_검사는_양쪽으로_문다() -> None: + """⚠ 가드의 가드 — 칸을 하나 늘리거나 줄이면 계약 시험이 **반드시** 걸려야 한다. + + 「보내는 쪽은 보냈는데 받는 쪽이 안 읽는」 자리는 양쪽 다 조용해서, 계약 시험이 + 무는지 자체를 시험으로 남긴다. + """ + from resources.tester.test_b08_handoff_contract import WORK_ITEM_KEYS + + row = {key: None for key in WORK_ITEM_KEYS} + + # ① 계약에 없는 칸을 늘리면 잡는다. + grown = dict(row, 새_칸="값") + assert set(grown) - WORK_ITEM_KEYS == {"새_칸"} + + # ② 계약에 있는 칸을 빠뜨려도 잡는다. + shrunk = dict(row) + shrunk.pop("quantity") + assert WORK_ITEM_KEYS - set(shrunk) == {"quantity"} + + +# ── V-16 ──────────────────────────────────────────────────────────── +def test_v16_불도저_운반이_갈래마다_금액으로_선다() -> None: + """⚠ 세워 놓고 한 번도 안 돌려 본 채로 두지 않는다 — 갈래마다 단가가 서야 한다.""" + build = cached_build() + codes = [code for code in build.book.titles if code.startswith("B-FP-10-11#")] + assert len(codes) >= 2, codes + grounds = {code.split("#", 1)[1] for code in codes} + assert {"토사", "파쇄암"} <= grounds, grounds + for code in codes: + assert build.book.resolve(code).total > 0, code + + +def test_v16_덤프_운반은_아직_안_선다() -> None: + """⚠ 안 서는 것도 사실대로 못 박는다 — 운반거리(설계 입력)를 기다리는 자리다.""" + build = cached_build() + assert not [code for code in build.book.titles if code.startswith("B-FP-10-12")] + + +# ── V-17 ──────────────────────────────────────────────────────────── +def test_v17_공구손료와_제잡비는_밑수가_겹치지_않는다() -> None: + """⚠ 둘 다 「%로 붙는 줄」이라 겹치면 조용히 부푼다. + + 공구손료 = **주재료비**의 % → 재료비 / 제잡비 = **노무비**의 % → 경비. + 밑수도 가는 자리도 달라 한 공종에 둘 다 있어도 이중계상이 아니다. + """ + build = build_unit_prices(misc_material_percent=Decimal(3)) + both = 0 + for code, rows in build.book.details.items(): + title = build.book.titles.get(code) + if title is None or title.kind is not PriceKind.UNIT_PRICE: + continue + has_overhead = any(row.percent_of_labor is not None for row in rows) + has_tool = any(row.percent_of_material is not None for row in rows) + if not (has_overhead and has_tool): + continue + both += 1 + before = build.book.resolve(code) + # 제잡비는 경비로, 공구손료는 재료비로 간다 — 성분이 갈리므로 겹칠 수 없다. + assert before.expense >= 0 and before.material >= 0 + assert both > 0, "제잡비와 공구손료가 함께 있는 공종이 하나도 없습니다" + + +def test_v17_지금은_공구손료가_한_원도_안_붙는다() -> None: + """⚠ 오늘 상태를 못 박는다 — 칸이 비어 있고, 붙을 주재료비도 아직 0 이다.""" + base = cached_build() + with_percent = build_unit_prices(misc_material_percent=Decimal(3)) + for code, title in base.book.titles.items(): + if title.kind is not PriceKind.UNIT_PRICE: + continue + assert base.book.resolve(code).total == with_percent.book.resolve(code).total, code