일위대가 나머지 460 제목 변화 0 · 9-19-1 못 붙은 줄(굴착기 0.6㎥) 걷힘 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
169 lines
9.8 KiB
Python
169 lines
9.8 KiB
Python
"""표 읽기 ②(2026-09-14 · PLAN 1장) — 「못 붙은 줄」 로 잘못 서던 자리를 **상태가 움직이는지**로 잼.
|
||
|
||
9-21 제근 고르기형 표 안내 글이 `unmatched` 에 들어가 못 붙은 줄로 섬 — 밑수는 사용자 확정
|
||
(건설품셈 1,000㎡당 빌림)이라 안내는 비고(`known_gap_note`)로 옮김(사유는 안 사라짐)
|
||
12-2 표면 마무리 원본이 12-17-1 [주] 문장을 절 제목으로 읽어 표 7장·가짜 갈래 #설치·#철거 가 붙음
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
|
||
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
|
||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||
|
||
|
||
def test_제근은_못_붙은_줄이_아니고_교차참조_사유는_비고에_남음() -> None:
|
||
build = cached_build()
|
||
assert not build.unattached.get("FP-09-21"), build.unattached.get("FP-09-21")
|
||
assert sum(1 for code in build.book.titles if code.startswith("B-FP-09-21#")) == 6
|
||
note = known_gap_note("FP-09-21")
|
||
assert "교차 참조" in note and "1,000㎡당" in note
|
||
|
||
|
||
def test_표면_마무리엔_미장공_표만_있고_가짜_갈래가_없음() -> None:
|
||
nodes = {n["work_item_code"]: n for n in load_work_item_master()["work_items"]}
|
||
assert [t["pum_table_id"] for t in nodes["FP-12-02"]["tables"]] == ["F0334"]
|
||
assert "F0356" in [t["pum_table_id"] for t in nodes["FP-12-17-01"]["tables"]]
|
||
build = cached_build()
|
||
assert [c for c in build.book.titles if c.startswith("B-FP-12-02")] == ["B-FP-12-02"]
|
||
assert not build.unattached.get("FP-12-02")
|
||
|
||
|
||
def test_육상_발파암_1_3m_들어내기는_0_1m_로_읽고_고른_값_버린_값을_사유에() -> None:
|
||
"""③ 원문 「육상 발파암(1~2m)와 동일」(고시 5189·5201줄)은 9-13-14 자기 참조 — 같은 절 암절취·
|
||
용수 발파암이 모두 「(0~1m)와 동일」 이라 0~1m(9-13-13)로 읽음(2026-09-14 브레인 승인)."""
|
||
build = cached_build()
|
||
for code in ("FP-09-13-14", "FP-09-13-15"):
|
||
assert code not in build.partial_ratio and code not in build.component_gaps, code
|
||
# 들어내기 참조 줄은 풀림 — 남는 것은 치즐뿐(기본 조립엔 치즐 단가가 없어 종전대로)
|
||
left = [label for label in build.unattached.get(code, []) if "치즐" not in label]
|
||
assert not left, left
|
||
source = build.factor_sources.get(code, "")
|
||
assert "육상 발파암(0~1m)" in source and "자기 참조" in source, source
|
||
|
||
|
||
def test_비탈면_고르기_발파암_소형브레이커는_1_0_규격으로_코드에_이어짐() -> None:
|
||
"""④ 9-19-3 값 「(2.45+3.05)/2/10㎡」 ← 9-19-1 표 ← [주]① 「소형브레이커는 1㎥/분」 —
|
||
값의 출처가 규격을 정함(2026-09-14 브레인 승인). 범위 별칭이 **코드로** 곧장 이음(이름만
|
||
돌려주면 규격 후보 넷이 다시 흐려짐). 「어어호스」 는 원문 오기 + 카탈로그에 호스 없음."""
|
||
from B09_Estimation.B09_Estimation_ResourceAxis import (
|
||
build_resource_axis,
|
||
load_combined_catalog,
|
||
)
|
||
|
||
axis = build_resource_axis(load_work_item_master(), load_combined_catalog())
|
||
rows = [
|
||
(r.resource_code, str(r.amount)) for r in axis.rows if r.work_item_code == "FP-09-19-03"
|
||
]
|
||
assert ("5210-0010", "0.275") in rows, rows
|
||
left = [u.cell for u in axis.unmatched if u.work_item_code == "FP-09-19-03"]
|
||
assert left == ["어어호스(3/4인치)"], left
|
||
|
||
|
||
def test_토사면_고르기는_두_표가_섞여_값이_불완전하다고_먼저_말함() -> None:
|
||
"""⑤ 사유 먼저(2026-09-14 브레인) — 원문은 절토면·성토면 두 표인데 섞여 보통인부 한 줄만
|
||
섰었음(조용히 틀린 값). ⑤ 본작업(같은 날)으로 두 표를 갈래로 읽어 「불완전」 사유와 섞인
|
||
본표(B-FP-09-19-01)가 함께 걷힘 — 갈래 없는 섞인 값이 다시 서면 빨강."""
|
||
assert "불완전" not in known_gap_note("FP-09-19-01")
|
||
assert "B-FP-09-19-01" not in cached_build().book.titles
|
||
|
||
|
||
#: ⑤ 9-19-1 원문 L5414~5436 — (단위: 10㎡당)이라 ㎡당 = 표값 ÷ 10.
|
||
CUT = "B-FP-09-19-01#절토면·" # 갈래 키는 공백을 지움
|
||
FILL = "B-FP-09-19-01#성토면·"
|
||
|
||
|
||
def _per_m2(code: str) -> dict[str, float]:
|
||
"""자원 줄(비율 줄 「%」 뺌) → ㎡당 수량."""
|
||
from B09_Estimation.B09_Estimation_UnitPrice import detail_of
|
||
|
||
rows = detail_of(cached_build(), code)["rows"]
|
||
return {str(r["ref_code"]): float(r["quantity"]) for r in rows if r.get("unit") != "%"}
|
||
|
||
|
||
def test_토사면_고르기_절토면은_토질_여섯_갈래로_규격까지_이어_섬() -> None:
|
||
"""[주]① 공기압축기 3.5㎥/분 · 소형브레이커 1㎥/분 · 굴착기 0.7㎥ — 범위 별칭이 코드로 이음
|
||
(카탈로그 0.7 은 무한궤도뿐). 「·」 는 그 갈래에 없음(칸 자리만 지킴)."""
|
||
titles = cached_build().book.titles
|
||
cut = sorted(code for code in titles if code.startswith(CUT))
|
||
assert len(cut) == 6, cut
|
||
sand = _per_m2(CUT + "모래ㆍ사질토ㆍ점토ㆍ점질토")
|
||
assert sand == {"1002": 0.005, "X-0201-0070": 0.015}, sand
|
||
soft_rock = _per_m2(CUT + "연암")
|
||
# 공기압축기 3.5 — 운전경비 표 괄호 딸림값 파서 고침(같은 날)으로 기계 층이 서서 붙음
|
||
assert soft_rock == {"1002": 0.046, "X-5205-0035": 0.125, "X-5210-0010": 0.245}, soft_rock
|
||
from B09_Estimation.B09_Estimation_ResourceAxis import (
|
||
build_resource_axis,
|
||
load_combined_catalog,
|
||
)
|
||
|
||
axis = build_resource_axis(load_work_item_master(), load_combined_catalog())
|
||
rock = {
|
||
(r.resource_code, float(r.amount))
|
||
for r in axis.rows
|
||
if r.work_item_code == "FP-09-19-01" and r.variant == "절토면 · 연암"
|
||
}
|
||
assert rock == {("1002", 0.046), ("5205-0035", 0.125), ("5210-0010", 0.245)}, rock
|
||
|
||
|
||
def test_토사면_고르기_성토면_인력_두_갈래_기계는_형식_두_갈래() -> None:
|
||
"""Ⓒ 둘째 줄이 병합 칸 탓에 한 칸 밀려 옴 — 사람 판정표로만 읽음(일반 보정 금지 · 목재틀흙막이
|
||
503만원 전례). 굴착기 0.6㎥ 는 원문이 형식을 안 적음 — 처음(Ⓑ)엔 근거가 없어 안 세웠고,
|
||
2026-09-14 브레인 ② 로 무한궤도·타이어 두 갈래를 세움(고르기는 B08 칸 · 제안 무한궤도)."""
|
||
build = cached_build()
|
||
assert _per_m2(FILL + "인력·점토또는점질토") == {"1002": 0.019}
|
||
assert _per_m2(FILL + "인력·모래또는사질토") == {"1002": 0.017}
|
||
assert _per_m2(FILL + "기계·무한궤도") == {"X-0201-0060": 0.009}
|
||
assert _per_m2(FILL + "기계·타이어") == {"X-0211-0060": 0.009}
|
||
left = build.unattached.get("FP-09-19-01", [])
|
||
assert not any("굴착기" in label for label in left), left
|
||
|
||
|
||
def test_범위_별칭은_칸이_규격을_적었으면_덮지_않음() -> None:
|
||
"""Ⓐ 별칭은 빈 곳을 채우는 것 — 같은 공종 성토면 「굴착기 0.6㎥」 가 0.7 로 덮이면 안 됨."""
|
||
from B09_Estimation.B09_Estimation_ResourceAxis import load_combined_catalog
|
||
from B09_Estimation.B09_Estimation_ResourceAxis_Join import (
|
||
apply_scoped_alias,
|
||
scoped_alias_entry,
|
||
)
|
||
|
||
catalog = load_combined_catalog()
|
||
assert scoped_alias_entry(catalog, "굴착기", "FP-09-19-01").code == "0201-0070"
|
||
side = ["0.6㎥", "시간", "0.09"]
|
||
assert scoped_alias_entry(catalog, "굴착기", "FP-09-19-01", side) is None
|
||
# 이름만 갈아도 옆 칸 0.6 으로 무한궤도가 조용히 골라짐 — 이름도 그대로
|
||
assert apply_scoped_alias(catalog, "굴착기", "FP-09-19-01", side) == "굴착기"
|
||
# 값·식 칸은 규격이 아님 — 9-19-3 소형브레이커 줄(값 0.275 · 식)은 그대로 이음
|
||
value = ["hr", "0.275", "(2.45+3.05)/2/10㎡"]
|
||
assert scoped_alias_entry(catalog, "소형브레이커", "FP-09-19-03", value).code == "5210-0010"
|
||
|
||
|
||
def test_토사면_고르기_원문_주는_값을_안_짓고_비고로() -> None:
|
||
"""Ⓓ [주]③ 착암공은 수량이 없어 줄을 안 지음 · Ⓔ [주]② 풍화암 연암 준용은 선택이라 갈래 아님."""
|
||
note = known_gap_note("FP-09-19-01")
|
||
assert "착암공" in note and "준용" in note, note
|
||
|
||
|
||
def test_제근은_빌린_밑수가_확정이라_갈래가_서면_내역_금액이_섬() -> None:
|
||
"""9-21 원문엔 밑수가 없어 마스터 `basis_missing` 에 F0294 가 남음(원문 사실 그대로) — 그러나
|
||
사용자 확정(건설품셈 3-9-2 「1,000㎡당」 · ÷1,000 은 ChooseOne 이 검)이 있어 내역에서
|
||
「밑수 미확보」로 막히면 안 됨(2026-09-14 브레인). 교차 참조 사유는 비고에 그대로 남음."""
|
||
from B09_Estimation.B09_Estimation_BasisSheet import open_gaps
|
||
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
|
||
from B09_Estimation.B09_Estimation_UnitPrice import detail_of, load_basis_missing
|
||
|
||
assert "F0294" in load_basis_missing()
|
||
build = cached_build()
|
||
assert "FP-09-21" not in build.basis_missing
|
||
assert not [g for g in open_gaps(build) if g["code"] == "FP-09-21"]
|
||
title = "B-FP-09-21#굴착기(무한궤도)0.7·소"
|
||
# ㎡당 환산이 걸린 값 — 1,000㎡당 그대로면 5만원대로 섬
|
||
assert build.book.title(title) and 50 <= int(detail_of(build, title)["total"]) <= 60
|
||
item = {"work_item_code": "FP-09-21", "name": "지장목제거", "spec": "뿌리뽑기", "unit": "㎡",
|
||
"quantity": "1000", "in_bill": True, "variant_axis": "stand_volume_class",
|
||
"variant_value": "굴착기(무한궤도)0.7·소"} # fmt: skip
|
||
result = build_bill({"work_items": [item], "materials": []}, build=build)
|
||
assert not result.missing, result.missing
|
||
row = next(r for r in result.rows if r.code == "FP-09-21")
|
||
assert row.amount_krw and int(row.amount_krw) > 0, row.note
|