tmp/ 가 창끼리 안 건너가는 것이 확정돼(랩탑이 시간 두고 두 번 확인) 시험·예외가 저절로 건너가도록 git 안으로 옮김. 사용자 확정. - 내용은 하나도 안 고침 — 자리만 옮김. tmp/tests 는 남겨 둠. - 같은 이름이 이미 있던 64 개는 랩탑 것을 그대로 두고 건너뜀. - helper_b05_*.js 둘은 랩탑이 .cjs 로 이미 올린 것과 **줄바꿈만 다른 같은 내용**이라 복사본을 도로 뺌(시험이 .cjs 를 부름). - resources/tester/ 에서 전체 1176 통과 · 29 건너뜀 · 실패 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
153 lines
6.5 KiB
Python
153 lines
6.5 KiB
Python
"""**코드가 없으면 반드시 막힘 표시가 붙는다** (2026-09-09 랩탑 메인 제보로 세운 불변식).
|
|
|
|
⚠⚠ 오늘 세운 규칙 「막혔다고 말하기 전에 `blocked_kind` 를 볼 것」의 **뒤집힌 얼굴**이다.
|
|
보는 쪽을 그렇게 고쳤으면 **다는 쪽도 빠짐없이 달아야** 한다. 안 그러면 받는 쪽이
|
|
「코드도 없고 막힘 표시도 없는 **멀쩡한 줄**」로 읽어 **금액이 조용히 빠진다.**
|
|
|
|
실제로 그랬다 — `측구터파기 · 굴삭기+브레카` 가 `work_item_code: null` · `blocked_kind: null`
|
|
이었고 화면에 아무 말도 안 떴다. 인계본의 `bill_flag_warnings`·`unmatched_work_items` 에는
|
|
**이미 실려 있었다** — 목록은 있는데 **줄에 표시가 없었던 것**이다. 줄 단위로 보는 쪽은
|
|
목록을 안 본다.
|
|
|
|
⚠ 예외 둘 — 이 둘만 코드 없이 서도 된다.
|
|
① **묶음 줄**(`composite_parts`) — 무엇으로 묶이는지 조각에 적혀 있다
|
|
② **내역에 안 서는 줄**(`in_bill: False`) — 합계·무대처럼 **막힌 게 아니라 안 세는 것**.
|
|
다만 그쪽도 **까닭은 있어야** 한다(`in_bill_reason` 또는 `blocked_kind`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
|
|
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
|
|
|
|
|
|
def 집계(item: str | None, group: str = "흙깎기") -> dict:
|
|
return {"rows": [{"group": group, "item": item, "unit": "㎥", "amount": 100.0}]}
|
|
|
|
|
|
def 인계(**kwargs) -> list[dict]:
|
|
return build_handoff(**kwargs)["work_items"]
|
|
|
|
|
|
def test_시공법을_못_고른_줄에_막힘_표시가_붙는다() -> None:
|
|
"""⚠ 실측 자리 — 「굴삭기+브레카」는 지반 갈래가 아니라 **시공법**이라 매핑이 안 맞았다."""
|
|
rows = 인계(summary_table=집계("리핑암"))
|
|
row = next(r for r in rows if r["name"] == "흙깎기")
|
|
assert row["work_item_code"] is None
|
|
assert row["blocked_kind"] == "input_missing" # 시공법을 고르면 풀린다
|
|
assert "시공법" in row["blocked_reason"]
|
|
|
|
|
|
def test_시공법을_고르면_코드가_붙고_표시가_사라진다() -> None:
|
|
# ⚠ 시공법 값은 **영문 키**다(`ripping`·`blasting`) — 한글을 넣으면 안 풀린다.
|
|
rows = 인계(summary_table=집계("리핑암"), ground_methods={"리핑암": "ripping"})
|
|
row = next(r for r in rows if r["name"] == "흙깎기")
|
|
assert row["work_item_code"] is not None
|
|
assert row["blocked_kind"] is None
|
|
|
|
|
|
def test_품셈을_못_이은_줄은_다른_갈래로_적힌다() -> None:
|
|
"""⚠ 「사용자가 고르면 풀림」과 「우리가 매핑을 못 이음」은 **다음에 할 일이 다르다**."""
|
|
rows = 인계(summary_table=집계(None, group="지장목제거"))
|
|
row = next(r for r in rows if r["name"] == "지장목제거")
|
|
assert row["work_item_code"] is None
|
|
assert row["blocked_kind"] == "unit_data_missing"
|
|
assert "못 이었습니다" in row["blocked_reason"]
|
|
|
|
|
|
def test_불변식_모든_줄에_대해_선다() -> None:
|
|
"""⚠ 한 자리를 고치는 것으로 끝내지 않는다 — **줄 빌더 여덟 곳 전부**를 건다."""
|
|
unit = build_table(
|
|
[
|
|
{
|
|
"structure_id": "s1",
|
|
"type_id": "masonry_wet",
|
|
"start_m": 0.0,
|
|
"end_m": 10.0,
|
|
"options": {"height_m": 2.5, "length_m": 10.0, "back_len_cm": 45},
|
|
},
|
|
{
|
|
"structure_id": "w1",
|
|
"type_id": "retaining_wall",
|
|
"start_m": 20.0,
|
|
"end_m": 30.0,
|
|
"options": {"height_m": 2.0, "length_m": 10.0, "form": "반중력식"},
|
|
},
|
|
]
|
|
)
|
|
haul = {
|
|
"rows": [
|
|
{"equipment": "dump_truck", "ground": "토사", "volume_m3": 50.0},
|
|
{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0},
|
|
{"equipment": "모르는장비", "ground": "토사", "volume_m3": 5.0},
|
|
]
|
|
}
|
|
rows = 인계(
|
|
summary_table=집계("리핑암"),
|
|
haul_table=haul,
|
|
unit_quantity_table=unit,
|
|
preparation_table={"rows": []},
|
|
)
|
|
나쁜 = [
|
|
r["name"]
|
|
for r in rows
|
|
if not r.get("work_item_code")
|
|
and not r.get("composite_parts")
|
|
and r.get("in_bill")
|
|
and not r.get("blocked_kind")
|
|
]
|
|
assert 나쁜 == [], f"코드도 막힘 표시도 없이 내역에 서는 줄: {나쁜}"
|
|
|
|
# 내역에 안 서는 줄도 **까닭 없이** 있으면 안 된다.
|
|
까닭없음 = [
|
|
r["name"]
|
|
for r in rows
|
|
if not r.get("in_bill")
|
|
and not r.get("blocked_kind")
|
|
and not str(r.get("in_bill_reason") or "").strip()
|
|
]
|
|
assert 까닭없음 == [], f"안 서는 까닭이 안 적힌 줄: {까닭없음}"
|
|
|
|
|
|
def test_무대는_막힌_것이_아니다() -> None:
|
|
"""⚠ 무대(소운반 20m 이내)는 **품에 포함**이라 안 세우는 것이지 막힌 것이 아니다.
|
|
|
|
여기에 막힘 표시를 달면 「만들어야 할 것」 목록에 올라 없는 일이 생긴다.
|
|
"""
|
|
rows = 인계(haul_table={"rows": [{"equipment": "free_haul", "ground": "토사", "volume_m3": 10.0}]})
|
|
row = next(r for r in rows if r["haul_equipment"] == "free_haul")
|
|
assert row["in_bill"] is False
|
|
assert row["blocked_kind"] is None
|
|
assert "품에 포함" in row["in_bill_reason"]
|
|
|
|
|
|
def test_물량_0_인_구조물은_내역에_안_선다() -> None:
|
|
"""⚠ 물넘이포장이 면적을 안 받아 **`0.0 ㎡` 로 내역에 서고 있었다**(2026-09-09 감사).
|
|
|
|
코드가 붙어 있어 **0 원 줄**이 만들어지고 화면에는 「값이 있는 줄」로 보인다 —
|
|
0 은 「없음」과 구별이 안 된다(오늘 표토제거에서 겪은 자리와 같은 결).
|
|
줄은 그대로 넘기되 **내역에서 빼고 까닭을 적는다.**
|
|
"""
|
|
unit = build_table(
|
|
[
|
|
{
|
|
"structure_id": "f1",
|
|
"type_id": "ford_pavement",
|
|
"start_m": 0.0,
|
|
"end_m": 10.0,
|
|
"options": {"thickness_cm": 20, "length_m": 10.0},
|
|
}
|
|
]
|
|
)
|
|
row = next(r for r in 인계(unit_quantity_table=unit) if r["name"] == "물넘이포장")
|
|
assert row["quantity"] == 0.0
|
|
assert row["in_bill"] is False
|
|
assert "물량이 0" in row["in_bill_reason"]
|
|
assert row["blocked_kind"] == "input_missing"
|