fix(b08): 구조물 표 구멍 넷 — 골막이·바닥막이 내역 수량 0 · 떼흙막이 m당 오표시 · 개거 L형 키 · 기슭막이 거짓 사유

구조물도 남은 종류 그림을 시작하며 시험 제원으로 표를 세워 보다 드러남:
- 골막이(개소당)·바닥막이(㎡당)가 「m · 연장 0」으로 읽혀 구조물도 전 줄 「단위당 못 냄」 · 인계 「물량이 0 이라 내역에 안 세움」 → 내역 단위 개소 1 / ㎡ 면적으로
- 떼흙막이 개소당 값이 연장으로 나뉘어 떼 0.139/m 로 보였음 → 개소당 1.39
- 개거 「L형수로 H=0.2」 — 엔진 키가 「콘크리트 L형수로 H=0.2」라 등록부에서 골라도 영영 안 섰음 → 등록부 글자로
- 기슭막이 사유 「돌 종류·뒷길이 칸이 없어」 — 칸이 선 뒤로 거짓 · 돌쌓기 한 줄과 같은 말 두 번 → 걷음

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-14 06:32:14 +09:00
co-authored by Claude Opus 5
parent ba2c36e803
commit 2c0ae48ac7
3 changed files with 117 additions and 21 deletions
@@ -162,6 +162,15 @@ def _revetment():
#: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다. #: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다.
EXPANDER_WITHHELD: dict[str, str] = {} EXPANDER_WITHHELD: dict[str, str] = {}
#: 전개식이 **개소당·㎡당**으로 값을 내는 종류의 내역 단위와 수량(2026-09-14 구조물도 그림에서 잡음).
#: ⚠ 안 적으면 「m · 연장」으로 읽혀 — 골막이·바닥막이(점 배치라 연장 0)는 **수량 0 이라 내역에 안 서고**
#: 구조물도가 전 줄 「단위당 못 냄」, 떼흙막이는 개소당 값이 연장으로 나뉘어 **떼 0.139/m** 로 보였음.
EXPANDER_BILLING: dict[str, Any] = {
"erosion_check": lambda options: ("개소", 1.0), # 정본 「골막이(찰)(치수조서연결)」 개소당
"soil_guard": lambda options: ("개소", 1.0), # 정본 「떼흙막이」 개소당(평균 붙박이)
"bed_sill": lambda options: ("", _num(options.get("area_m2"), 0.0)), # 정본 돌붙임 ㎡당
}
#: 한 구조물이 **여러 내역 줄**을 낳는 자리. 배수관은 관 자체와 유입부 집수정이 따로 선다 #: 한 구조물이 **여러 내역 줄**을 낳는 자리. 배수관은 관 자체와 유입부 집수정이 따로 선다
#: (품셈도 관부설과 집수정을 다른 공종으로 둔다). 한 줄로 합치면 어느 쪽 물량인지 못 가른다. #: (품셈도 관부설과 집수정을 다른 공종으로 둔다). 한 줄로 합치면 어느 쪽 물량인지 못 가른다.
@@ -305,6 +314,9 @@ def expand(
face, face_reason = structure_face_role(section_mode, options.get("side")) face, face_reason = structure_face_role(section_mode, options.get("side"))
result.components, notes = expander(height, length, options, face, face_reason) result.components, notes = expander(height, length, options, face, face_reason)
result.notes.extend(notes) result.notes.extend(notes)
billing = EXPANDER_BILLING.get(type_id)
if billing is not None and result.components:
result.billing_unit, result.billing_quantity = billing(options)
return result return result
@@ -12,9 +12,9 @@
⚠ **형태가 돌쌓기가 아닌 갈래는 식이 없다** — 콘크리트·돌망태·통나무/목재틀·바자. ⚠ **형태가 돌쌓기가 아닌 갈래는 식이 없다** — 콘크리트·돌망태·통나무/목재틀·바자.
지어내지 않고 **왜 없는지**를 줄로 남긴다. 지어내지 않고 **왜 없는지**를 줄로 남긴다.
⚠ **뒷길이·돌 종류 칸 기슭막이 등록부에 아직 없다**(2026-09-09 실측). 돌쌓기 식은 그 둘로 뒷길이·돌 종류 칸 기슭막이 등록부에 섰음(`test_revetment_stone_fields`). 안 고른 장은
계수가 갈리는데, 없으면 `_back_length` 가 **조용히 45㎝ 로 돈다**. 값이 나오므로 아무도 돌쌓기 식이 내는 한 줄(「뒷길이를 안 골라 기본 45㎝」·「돌 종류를 안 정해」)이 그대로 드러냄.
못 알아채는 자리라 **그 사실을 줄로 드러낸다** — 「값이 있기는 하니 안 보이는」 그것. 2026-09-14 까지 붙이던 「칸이 없어」 줄은 **거짓 + 같은 말 두 번**이라 걷음.
""" """
from __future__ import annotations from __future__ import annotations
@@ -85,12 +85,6 @@ BED_SILL_FORMS: dict[str, dict[str, Any]] = {
}, },
} }
#: 돌쌓기 식이 계수를 가르는 데 쓰는 칸 — 기슭막이 등록부에 아직 없는 것을 알린다.
STONE_SPEC_KEYS: tuple[tuple[str, str], ...] = (
("back_len_cm", "뒷길이"),
("stone_kind", "돌 종류"),
)
def revetment( def revetment(
height_m: float, height_m: float,
@@ -110,17 +104,7 @@ def revetment(
"— 형태를 골라야 물량이 섭니다" "— 형태를 골라야 물량이 섭니다"
] ]
components, notes = stone_masonry( return stone_masonry(height_m, length_m, options, STONE_FORMS[form], face, face_reason)
height_m, length_m, options, STONE_FORMS[form], face, face_reason
)
# ⚠ 계수를 가르는 칸이 없으면 **기본값으로 조용히 돈다** — 그 사실을 드러낸다.
missing = [label for key, label in STONE_SPEC_KEYS if not options.get(key)]
if missing and components:
notes.append(
f"⚠ 기슭막이 제원에 {' · '.join(missing)} 칸이 없어 **돌쌓기 기본값으로 섰음** "
"— 그 값이 고임돌·야면석·채움콘크리트 계수를 가름"
)
return components, notes
#: 돌골막이 원단위 — 소광리 정본 「골막이(찰)(치수조서연결)」, **개소당**. #: 돌골막이 원단위 — 소광리 정본 「골막이(찰)(치수조서연결)」, **개소당**.
@@ -414,7 +398,9 @@ OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = {
("면목", "m", 2.0 * 1.0, "2 × 1"), ("면목", "m", 2.0 * 1.0, "2 × 1"),
), ),
}, },
"콘크리트 L형수로 H=0.2": { # ⚠ 키는 **등록부 선택지 글자 그대로**(「L형수로 H=0.2」) — 2026-09-14 까지 앞에
# 「콘크리트 」가 붙어 있어 골라도 **영영 「원단위가 없습니다」**로 섰음.
"L형수로 H=0.2": {
"rows": ( "rows": (
("터파기", "", 0.31645, "[{(1.11+1.51)÷2×0.2} + {(0.33×0.33)÷2}] × 1.0"), ("터파기", "", 0.31645, "[{(1.11+1.51)÷2×0.2} + {(0.33×0.33)÷2}] × 1.0"),
("되메우기", "", 0.129975, "(0.2+0.30)÷2×0.25×1 + (0.2+0.19)÷2×0.33×1.0"), ("되메우기", "", 0.129975, "(0.2+0.30)÷2×0.25×1 + (0.2+0.19)÷2×0.33×1.0"),
@@ -0,0 +1,98 @@
"""구조물도 그림을 그리려다 드러난 **표 구멍** (2026-09-14 · PLAN 12장 남은 종류 그림).
⚠ 셋 — 값이 서기는 하거나(떼흙막이) 조용히 0 이라 아무 시험도 안 잡던 자리.
① 골막이·바닥막이는 개소당·㎡당 값인데 내역 단위가 「m · 연장 0」 → **수량 0, 내역에 안 섬**
② 떼흙막이는 개소당 값이 연장으로 나뉘어 **떼 0.139/m** 로 보였음
③ 개거 「L형수로 H=0.2」 — 등록부 선택지 글자와 엔진 키가 달라 골라도 영영 안 섬
④ 기슭막이 사유 「돌 종류·뒷길이 칸이 없어」 — 칸이 선 뒤로 거짓 · 같은 말 두 번
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
from B08_Quantity.B08_Quantity_Engine_StructureSheet import build_standard_sheets # noqa: E402
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
def _table(type_id: str, start: float, end: float, **options) -> dict:
structure = {
"structure_id": type_id,
"type_id": type_id,
"start_m": start,
"end_m": end,
"options": options,
}
return build_table([structure], None)
def _sheet(table: dict) -> dict:
return build_standard_sheets(table)["sheets"][0]
def _unit(sheet: dict, name: str) -> float:
return next(row["unit_amount"] for row in sheet["rows"] if row["name"] == name)
def test_골막이는_개소당으로_서고_내역_수량이_1_개소() -> None:
table = _table(
"erosion_check", 50, 50, form="", height_m=1.5, top_length_m=4, bottom_length_m=2
)
item = table["structures"][0]
assert (item["billing_unit"], item["billing_quantity"]) == ("개소", 1.0)
sheet = _sheet(table)
assert sheet["unit_label"] == "개소당" and sheet["unpriced_rows"] == []
row = next(r for r in build_handoff(unit_quantity_table=table)["work_items"] if r["unit"])
assert (row["unit"], row["quantity"]) == ("개소", 1.0) # 전: m · 0.0 → 내역에 안 섬
def test_떼흙막이는_개소당_값이_그대로_보인다() -> None:
sheet = _sheet(_table("soil_guard", 60, 70, form="", height_m=0.5, length_m=10))
assert sheet["unit_label"] == "개소당"
assert abs(_unit(sheet, "") - 1.39) < 1e-9 # 전: 0.139 (m당으로 나뉨)
def test_바닥막이는_제곱미터당() -> None:
table = _table("bed_sill", 80, 80, form="돌붙임(찰)", area_m2=12, height_m=0.3)
sheet = _sheet(table)
assert sheet["unit_label"] == "㎡당" and sheet["billing_total"] == 12
assert abs(_unit(sheet, "돌붙임") - 1.0) < 1e-9 and abs(_unit(sheet, "입적") - 0.45) < 1e-9
def test_개거_L형수로는_등록부_글자로_선다() -> None:
registry = json.loads(
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
)
choices = next(
option["choices"]
for item in registry["types"]
if item["type_id"] == "open_ditch"
for option in item["options"]
if option["key"] == "ditch_spec"
)
for choice in choices:
item = _table("open_ditch", 0, 0, ditch_spec=choice, length_m=5)["structures"][0]
assert item["components"], (choice, item["notes"])
concrete = next(
c
for c in _table("open_ditch", 0, 0, ditch_spec="L형수로 H=0.2", length_m=5)["structures"][
0
]["components"]
if c["name"] == "콘크리트"
)
assert abs(concrete["amount"] - 0.1508 * 5) < 1e-9
def test_기슭막이_사유에_없는_칸_이야기가_없다() -> None:
notes = _table("revetment", 0, 10, form="돌쌓기(찰)", height_m=2, length_m=10)["structures"][0][
"notes"
]
assert not any("칸이 없어" in note for note in notes)
assert any("뒷길이를 안 골라" in note for note in notes) # 같은 사실은 돌쌓기 한 줄이 말함