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:
@@ -162,6 +162,15 @@ def _revetment():
|
||||
#: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다.
|
||||
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"))
|
||||
result.components, notes = expander(height, length, options, face, face_reason)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
⚠ **형태가 돌쌓기가 아닌 갈래는 식이 없다** — 콘크리트·돌망태·통나무/목재틀·바자.
|
||||
지어내지 않고 **왜 없는지**를 줄로 남긴다.
|
||||
|
||||
⚠ **뒷길이·돌 종류 칸이 기슭막이 등록부에 아직 없다**(2026-09-09 실측). 돌쌓기 식은 그 둘로
|
||||
계수가 갈리는데, 없으면 `_back_length` 가 **조용히 45㎝ 로 돈다**. 값이 나오므로 아무도
|
||||
못 알아채는 자리라 **그 사실을 줄로 드러낸다** — 「값이 있기는 하니 안 보이는」 그것.
|
||||
ⓘ 뒷길이·돌 종류 칸은 기슭막이 등록부에 섰음(`test_revetment_stone_fields`). 안 고른 장은
|
||||
돌쌓기 식이 내는 한 줄(「뒷길이를 안 골라 기본 45㎝」·「돌 종류를 안 정해」)이 그대로 드러냄.
|
||||
2026-09-14 까지 붙이던 「칸이 없어」 줄은 **거짓 + 같은 말 두 번**이라 걷음.
|
||||
"""
|
||||
|
||||
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(
|
||||
height_m: float,
|
||||
@@ -110,17 +104,7 @@ def revetment(
|
||||
"— 형태를 골라야 물량이 섭니다"
|
||||
]
|
||||
|
||||
components, notes = stone_masonry(
|
||||
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
|
||||
return stone_masonry(height_m, length_m, options, STONE_FORMS[form], face, face_reason)
|
||||
|
||||
|
||||
#: 돌골막이 원단위 — 소광리 정본 「골막이(찰)(치수조서연결)」, **개소당**.
|
||||
@@ -414,7 +398,9 @@ OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = {
|
||||
("면목", "m", 2.0 * 1.0, "2 × 1"),
|
||||
),
|
||||
},
|
||||
"콘크리트 L형수로 H=0.2": {
|
||||
# ⚠ 키는 **등록부 선택지 글자 그대로**(「L형수로 H=0.2」) — 2026-09-14 까지 앞에
|
||||
# 「콘크리트 」가 붙어 있어 골라도 **영영 「원단위가 없습니다」**로 섰음.
|
||||
"L형수로 H=0.2": {
|
||||
"rows": (
|
||||
("터파기", "㎥", 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,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) # 같은 사실은 돌쌓기 한 줄이 말함
|
||||
Reference in New Issue
Block a user