"""건설 표 모양 판정·밑수 읽기 — 2026-09-18 브레인 일감(랩탑 메인). 못박는 것 - ⚠ **산림은 건설 층에 닿지 않음** — 건설 규칙은 산림 잣대가 못 가른 표에서만 돌고, 산림 미판정은 0. - 건설 꼴 여섯(기계 손료표 · 율·계수 · 값 없는 설명표 · 건설 직종 · 단위 칸 · 공식 기호)을 가름. - **못 가리면 미판정** — 지어내지 않음. - 밑수 읽기: 건설 꼴 `(용접개소당)`·`(세면기 개당)`·`(m당 : 관길이기준)`·`(일당)` 를 읽되 ⚠ `(1ha당, 100본당)` 처럼 **둘이면 안 고름** · `공㎥당` 은 `㎥` 로 안 깎임 · 「회」는 밑수가 아님. - ⚠ **작업조 표에는 「일」 밑수를 안 붙임**(c3da333e — 붙이면 유로폼 호표가 사라짐). 금액 불변은 `test_work_item_key_gate.py` 원가계산서 시험(직접공사비 5,983,724)이 잼. """ from __future__ import annotations import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity.B08_Quantity_Build_WorkItemMaster_FormsConst import ( # noqa: E402 judged_const_form, ) from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Table import ( # noqa: E402 basis_from_source, crew_table, detect_form, new_report, table_entry, ) FOREST = ROOT / "resources/data_cost_input_value/pum_forest_2026.json" MASTER = ROOT / "resources/data_work_item_master" def table(headers: list[str], rows: list[list[str]]) -> dict: return {"table_id": "T0001", "section": "시험", "line": 3, "headers": headers, "rows": rows} # ── 산림을 흔들지 않음 ──────────────────────────────────────────────────── def test_산림_표는_건설_층에_닿지_않음() -> None: """산림 표는 산림 잣대에서 이미 갈림 — 건설 규칙이 산림 값을 바꿀 수 없음(마스터 산출로 잼).""" master = json.loads((MASTER / "work_item_master_2026-01-01.json").read_text(encoding="utf-8")) reached = [ t["pum_table_id"] for w in master["work_items"] for t in w.get("tables", []) if str(t.get("form_basis", "")).startswith("건설 꼴") ] assert reached == [], f"산림 표가 건설 층까지 내려감: {reached[:5]}" def test_산림_마스터는_형태_미판정이_없음() -> None: master = json.loads((MASTER / "work_item_master_2026-01-01.json").read_text(encoding="utf-8")) assert master["stats"]["form_undetermined"] == 0 # ── 건설 꼴 가름 ───────────────────────────────────────────────────────── def test_기계_손료표는_계수() -> None: got = judged_const_form( table( ["분류 번호", "규격 (ton)", "내용 시간", "상각 비율", "시 간 당(10-7)"], [["0101-0007", "7", "12,000", "0.9", "1,811"]], ) ) assert got and got[0] == "coefficient" @pytest.mark.parametrize( "headers, rows", [ ( ["사용기간별 구 분", "3개월 (%)", "2개년초과 평균손율 (%)"], [["철 물", "30", "85"]], ), # 손율 (["지형종류", "시가지"], [["계수", "0.58"]]), # 보정계수 (["설치간격", "0.6m이하"], [["요 율", "17%"]]), # 요율 ], ) def test_율_계수표는_계수(headers: list[str], rows: list[list[str]]) -> None: got = judged_const_form(table(headers, rows)) assert got and got[0] == "coefficient" def test_값이_없는_설명표는_참조() -> None: got = judged_const_form( table(["노 임 구 분", "산 정 식"], [["노임합계", "P+PO"], ["기본노임", "P"]]) ) assert got == ("reference", "값(숫자) 칸이 없음 — 설명·서식 표") def test_건설_직종이_있으면_소요량() -> None: got = judged_const_form( table(["구 분", "특급 기술자", "고급 기술자"], [["작업계획", "0.5", "1.1"]]) ) assert got and got[0] == "requirement" def test_비고의_산출식은_계수로_안_뒤집음() -> None: """인력품 표의 비고에 산출식이 있어도 **품 표**다(9-5-4 수치지도).""" got = judged_const_form( table( ["구 분", "고급 기술자", "비 고"], [["자동독취", "0.016인", "4매×20분/60분/8시간=0.166일"]], ) ) assert got and got[0] == "requirement" def test_단위_칸과_값이_한_줄이면_소요량() -> None: got = judged_const_form(table(["구 분", "단 위", "브라켓형"], [["비 계 공", "인", "1.40"]])) assert got and got[0] == "requirement" def test_줄_이름이_공식_기호뿐이면_계수() -> None: got = judged_const_form(table(["구 분", "R.C.D", "올케이싱"], [["β", "1.14", "1.08"]])) assert got and got[0] == "coefficient" def test_못_가리면_미판정으로_남김() -> None: """지역·월별 숫자만 있는 표 — 무엇의 값인지 원문으로 못 가름.""" assert judged_const_form(table(["지역별", "1월", "2월"], [["춘천", "7", "5"]])) is None assert detect_form(table(["지역별", "1월"], [["춘천", "7"]]), None)[0] == "undetermined" # ── 밑수 읽기 ──────────────────────────────────────────────────────────── @pytest.mark.parametrize( "line, want", [ ("1-2-1 용접접합('93년 보완)(용접개소당)", (1.0, "개소")), ("7-1-5 카운터형 세면기 설치(분리형)('26년 보완)(세면기 개당)", (1.0, "개")), ("- 1. 이중보온관 부설(m당 : 관길이기준)", (1.0, "m")), ("1-3-10 플륨관 해체('22년 보완)(일당)", (1.0, "일")), ("6-8-1 주철제 게이트 제수밸브 부설 및 접합('23년 보완)(기당)", (1.0, "기")), ("1-3-1 데크플레이트 가스절단('18년 보완)(절단길이 10m당)", (10.0, "m")), ("### 12-19. 강관비계 (단위: 공㎥당)", (1.0, "공㎥")), ("### 6-2-1. 둘레베기 (1ha당, 100본당)", (None, None)), # 밑수가 둘 — 안 고름 ("### 10-4. 중기운반 (단위: 회당)", (None, None)), # 「회」는 밑수 단위가 아님 ], ) def test_건설_꼴_밑수_읽기(line: str, want: tuple) -> None: assert basis_from_source([line, "", "| 머리 |"], 3) == want def test_작업조_표에는_일_밑수를_안_붙임() -> None: """원문이 `(일당)` 이라도 작업조 표면 밑수를 비움 — 밑수는 시공량 열.""" crew = table( ["구 분", "단 위", "수 량", "시 공 량 (㎡)"], [["형틀목공 보통인부", "인 인", "4 1", "25"]], ) assert crew_table(crew) lines = ["### 12-38-3 설치 및 해체 (일당)", "", "| 머리 |"] entry = table_entry(crew, "12-38-3", "12-38-3", "12", lines, None, new_report()) assert (entry["basis_quantity"], entry["basis_unit"]) == (None, None) plain = table(["구 분", "단 위", "수 량"], [["보통인부", "인", "1.4"]]) entry = table_entry(plain, "2-1", "2-1", "2", lines, None, new_report()) assert (entry["basis_quantity"], entry["basis_unit"]) == (1.0, "일")