feat(B08): 돌 종류 축을 이음 — 계수 셋이 그 축으로 갈림
랩탑이 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 ""),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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 개정안 [주] 「시공량은 석재의 **전면면적**(㎡)을 기준한다」로 용어만 확인됐다."
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user