From e9a25bbf875b3809f140a8a8d02de754852ba378 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 12:28:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EB=8F=8C=20=EC=A2=85=EB=A5=98=20?= =?UTF-8?q?=EC=B6=95=EC=9D=84=20=EC=9D=B4=EC=9D=8C=20=E2=80=94=20=EA=B3=84?= =?UTF-8?q?=EC=88=98=20=EC=85=8B=EC=9D=B4=20=EA=B7=B8=20=EC=B6=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=88=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 랩탑이 stone_kind 칸을 만들어 축이 생김. 지금까지 계수 셋이 축 없이 한 벌(깬돌 계열)로만 돌고 있었음. 원문 근거 셋을 데이터 파일로 뺌 (resources/data_masonry/stone_kind_*) 고임돌 품셈 13-4-3 · 돌 종류 네 줄(야면석/깬잡석/깬돌/견치돌) 채움콘크리트 품셈 13-4-4 [주]① · 두 줄(야면석 33.3% / 깬돌 계열 45%) 뒤채움 교본 7-3 · 깬돌·잡석 1/2 · 야면석 1/3 ⚠ 안 고르면 종전 값 그대로 — 건설품셈 참고자료 한 벌로 돌되 그 사실을 알림. 값을 못 낸다고 멈추면 이미 저장된 프로젝트가 통째로 빔. (실측: 미지정 고임돌 3.915 · 채움콘 5.220 · 막자갈 15.130 — 고치기 전과 같음) ⚠ 만들다 잡은 것 — 표는 「뒤채움 몫」인데 우리 식은 「빼는 몫」이라 뜻이 반대. 그대로 넣었더니 미지정 막자갈이 15.130 → 19.045 로 바뀌었음. 1 에서 빼게 고침. 종전 하드코딩 2/3 이 곧 야면석(1 − 1/3)이었음. ⚠ 지어내지 않는 자리 — 원문 「-」(견치돌 25·30 · 야면석 75)는 null 로 두고 사유를 냄. 모르는 종류도 조용히 안 넘김. 시험 7건 — 미지정이 종전 그대로 · 야면석이 바뀜 · 깬돌이 종전과 같음 · ⚠ 뒤채움 몫을 뒤집어 쓰지 않음 · 「-」를 안 지어냄 · 모르는 종류 · 근거 문구 시험: 720 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관). Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_UnitQuantity.py | 93 ++++++++++++++++++- .../data_masonry/stone_kind_2026-01-01.json | 69 ++++++++++++++ 2 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 resources/data_masonry/stone_kind_2026-01-01.json diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index ed3dcd3c..16edc900 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -30,7 +30,10 @@ from __future__ import annotations +import json import math +from pathlib import Path +from functools import lru_cache from dataclasses import dataclass, field from typing import Any, Iterable @@ -57,6 +60,66 @@ STONE_BACK_LENGTH_TABLE: dict[int, dict[str, float | None]] = { } DEFAULT_BACK_LENGTH_CM = 45 + +#: 돌 종류별 계수표 — 품셈 13-4-3·13-4-4 [주]① · 교본 7-3. +#: ⚠ 지금까지 **건설품셈 참고자료 한 벌**(돌 종류로 안 갈리는 표)로만 돌고 있었다. +#: 그 값이 「깬돌」 계열이라, **자재로는 야면석을 내면서 계수는 깬돌**을 쓰는 어긋남이 +#: 있었다(2026-09-08 지식DB 대조). 랩탑 창이 `stone_kind` 칸을 만들어 축이 생겼다. +STONE_KIND_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_masonry" +STONE_KIND_PREFIX = "stone_kind_" +STONE_KIND_OPTION = "stone_kind" + + +@lru_cache(maxsize=1) +def load_stone_kind_table() -> dict[str, Any]: + """돌 종류별 계수표. 파일이 없으면 **빈 표** — 그러면 종전 값으로 돈다.""" + folder = STONE_KIND_DIR + if not folder.is_dir(): + return {} + files = sorted(folder.glob(STONE_KIND_PREFIX + "*.json")) + if not files: + return {} + try: + return json.loads(files[-1].read_text(encoding="utf-8")) + except (OSError, ValueError): + return {} + + +def stone_coefficients(options: dict[str, Any], back_cm: int) -> tuple[dict[str, Any], str]: + """(계수 한 벌, 알림). 돌 종류를 안 고르면 **종전 값**으로 돌되 그 사실을 알린다. + + ⚠ 값을 못 낸다고 멈추지 않는다 — 이미 저장된 프로젝트가 통째로 비어 버린다. + ⚠ 표에 「-」(그 규격에 그 돌을 안 씀)면 **지어내지 않고** 사유를 낸다. + """ + table = load_stone_kind_table() + key = str(back_cm) + kind = str(options.get(STONE_KIND_OPTION) or "").strip() + if not table: + return {}, "" + if not kind: + fallback = table.get("fallback") or {} + return { + "wedge_stone_m3_per_m2": (fallback.get("wedge_stone_m3_per_m2") or {}).get(key), + "fill_concrete_m3_per_m2": (fallback.get("fill_concrete_m3_per_m2") or {}).get(key), + "backfill_ratio": fallback.get("backfill_ratio_of_back_length"), + "kind": "", + }, str(fallback.get("message") or "") + if kind not in (table.get("kinds") or []): + return {}, f"「{kind}」은(는) 아는 돌 종류가 아니라 계수를 못 골랐습니다" + wedge = ((table.get("wedge_stone_m3_per_m2") or {}).get(kind) or {}).get(key) + fill = ((table.get("fill_concrete_m3_per_m2") or {}).get(kind) or {}).get(key) + ratio = (table.get("backfill_ratio_of_back_length") or {}).get(kind) + note = "" + if wedge is None: + note = f"품셈 13-4-3 에 「{kind} · 뒷길이 {back_cm}㎝」 칸이 비어 있습니다 — 그 규격에 그 돌을 쓰지 않습니다" + return { + "wedge_stone_m3_per_m2": wedge, + "fill_concrete_m3_per_m2": fill, + "backfill_ratio": ratio, + "kind": kind, + }, note + + # 돌쌓기 전개식의 상수 — 실무 수식에 박혀 있던 값을 뺀 것. STONE_MASONRY = { # ⚠ **곱하는 값이 아니라 검산 참고값이다** (2026-09-08 ㉘ 에서 고침). @@ -304,7 +367,23 @@ def stone_masonry( return [], ["높이·연장이 없어 전개하지 않음"] back_cm = _back_length(options) - table = STONE_BACK_LENGTH_TABLE[back_cm] + table = dict(STONE_BACK_LENGTH_TABLE[back_cm]) + # ⚠ **돌 종류로 계수가 갈린다** (품셈 13-4-3 · 13-4-4 [주]① · 교본 7-3). + # 안 고르면 종전 값(건설품셈 참고자료)으로 돌되 그 사실을 알린다 — 값을 못 낸다고 + # 멈추면 이미 저장된 프로젝트가 통째로 빈다. + picked, kind_note = stone_coefficients(options, back_cm) + if kind_note: + notes.append(kind_note) + for key in ("wedge_stone_m3_per_m2", "fill_concrete_m3_per_m2"): + if picked.get(key) is not None: + table[key] = picked[key] + #: ⚠ **표는 「뒤채움 몫」, 우리 식은 「빼는 몫」** — 뜻이 반대라 1 에서 뺀다. + #: 교본은 「뒤채움 = 뒷길이 × (깬돌·잡석 1/2, 야면석 1/3)」이고, 우리 식이 입적에서 + #: 빼는 것은 **돌 몸통**이라 `1 − 뒤채움몫` 이다. 종전 2/3 이 곧 야면석(1 − 1/3)이었다. + #: ⚠ 그대로 넣었더니 미지정 값이 15.130 → 19.045 로 바뀌었다(만들다 잡음). + backfill_share = picked.get("backfill_ratio") + body_ratio = 1.0 - float(backfill_share) if backfill_share is not None else 2.0 / 3.0 + kind_label = str(picked.get("kind") or "") # ⚠ `face_slope_ratio` 는 **레지스트리에 없는 키**다 — 즉 지금은 늘 기본 0.3 으로 돈다. # 상수로 두는 것이 아니라 「칸이 생기면 바로 받는다」는 뜻으로 남겨 둔다. # (키 이름 어긋남으로 저장값이 안 닿던 `back_len_cm` 사고와 구별할 것 — 이쪽은 **칸 자체가 없다**.) @@ -356,13 +435,19 @@ def stone_masonry( ) ) - # 막자갈 = 입적 − (면적 × 뒷길이 × 2/3 + 고임돌). 실무 식 그대로. + # 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌). + # 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3). wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) - rubble = volume - (masonry_area * (back_cm / 100.0) * 2.0 / 3.0 + wedge) + rubble = volume - (masonry_area * (back_cm / 100.0) * body_ratio + wedge) if rubble > 0: components.append( Component( - "막자갈", "㎥", rubble, DESTINATION["막자갈"], "입적 − (면적×뒷길이×2/3 + 고임돌)" + "막자갈", + "㎥", + rubble, + DESTINATION["막자갈"], + f"입적 − (면적×뒷길이×{body_ratio:.4g} + 고임돌)" + + (f" · {kind_label}" if kind_label else ""), ) ) diff --git a/resources/data_masonry/stone_kind_2026-01-01.json b/resources/data_masonry/stone_kind_2026-01-01.json new file mode 100644 index 00000000..13611713 --- /dev/null +++ b/resources/data_masonry/stone_kind_2026-01-01.json @@ -0,0 +1,69 @@ +{ + "schema_version": "1.0", + "dataset_id": "stone_kind_unit", + "effective_date": "2026-01-01", + "note": "돌쌓기 계수는 **돌 종류로 갈린다**. 지금까지 한 벌(깬돌 계열)로만 돌고 있었다 — 2026-09-08 지식DB 대조에서 드러났다.", + "why": "품셈 13-4-3(고임돌)은 돌 종류로 **네 줄**, 13-4-4 [주]①(채움 콘크리트)은 **두 줄**, 교본 7-3 뒤채움은 **두 값**이다. 세 곳이 같은 축인데 우리는 축 없이 하나로 돌고 있었다.", + "option_key": "stone_kind", + "option_note": "레지스트리 `masonry_wet`·`masonry_dry` 의 옵션. 2026-09-08 랩탑 창이 만들었고 키 이름을 두 창이 미리 맞췄다(`back_len_cm` 이름 어긋남 사고를 되풀이하지 않으려고).", + "sources": { + "forest_13_4_3": { + "doc": "산림사업 표준품셈 13-4-3 고임돌 소요량 (단위: ㎥/㎡당)", + "note": "돌 종류 네 줄. 「-」는 그 규격에 그 돌을 안 쓴다는 뜻이라 지어내지 않는다." + }, + "forest_13_4_4_note1": { + "doc": "산림사업 표준품셈 13-4-4 [주]① 찰쌓기 및 찰붙임의 채움 콘크리트 소요량 (㎥/㎡당)", + "note": "두 줄뿐 — 야면석·호박돌(뒷길이의 33.3 %) / 깬잡석·깬돌·견치돌(45 %)." + }, + "textbook_7_3": { + "doc": "임도기술교본 7장 3절 (지식DB 돌쌓기.md §4 · 흙막이.md §4)", + "quote": "뒤채움 = 돌쌓기 표면적 × 뒷길이 × (깬돌·잡석 1/2, 야면석 1/3)" + }, + "construction_reference": { + "doc": "건설공사 표준품셈 [참고자료] 돌쌓기 규격별 소요량", + "note": "⚠ **돌 종류로 안 갈리는 한 벌**이고, 그 값이 우리가 지금까지 쓰던 값이다(고임돌 = 깬돌 줄 · 채움 콘크리트 0.16/0.20/0.25/0.27). 2026 개정안이 이 참고자료를 **「삭제 검토」** 로 두었다." + } + }, + "policy": { + "primary": "산림사업이므로 **산림품셈이 1차 적용**(CLAUDE.md 3장). 건설품셈 값은 미지정일 때의 기본값 근거로만 쓴다.", + "unset_is_flagged": "돌 종류를 안 고르면 **건설품셈 참고자료 한 벌**로 돌되 그 사실을 알린다 — 값을 못 낸다고 멈추면 이미 저장된 프로젝트가 통째로 빈다.", + "no_interpolation": "표에 없는 뒷길이·「-」 칸은 **지어내지 않는다.**" + }, + "kinds": ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"], + "wedge_stone_m3_per_m2": { + "note": "고임돌 — 품셈 13-4-3. 키는 뒷길이(㎝). `null` 은 원문 「-」.", + "야면석·호박돌": { "25": 0.06, "30": 0.07, "35": 0.09, "45": 0.11, "55": 0.14, "60": 0.15, "75": null }, + "깬잡석": { "25": 0.09, "30": 0.11, "35": 0.13, "45": 0.16, "55": 0.19, "60": 0.21, "75": 0.26 }, + "깬돌": { "25": null, "30": 0.10, "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20, "75": 0.25 }, + "견치돌": { "25": null, "30": null, "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20, "75": 0.25 } + }, + "fill_concrete_m3_per_m2": { + "note": "채움 콘크리트 — 품셈 13-4-4 [주]①. **두 줄뿐**이라 견치돌·깬잡석·깬돌이 한 값을 쓴다.", + "야면석·호박돌": { "25": 0.08, "30": 0.10, "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20, "75": 0.25 }, + "깬잡석": { "25": 0.11, "30": 0.14, "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27, "75": 0.34 }, + "깬돌": { "25": 0.11, "30": 0.14, "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27, "75": 0.34 }, + "견치돌": { "25": 0.11, "30": 0.14, "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27, "75": 0.34 } + }, + "backfill_ratio_of_back_length": { + "note": "뒤채움이 뒷길이에서 차지하는 몫 — 교본 7-3. 나머지가 잡석(막자갈)이 된다.", + "야면석·호박돌": 0.3333333333333333, + "깬잡석": 0.5, + "깬돌": 0.5, + "견치돌": 0.5 + }, + "fallback": { + "note": "돌 종류 미지정일 때 — 건설품셈 참고자료 한 벌. **지금까지 쓰던 값 그대로**라 저장된 프로젝트의 값이 안 바뀐다.", + "wedge_stone_m3_per_m2": { "35": 0.12, "45": 0.15, "55": 0.18, "60": 0.20 }, + "fill_concrete_m3_per_m2": { "35": 0.16, "45": 0.20, "55": 0.25, "60": 0.27 }, + "backfill_ratio_of_back_length": 0.3333333333333333, + "backfill_note": "⚠ 이 값은 **뒤채움이 차지하는 몫**이다. 우리 막자갈 식은 입적에서 **돌 몸통**을 빼므로 코드가 `1 − 이 값` 을 쓴다. 종전 하드코딩 2/3 이 곧 야면석(1 − 1/3)이었다.", + "message": "돌 종류를 안 정해 건설품셈 참고자료 값으로 섰습니다 — 구조물 상세 입력에서 고르면 산림품셈 계수로 바뀝니다" + }, + "not_here": { + "note": "이 표가 정하지 않는 것.", + "items": [ + "돌중량(ton/㎡) — 우리 값 0.575/0.88/1.10 은 **실무 관측**(울진 라이브러리)이다. 품셈은 「돌의 중량은 형상·종류·부피를 고려하고 건설품셈 1-3-3 재료의 단위중량을 참고하여 계상한다」로만 두어 **표를 안 준다.** 돌 종류 축과 짝이 맞는지 사용자 확정 대기.", + "전면면적이 정면적인지 비탈면적인지 — 2026 개정안 [주] 「시공량은 석재의 **전면면적**(㎡)을 기준한다」로 용어만 확인됐다." + ] + } +}