From 0d577cf4634d0f146dcd4c8598d5b6906950c28e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 19:51:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EB=8F=8C=EC=8C=93=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EC=9A=B8=EA=B8=B0=EB=A5=BC=20=ED=92=88=EC=85=88=20?= =?UTF-8?q?=ED=91=9C=EC=A4=80=EA=B2=BD=EC=82=AC=ED=91=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=ED=8C=90=EC=A0=95=20(=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=99=95=EC=A0=95=20=E2=91=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ 확정 ⑨ 가 통째로 미구현이었음 — 레지스트리에 `face_slope_ratio` 칸이 없어 **늘 1:0.3** 으로 돌았고, 찰 H=2.5 와 메 H=2.0 이 같은 기울기로 나왔음. 우리 0.3 은 실은 **「메쌓기·성토·직고 1.5m 이하」 한 칸**이었음. 품셈 13-4-4 [주]⑪ <표준경사>를 데이터로 두고 **직고 · 메/찰 · 성토/절토** 셋으로 고름. 메 성토 0.30 0.35 0.40 0.45 0.50 · 메 절토 0.25 0.30 0.35 0.40 0.45 찰 성토 0.25 0.30 0.35 0.40 0.45 · 찰 절토 0.20 0.25 0.30 0.35 0.40 ⚠ 경계값은 **앞 칸** — 원문 표기가 `∼1.5 · ∼3` 이라 이하로 읽음(직고 3.0m → `~3` 칸). 7.0m 은 `~7`, 그보다 커야 마지막 칸. 해석을 데이터·주석·시험에 함께 박음. ⚠ **성토/절토가 저장에 아직 없음** — 레지스트리에 없는 키를 읽지 않고 **성토 열로 잠정** 적용하며 **근거 문구에 그 사실을 적음**. 값이 생기면 한 줄만 바꾸면 됨. ⚠ 저장 제원에 값이 있으면 **그 값이 이김**(확정 ⑨). 자동으로 고른 값은 **저장에 안 박음** — 「정한 적 없음」과 「0.3 으로 정함」이 구별돼야 함. ⚠ 큰돌쌓기는 이 표 대상이 아님 — 교본이 「1:0.3 **이상**」으로만 둠. 그대로 둠. 검증 프로젝트 실측 — 돌쌓기(메) H=2.0 면적 **20.881 → 21.190㎡(+1.5%)**, 찰 H=2.5 는 1:0.3 그대로. 옛 시험 셋이 「늘 1:0.3」을 계약으로 못 박고 있어 기울기를 명시하도록 정정. 시험 761 통과(새 7건 포함). Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_UnitQuantity.py | 70 ++++++++++++++++++- .../masonry_slope_2026-01-01.json | 35 ++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 resources/data_masonry/masonry_slope_2026-01-01.json diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index d730a4ca..39ea57e7 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -258,6 +258,67 @@ class StructureQuantity: billing_quantity: float = 0.0 +#: 표준경사표 — 품셈 13-4-4 [주]⑪. 파일이 없으면 종전 기본값(0.3)으로 돈다. +SLOPE_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_masonry" +SLOPE_PREFIX = "masonry_slope_" +#: 사용자가 덮어쓰는 칸 — 값이 있으면 **그 값이 이긴다**(확정 ⑨). +FACE_SLOPE_KEYS = ("face_slope_ratio",) +#: 성토/절토를 아직 저장이 안 갖고 있다 — 어느 칸을 잠정으로 쓰는지 한 곳에 둔다. +DEFAULT_SLOPE_FACE = "성토" +LEGACY_FACE_SLOPE_RATIO = 0.3 + + +@lru_cache(maxsize=1) +def load_slope_table(path: str | None = None) -> dict[str, Any]: + """표준경사표. 없으면 빈 표 — 그때는 종전 기본값으로 돈다(값을 지어내지 않는다).""" + target = Path(path) if path else None + if target is None: + files = sorted(SLOPE_DIR.glob(SLOPE_PREFIX + "*.json")) if SLOPE_DIR.is_dir() else [] + target = files[-1] if files else None + if target is None or not target.is_file(): + return {} + return json.loads(target.read_text(encoding="utf-8")) + + +def face_slope_ratio( + options: dict[str, Any], *, wet: bool, height_m: float, table: dict[str, Any] | None = None +) -> tuple[float, str]: + """(기울기 1:n 의 n, 근거 문구). + + ⚠ **사용자가 정했으면 그 값이 이긴다**(확정 ⑨). 안 정했으면 품셈 13-4-4 [주]⑪ + <표준경사>에서 **직고 · 메/찰 · 성토/절토** 셋으로 고른다. + ⚠ 자동으로 고른 값을 **저장에 박지 않는다** — 「정한 적 없음」과 「0.3 으로 정함」이 + 구별돼야 한다(오늘 패널 접힘에서 지킨 것과 같은 자리). + ⚠ 경계값은 **앞 칸**이다 — 원문이 `∼3` 이라 직고 3.0m 은 `~3` 칸이다. + """ + for key in FACE_SLOPE_KEYS: + given = _num(options.get(key), 0.0) + if options.get(key) is not None and given > 0: + return given, f"사용자 지정 1:{given:g}" + + found = table if table is not None else load_slope_table() + steps = found.get("steps_m") or [] + bond = "찰쌓기" if wet else "메쌓기" + # ⚠ 성토/절토는 **저장이 아직 안 갖고 있다.** 레지스트리에 없는 키를 읽지 않는다 + # (읽으면 「없는 칸을 읽는」 자리가 되어 옵션 키 검사가 잡는다). 값이 생기면 + # 여기 한 줄만 바꾸면 된다 — 그때까지 성토 열로 잠정 적용하고 근거에 적는다. + face = DEFAULT_SLOPE_FACE + row = ((found.get("table") or {}).get(bond) or {}).get(face) + if not row: + return LEGACY_FACE_SLOPE_RATIO, f"표준경사표를 못 읽어 종전값 1:{LEGACY_FACE_SLOPE_RATIO}" + + index = len(steps) + for i, limit in enumerate(steps): + if height_m <= float(limit): + index = i + break + ratio = float(row[min(index, len(row) - 1)]) + label = f"직고 {height_m:g}m" + (f" ≤{steps[index]:g}m" if index < len(steps) else " 7m 초과") + note = f"품셈 13-4-4 [주]⑪ 표준경사 · {bond} {face} · {label} → 1:{ratio:g}" + note += " · ⚠ 성토/절토가 저장에 없어 **성토 열로 잠정**" + return ratio, note + + def is_collected_stone(options: dict[str, Any]) -> bool: """이 구조물의 돌을 **캐서 쓰는가**. 정한 적이 없으면 「캔다」(확정 ②).""" for key in STONE_SUPPLY_KEYS: @@ -383,7 +444,9 @@ def boulder_masonry( f"{' · '.join(BOULDER_DIAMETERS)}㎝ 중 하나를 고르면 값이 섭니다" ] - slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 레지스트리에 칸 없음 — 기본 0.3 + # ⚠ 큰돌쌓기는 **표준경사표 대상이 아니다** — 교본이 「1:0.3 **이상**」으로만 두었다. + # 저장 제원에 값이 있으면 그것을 쓰고, 없으면 하한 0.3 으로 둔다. + slope_ratio = _num(options.get("face_slope_ratio"), 0.3) constants = BOULDER_MASONRY face_area = height_m * length_m # 기울기 몫은 **한 번만** — 13-4 와 같은 자리다(㉘). @@ -492,7 +555,8 @@ def stone_masonry( # **기본 0.3 의 근거** — 교본 7-3 돌흙막이 기준: 「돌 찰쌓기 3.0m 이하 **1:0.3** / # 돌 메쌓기 2.0m 이하 **1:0.3** / 큰돌쌓기 **1:0.3 이상**(전도 방지)」 # (`지식DB 02_상세설계/구조물/돌쌓기.md §1`, 값은 `data_masonry` 의 `face_slope`). - slope_ratio = _num(options.get("face_slope_ratio"), 0.3) # 전면 기울기 1:0.3 (교본 7-3) + # 전면 기울기 — 품셈 표준경사표로 자동 판정하고, 저장 제원에 값이 있으면 그것이 이긴다. + slope_ratio, slope_basis = face_slope_ratio(options, wet=wet, height_m=height_m) constants = STONE_MASONRY face_area = height_m * length_m # 정면적 @@ -511,7 +575,7 @@ def stone_masonry( "㎡", masonry_area, DESTINATION["돌쌓기"], - f"정면적 × √(1+{slope_ratio}²) — 비탈면적", + f"정면적 × √(1+{slope_ratio:g}²) — 비탈면적 · {slope_basis}", ), ] # ⚠ 고임돌 계수가 **원문에서 「-」**인 칸이 있다(견치돌 25·30 · 야면석 75 · 깬돌 25). diff --git a/resources/data_masonry/masonry_slope_2026-01-01.json b/resources/data_masonry/masonry_slope_2026-01-01.json new file mode 100644 index 00000000..b31b8100 --- /dev/null +++ b/resources/data_masonry/masonry_slope_2026-01-01.json @@ -0,0 +1,35 @@ +{ + "schema_version": "1.0", + "dataset_id": "masonry_standard_slope", + "effective_date": "2026-01-01", + "note": "돌쌓기 표준경사 — **직고 · 메/찰 · 성토/절토** 셋으로 갈린다. 우리는 늘 1:0.3 한 값으로 돌고 있었는데, 그것은 이 표의 「메쌓기·성토·직고 1.5m 이하」 한 칸일 뿐이었다(2026-09-09).", + "source": { + "doc": "산림사업 표준품셈(산림청고시 제2025-82호) 13-4-4 [주]⑪ <표 준 경 사>", + "line": "원문 L7182~7194", + "quote": "돌쌓기의 표준경사는 특수한 경우를 제외하고는 쌓기 높이의 최대치를 취하고 표준 뒷길이 상부에서부터 적용한다" + }, + "policy": { + "user_override": "저장 제원에 `face_slope_ratio` 가 있으면 **그 값이 이긴다**(사용자 확정 ⑨ 「자동 판정 + 사용자가 바꿀 수 있게」).", + "no_write_back": "⚠ 자동으로 고른 값을 **저장에 박지 않는다.** 「정한 적 없음」과 「0.3 으로 정함」이 구별돼야 한다.", + "boundary": "⚠ 원문 표기는 `∼1.5 · ∼3 · ∼5 · ∼7 · 7이상` 이다. 「∼」를 **이하**로 읽어 경계값은 **앞 칸**에 넣는다(직고 3.0m → `~3` 칸). 7.0m 은 `~7` 칸이고 그보다 커야 `7이상` 칸이다.", + "face_unknown": "⚠ 성토/절토를 **저장이 아직 안 갖고 있다.** 구조물 레지스트리의 설치 측 기본이 「자동(성토 쪽)」이고 사면 계열도 층따기가 성토 전용이라 **성토 열로 잠정 적용**하고, 근거 문구에 그 사실을 적는다 — 값이 들어오면 그 칸만 바꾸면 된다." + }, + "steps_m": [1.5, 3.0, 5.0, 7.0], + "table": { + "메쌓기": { + "성토": [0.3, 0.35, 0.4, 0.45, 0.5], + "절토": [0.25, 0.3, 0.35, 0.4, 0.45] + }, + "찰쌓기": { + "성토": [0.25, 0.3, 0.35, 0.4, 0.45], + "절토": [0.2, 0.25, 0.3, 0.35, 0.4] + } + }, + "not_here": { + "note": "이 표가 정하지 않는 것.", + "items": [ + "⚠ 교본 부록 4-5:752 는 **시설 종류**로 가른다 — 「돌 기슭막이의 돌쌓기 기울기는 찰쌓기 1:0.3, **메쌓기 1:0.5**」. 품셈은 직고·성절토 축이라 **축이 다르다.** 기슭막이로 놓은 벽은 사용자가 덮어쓰기로 처리한다(확정 ⑨).", + "큰돌쌓기(13-6)는 이 표의 대상이 아니다 — 교본이 「1:0.3 이상」으로만 두었다." + ] + } +}