From cbadb7fd93565d8a69b9a9902cd5e91fc577f679 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 11:56:57 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(B08):=20=EB=8F=8C=EA=B3=A8=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=C2=B7=20=EB=96=BC=ED=9D=99=EB=A7=89=EC=9D=B4=20?= =?UTF-8?q?=EC=9B=90=EB=8B=A8=EC=9C=84=20=E2=80=94=203=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=20=EC=84=A0=EC=96=B8=20=EB=B2=94=EC=9C=84=20=EB=8B=AB=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 돌골막이(erosion_check) - 소광리 정본 「골막이(찰)(치수조서연결)」 식을 개소당으로 전개. 정면적 사다리꼴 − 방수로 파형강관 → 돌쌓기 → 평균두께 → 입적 → 야면석·고임돌·막자갈·채움콘크리트·물구멍관 → 바닥파기·잔토·되메우기 11줄. - 거울 시험이 정본 W열과 소수점까지 일치(19.62 / 2.625 / 20.56 / 19.58 / 3.34 / 10.54 / 4.45 / 4.91 / 8.61 / 2.74 / 5.87). - 엑셀의 줄별 INT(x*100)/100 자름을 그대로 옮김 — 다음 줄이 잘린 값을 받음. - 두께식 주석 정정: 「0.45 + 0.1H」의 0.45 는 상수가 아니라 뒷길이 ℓ3 (정본 C26 이 P6/100 을 읽음). 골막이도 뒷길이 기반임. - 등록부에 없는 칸 셋(뒷길이·돌 종류·방수로 유무)은 붙박이로 돌되 사유를 냄. 떼흙막이(soil_guard) - 형식 「떼」가 들어와 정본 「떼흙막이」 표가 섬 — 떼 1.39㎡ · 터파기 0.17㎥. - 사용자 확정 4차대로 그림·옵션은 기슭막이와 한 벌, 수량 데이터는 분리. 나머지 일곱 형식은 원단위 미확보 사유로 뜸. Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_UnitQuantity.py | 21 +- ..._Quantity_Engine_UnitQuantity_Revetment.py | 301 +++++++++++++++--- 2 files changed, 272 insertions(+), 50 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 97260c1a..d69923b7 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -172,9 +172,10 @@ def stone_coefficients(options: dict[str, Any], back_cm: int) -> tuple[dict[str, # 돌쌓기 전개식의 상수 — 실무 수식에 박혀 있던 값을 뺀 것. #: ⚠⚠ **기슭막이 계열(돌쌓기 찰·메) 전용이다. 다른 구조물로 넓히지 말 것**(2026-09-09). #: 골막이는 **두께식부터 다르다** — 소광리 숨김탭 「골막이(찰)(치수조서연결)」 C26: -#: 평균두께 = {(0.45 + 0.1×H) + (0.45 + 0.4×H)} ÷ 2 -#: 우리가 확정 ② 로 버린 옛 식과 **글자까지 같다.** 그 식은 틀린 식이 아니라 **골막이 식**이고, -#: 그것을 기슭막이에까지 쓰고 있었던 것이 어긋남의 정체였다. +#: 평균두께 = {(ℓ3 + 0.1×H) + (ℓ3 + 0.4×H)} ÷ 2 +#: ⚠ 앞서 이 식을 「0.45 + 0.1H」로 적어 두었으나 **0.45 는 상수가 아니라 뒷길이 ℓ3** +#: 이다(C26 이 `P6/100` 을 읽고 P6 = 45㎝). ⇒ 두 식 다 뒷길이 기반이고 **더하는 몫만** +#: 다르다. 우리가 확정 ② 로 버린 옛 식이 뒷길이를 안 보던 것과는 그 점이 다르다. #: ⚠ 상수만 보고 골막이에 가져다 쓰면 **값이 나오므로 아무 시험도 안 잡는다.** #: ⚠ 골막이는 그 밖에도 셋이 더 다르다 — 밑수가 **돌쌓기 + 돌붙임**(기슭막이는 돌쌓기만) · #: 단위가 **개소당**(기슭막이는 m당) · 정면적이 **사다리꼴**. @@ -966,11 +967,13 @@ EXPANDERS = { # 이고 그 안의 계산이 같다. 갈래·사유는 `_UnitQuantity_Revetment` 가 든다(늦게 부른다 — # 그쪽이 이 모듈의 `stone_masonry` 를 쓰므로 위에서 부르면 맞물린다). "revetment": lambda h, l, o, f=None, r="": _revetment()(h, l, o, f, r), - # 골막이·바닥막이는 돌 계열이나 **돌쌓기 식을 그대로 못 쓴다** — 까닭은 그 모듈에 적었다. - "erosion_check": lambda h, l, o, f=None, r="": _stone_kin()("erosion_check"), + # 골막이는 **개소당**이고 정면적이 사다리꼴이라 상장·하장·높이를 본다 + # (정본 「골막이(찰)(치수조서연결)」). 돌쌓기 식을 빌려 쓰지 않는다 — 까닭은 그 모듈에 적었다. + "erosion_check": lambda h, l, o, f=None, r="": _erosion_check()(h, o), # 개거는 **m당** 원단위라 연장이 밑수다(정본 「개거(150-200)」·「L형수로-(201)」). "open_ditch": lambda h, l, o, f=None, r="": _open_ditch()(l, o), - # 흙막이는 **아직 못 세움** — 정본에 떼흙막이 하나뿐인데 우리 형식 칸에 「떼」가 없음. + # 흙막이는 **「떼」만 섬**(정본 「떼흙막이」 개소당). 나머지 일곱 형식은 원단위 미확보. + # ⚠ 확정 4차 — 그림·옵션은 기슭막이와 한 벌이되 **수량 데이터는 분리**. "soil_guard": lambda h, l, o, f=None, r="": _soil_guard()(o), # 바닥막이는 **돌붙임 ㎡당**이라 높이·연장이 아니라 **면적**이 밑수다(정본 「돌붙임L3=…」). "bed_sill": lambda h, l, o, f=None, r="": _bed_sill()(_num(o.get("area_m2"), 0.0), o), @@ -1005,11 +1008,11 @@ def _bed_sill(): return bed_sill -def _stone_kin(): +def _erosion_check(): """골막이·바닥막이 사유 함수 — 늦게 가져온다(서로 부르는 것을 푸는 자리).""" - from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import stone_kin_withheld + from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revetment import erosion_check_dam - return stone_kin_withheld + return erosion_check_dam def _revetment(): diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index 23c0ff91..b4e4cd14 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -19,9 +19,11 @@ from __future__ import annotations +import math +from decimal import ROUND_HALF_UP, Decimal from typing import Any -from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, stone_masonry +from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, _num, stone_masonry #: 형태 → 돌쌓기 갈래(찰이면 True). 여기 없는 형태는 전개식이 없다. STONE_FORMS: dict[str, bool] = { @@ -43,28 +45,6 @@ WITHHELD_FORMS: dict[str, str] = { "바자": "바자얽기(품셈 5-15)는 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4)", } -#: 골막이·바닥막이 — **왜 아직 안 서는지**. 둘 다 돌 계열이나 돌쌓기 식을 그대로 못 쓴다. -#: ⚠ 근거 없는 식을 만들지 않는다(2026-09-09 확인). 칸·표가 오면 그때 붙인다. -#: -#: ⚠⚠ **골막이를 돌쌓기와 합치지 말 것** — 「같은 돌쌓기니 한 함수로」가 되기 쉬운 자리다. -#: 소광리 정본 「골막이(찰)(치수조서연결)」과 「돌기슭막이」를 나란히 재면 **넷이 다르다**: -#: -#: 두께식 골막이 (0.45+0.1H)/(0.45+0.4H) ↔ 기슭막이 뒷길이 기반(ℓ3+0.30 …) -#: 밑수 골막이 **돌쌓기 + 돌붙임** ↔ 기슭막이 돌쌓기만 -#: 단위 골막이 **개소당** ↔ 기슭막이 m당 (확정 ⑦ 「통일 안 함」) -#: 정면적 골막이 **사다리꼴**(상장·하장) ↔ 기슭막이 직사각(H × 1) -#: -#: ⓘ 같은 것도 있다 — 막자갈 **2/3**·야면석 **0.88 ton/㎡**·물구멍 **2㎡/개소**· -#: 기울기 몫 **1.04**·고임돌 **0.15**·채움콘크리트 **0.2** 는 두 시트가 같은 값이다. -#: 우리 기본값이 어디서 왔는지의 답이기도 하다. -STONE_KIN_WITHHELD: dict[str, str] = { - "erosion_check": ( - "골막이는 돌쌓기와 **두께식·밑수·단위가 다름**(소광리 정본 「골막이(찰)(치수조서연결)」 " - "실측) — 평균두께 {(0.45+0.1H)+(0.45+0.4H)}÷2 · 밑수 「돌쌓기+돌붙임」 · 개소당. " - "게다가 정면적이 사다리꼴이라 **상장·하장 칸이 있어야** 셈이 서는데 등록부에 없음" - ), -} - #: 돌붙임 원단위 — 소광리 정본 「돌붙임L3=45(사방)」(찰) · 「돌붙임L3=30(야면석메붙임)」(메). #: ⚠⚠ **돌쌓기 표(13-4)와 합치지 말 것.** 값이 겹쳐 보여 합치고 싶어지는 자리다: #: @@ -142,15 +122,235 @@ def revetment( return components, notes -def stone_kin_withheld(type_id: str) -> tuple[list[Component], list[str]]: - """골막이·바닥막이 — **식을 안 두는 까닭**만 낸다. +#: 돌골막이 원단위 — 소광리 정본 「골막이(찰)(치수조서연결)」, **개소당**. +#: 「돌-골막이치수」 탭이 개소별 상장·하장·높이를 받아 **평균치수**를 내고, 이 표가 그 평균 +#: 하나로 개소당 수량을 낸다. 우리는 개소마다 제 치수로 돌린다 — 밑수가 같으므로 식은 그대로다. +#: +#: ⚠⚠ **돌쌓기(기슭막이) 식과 합치지 말 것.** 넷이 다르다: +#: +#: 정면적 **사다리꼴** (상장+하장)÷2×H − 파형강관 ↔ 기슭막이 직사각 H×1 +#: 두께식 (ℓ3+0.1H) / (ℓ3+0.4H) 의 평균 ↔ 기슭막이 ℓ3+0.30 / +0.30(H−1) +#: 밑수 **돌쌓기 + 돌붙임** ↔ 기슭막이 돌쌓기만 +#: 단위 **개소당** ↔ 기슭막이 m당 (확정 ⑦ 「통일 안 함」) +#: +#: ⓘ 앞서 이 두께식을 「0.45 + 0.1H」로 적어 두었으나 **0.45 는 상수가 아니라 뒷길이 ℓ3** +#: 이다(정본 C26 이 `P6/100` 을 읽는다 — P6 = 45㎝). 두 식 모두 뒷길이 기반이고 +#: **더하는 몫만 다르다.** +#: ⓘ 같은 것 — 막자갈 **2/3** · 야면석 **0.88 ton/㎡** · 물구멍 **2㎡/개소 × 0.5m** · +#: 고임돌 **0.15** · 채움콘크리트 **0.2** 는 두 시트가 같은 값이라 표를 함께 쓴다. +#: +#: ⚠ **엑셀의 자름·반올림을 그대로 옮긴다** — 정본이 줄마다 `INT(x*100)/100` 으로 자르고 +#: **다음 줄이 그 잘린 값을 받는다**(막자갈·잔토가 그렇다). 안 자르면 정본과 갈린다. +#: +#: ⚠ **방수로 파형강관을 늘 뺀다** — 정본이 상수 `(0.4 × 0.4) × 3.14` 로 박아 두었고 +#: (Ø0.8 파형강관, 울진 관급 목록에 있는 규격) 방수로 치수가 0 인 개소에서도 뺀다. +#: **「방수로 없음」을 고를 칸이 등록부에 없어** 그대로 따른다 — 그 사실을 줄로 낸다. +EROSION_CHECK_DAM = { + "spillway_pipe_r_m": 0.4, # 정본 상수 — 0.4 × 0.4 × 3.14 + "spillway_pipe_pi": 3.14, + "thickness_top_per_m": 0.1, # 상부 두께 = ℓ3 + 0.1H + "thickness_bottom_per_m": 0.4, # 하부 두께 = ℓ3 + 0.4H + "rubble_body_ratio": 2 / 3, # 막자갈에서 뺄 몸통 몫 + "trench_extra_m": 0.3, # 바닥파기 폭 = 평균두께 + 0.3 + "trench_depth_m": 0.5, # 바닥파기 깊이 + "default_slope_ratio": 0.3, # 반수면 비탈 1:0.3 — 정본 붙박이 +} - ⚠ 돌 계열이라 `stone_masonry` 를 부르고 싶어지는 자리다. 실제로 부르면 **값이 나오므로** - 아무 시험도 안 잡는다 — 오늘 큰돌쌓기에서 같은 일이 있었다(직경 60~80㎝ 가 「뒷길이 - 45㎝」 계수로 돌던 자리). 그래서 **일부러 안 부른다.** - """ - why = STONE_KIN_WITHHELD.get(type_id) - return [], [why] if why else [] +#: 골막이가 세우는 형식. 정본 탭이 **찰쌓기 하나**뿐이다. +EROSION_CHECK_FORMS: frozenset[str] = frozenset({"돌"}) + +#: 나머지 형식과 **왜 없는지**. 지어내지 않는다. +EROSION_CHECK_WITHHELD: dict[str, str] = { + "돌망태": "돌망태는 품셈 13-8 이고 규격 축이 망태 치수라 돌쌓기 표를 못 씀 — 원단위 미확보", + "콘크리트": "콘크리트 골막이는 몸체 두께·저판이 정본에 없어 물량이 서지 않음", + "통나무": ( + "통나무 골막이는 품셈 13-13(목재틀)이고 밑수가 ㎥당 목공 품이라 축이 다름 — 원단위 미확보" + ), + "바자": "바자얽기(품셈 5-15)는 별도 일위대가를 만들어 잇기로 확정(2026-09-09 ⑧-4)", + "흙": "흙골막이는 정본에 탭이 없고 품셈 장도 다름 — 원단위 미확보", +} + + +def _floor2(value: float) -> float: + """엑셀 `INT(x*100)/100` · `ROUNDDOWN(x,2)`.""" + return math.floor(value * 100) / 100 + + +def _round2(value: float) -> float: + """엑셀 `ROUND(x,2)` — 파이썬 기본(짝수 반올림)과 갈리는 자리라 규칙을 맞춘다.""" + return float(Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) + + +def _ceil2(value: float) -> float: + """엑셀 `ROUNDUP(x,2)`.""" + return math.ceil(value * 100) / 100 + + +def erosion_check_dam( + height_m: float, options: dict[str, Any] +) -> tuple[list[Component], list[str]]: + """돌골막이 1개소 전개 — 정본 「골막이(찰)(치수조서연결)」의 식을 그대로 돌린다.""" + from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( + DESTINATION, + STONE_BACK_LENGTH_TABLE, + STONE_MASONRY, + _back_length, + fill_concrete_mpa, + ) + + form = str(options.get("form") or "").strip() + if form not in EROSION_CHECK_FORMS: + why = EROSION_CHECK_WITHHELD.get(form) + if why: + return [], [f"골막이 형식 「{form}」 — {why}"] + return [], [ + f"골막이 형식이 정해지지 않았습니다(지금 「{form or '빈 값'}」) " + "— 형식을 골라야 물량이 섭니다" + ] + + top_m = _num(options.get("top_length_m"), 0.0) + bottom_m = _num(options.get("bottom_length_m"), 0.0) + if top_m <= 0 or bottom_m <= 0 or height_m <= 0: + return [], [ + "골막이 정면적이 **사다리꼴**이라 상장ⓐ·하장ⓑ·높이가 다 있어야 셈이 섭니다 " + f"(지금 상장 {top_m:g}m · 하장 {bottom_m:g}m · 높이 {height_m:g}m)" + ] + + const = EROSION_CHECK_DAM + notes: list[str] = [] + + back_cm = _back_length(options) + coeff = STONE_BACK_LENGTH_TABLE.get(back_cm) + if coeff is None: + return [], [f"뒷길이 {back_cm}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 계수가 없습니다"] + if not any(options.get(key) is not None for key in ("back_len_cm", "stone_back_length_cm")): + notes.append( + f"⚠ 골막이 제원에 **뒷길이 칸이 없어** 기본 {back_cm}㎝ 로 섰습니다 " + "— 그 값이 두께식·고임돌·야면석·채움콘크리트 계수를 모두 가릅니다" + ) + back_m = back_cm / 100.0 + + given = _num(options.get("face_slope_ratio"), 0.0) + if given > 0: + slope, slope_note = given, f"사용자 지정 1:{given:g}" + else: + slope = const["default_slope_ratio"] + slope_note = f"1:{slope:g} — 정본 「반수면비탈」 붙박이(안 정함)" + + # ① 정면적(사다리꼴) − ② 방수로 파형강관 단면 + trapezoid = _floor2((top_m + bottom_m) / 2 * height_m) + pipe = const["spillway_pipe_r_m"] ** 2 * const["spillway_pipe_pi"] + front_area = trapezoid - pipe + notes.append( + f"방수로 파형강관 {pipe:.4f}㎡ 를 정면적에서 뺐습니다 — 정본 붙박이(Ø0.8) " + "⚠ 「방수로 없음」을 고를 칸이 등록부에 없어 늘 뺍니다" + ) + + masonry = _floor2(front_area * _round2(math.hypot(slope, 1.0))) + top_t = _floor2(back_m + const["thickness_top_per_m"] * height_m) + thickness = _floor2( + ( + (back_m + const["thickness_top_per_m"] * height_m) + + (back_m + const["thickness_bottom_per_m"] * height_m) + ) + / 2 + ) + volume = _floor2(front_area * thickness) + facing = top_m * (top_t - back_m) + base_area = masonry + facing + + rows: list[tuple[str, str, float, str]] = [ + ("돌쌓기", "㎡", masonry, f"정면적 {front_area:.4f}㎡ × √(1+n²) · 기울기 {slope_note}"), + ("돌붙임", "㎡", facing, f"상장 {top_m:g}m × (상부두께 {top_t:g} − 뒷길이 {back_m:g})"), + ( + "입적", + "㎥", + volume, + f"정면적 × 평균두께 {thickness:g}m " + f"[{{(ℓ3+0.1H)+(ℓ3+0.4H)}}÷2 · ℓ3={back_m:g} · H={height_m:g}]", + ), + ] + + stone_ton = coeff["stone_ton_per_m2"] + if stone_ton is None: + notes.append(f"뒷길이 {back_cm}㎝ 에 야면석 중량 칸이 비어 있어 돌을 못 세웠습니다") + else: + rows.append( + ( + "야면석", + "ton", + _ceil2(base_area * stone_ton), + f"(돌쌓기+돌붙임) {base_area:.4f}㎡ × {stone_ton} ton/㎡ (뒷길이 {back_cm}㎝)", + ) + ) + wedge_per = coeff["wedge_stone_m3_per_m2"] + wedge = _round2(base_area * _num(wedge_per)) if wedge_per is not None else 0.0 + if wedge_per is None: + notes.append(f"뒷길이 {back_cm}㎝ 에 고임돌 칸이 비어 있습니다 — 막자갈에서 안 뺐습니다") + else: + rows.append(("고임돌", "㎥", wedge, f"(돌쌓기+돌붙임) × {wedge_per} ㎥/㎡")) + + rows.append( + ( + "막자갈", + "㎥", + _floor2(volume - (base_area * back_m * const["rubble_body_ratio"] + wedge)), + "입적 − ((돌쌓기+돌붙임) × 뒷길이 × 2/3 + 고임돌)", + ) + ) + rows.append( + ( + "물구멍관", + "m", + _round2( + masonry / STONE_MASONRY["weep_hole_area_m2"] * STONE_MASONRY["weep_hole_length_m"] + ), + f"돌쌓기 ÷ {STONE_MASONRY['weep_hole_area_m2']:g}㎡/개소 × " + f"{STONE_MASONRY['weep_hole_length_m']:g}m/개소", + ) + ) + + # 바닥파기 — 비탈 사면장 두 쪽 + 하장 한 쪽, 폭은 평균두께 + 0.3. + slant = _floor2(math.hypot((top_m - bottom_m) / 2, height_m)) + width = thickness + const["trench_extra_m"] + depth = const["trench_depth_m"] + trench = _floor2(slant * depth * width * 2 + bottom_m * depth * width) + spoil = _round2(trench - (slant * depth * width + bottom_m * depth * width)) + rows.extend( + [ + ( + "터파기", + "㎥", + trench, + f"(사면장 {slant:g} × {depth:g} × {width:g}) × 2 + (하장 {bottom_m:g} × " + f"{depth:g} × {width:g}) · 정본 이름 「바닥파기」", + ), + ("잔토처리", "㎥", spoil, f"바닥파기 − (사면장 + 하장) × {depth:g} × {width:g}"), + ("되메우기", "㎥", trench - spoil, "바닥파기 − 잔토"), + ] + ) + + components = [ + Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis) + for name, unit, amount, basis in rows + ] + mpa, mpa_basis = fill_concrete_mpa(options) + fill_per = coeff["fill_concrete_m3_per_m2"] + components.append( + Component( + "채움콘크리트", + "㎥", + _round2(base_area * _num(fill_per)), + DESTINATION["채움콘크리트"], + f"(돌쌓기+돌붙임) × {fill_per} ㎥/㎡ · {mpa_basis}", + spec=f"{mpa}", + ) + ) + if not options.get("stone_kind"): + notes.append( + "돌 종류 칸이 골막이 등록부에 없어 **야면석**으로 섰습니다 — 정본 탭이 그 돌입니다" + ) + return components, notes #: 개거(겉도랑) 원단위 — 소광리 정본 「개거(150-200) (2)」·「L형수로-(201)」, **둘 다 m당**. @@ -208,10 +408,12 @@ OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = { #: 떼흙막이 원단위 — 소광리 정본 「떼흙막이」, **개소당**. 치수는 전부 **평균 붙박이**다 #: (상단 1.5 · 하단 1.1 · 높이 0.5 · 두께 0.2 · 기슭 0.54 — 원문에 「(평균)」이라 적혀 있다). -#: ⚠⚠ **아직 못 쓴다** — 우리 `soil_guard` 형식이 「콘크리트 · 돌(찰) · 돌(메) · 블록쌓기 · -#: 돌망태」 다섯이고 **「떼」가 없다.** 형식 칸이 생기면 이 표를 그대로 붙이면 된다. -#: ⚠ 나머지 다섯 형식은 **정본에 탭이 없다.** 돌(찰)/(메)에 기슭막이 식을 빌려 쓰지 않는다 — +#: ✅ **형식 칸에 「떼」가 들어와 이 표가 선다**(2026-09-09 랩탑 메인 `513aff3e`). +#: ⚠ 나머지 일곱 형식은 **정본에 탭이 없다.** 돌(찰)/(메)에 기슭막이 식을 빌려 쓰지 않는다 — #: 흙막이는 교본상 앞면 1:0.3·뒷면 수직·천단 30㎝ 라 축이 다르고, 무엇보다 **근거가 없다.** +#: ⚠⚠ **사용자 확정 4차**(2026-09-09) — 「흙막이는 횡단도에서 표현방식·옵션이 기슭막이와 +#: 동일, 형상도 동일. 다만 **데이터는 분리하여 계산**」. ⇒ **그림·옵션은 한 벌로 쓰되 +#: 수량은 여기서 제 것으로 낸다.** 그림이 같다고 기슭막이 원단위를 끌어오면 그 확정을 어긴다. SOIL_GUARD_SOD: dict[str, Any] = { "unit_label": "개소당", "rows": ( @@ -222,21 +424,38 @@ SOIL_GUARD_SOD: dict[str, Any] = { "머리떼 1.5×0.2 + 바닥떼 1.1×0.2 + 정면떼 (1.5+1.1)÷2×0.5 " "+ 양기슭바닥떼 0.54×0.2×2 · 규격 20×20㎝", ), - ("바닥파기", "㎥", 0.17, "(0.54×0.4×0.2×2) + (1.1×0.4×0.2)"), + # ⚠ 이름을 **터파기**로 낸다 — 정본 이름은 「바닥파기」이나 그대로 두면 토공 축에 + # 안 실린다(`DESTINATION` 이 이름으로 가른다). 골막이도 같은 자리를 그렇게 냈다. + ("터파기", "㎥", 0.17, "(0.54×0.4×0.2×2) + (1.1×0.4×0.2) · 정본 이름 「바닥파기」"), ), } def soil_guard(options: dict[str, Any]) -> tuple[list[Component], list[str]]: - """흙막이 — **아직 못 세운다.** 어느 형식이 왜 안 되는지만 낸다.""" + """흙막이 전개 — **「떼」만 선다**(정본 「떼흙막이」, 개소당). 나머지는 사유만. + + ⚠ 치수가 **전부 평균 붙박이**다 — 정본 표에 「(평균)」이라 적혀 있고 개소별 치수조서가 + 따로 없다. 그래서 높이·길이를 받지 않는다. 개소별로 갈리게 하려면 치수 칸이 먼저다. + """ + from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION + form = str(options.get("form") or "").strip() if not form: return [], ["흙막이 형식이 정해지지 않았습니다 — 형식을 골라야 물량이 섭니다"] - return [], [ - f"흙막이 형태 「{form}」 — 정본(소광리 53탭)에 그 형식의 산출식이 없습니다. " - "있는 것은 **떼흙막이** 하나인데 우리 형식 칸에 「떼」가 없습니다. " - "⚠ 돌(찰)·돌(메)에 기슭막이 식을 빌려 쓰지 않습니다 — 흙막이는 앞면 1:0.3·뒷면 수직·" - "천단 30㎝(교본)라 축이 다르고 근거가 없습니다" + if form != "떼": + return [], [ + f"흙막이 형식 「{form}」 — 정본(소광리 53탭)에 그 형식의 산출식이 없습니다" + "(있는 것은 **떼흙막이** 하나). ⚠ 돌(찰)·돌(메)에 기슭막이 식을 빌려 쓰지 않습니다 " + "— 흙막이는 앞면 1:0.3·뒷면 수직·천단 30㎝(교본)라 축이 다르고 근거가 없습니다" + ] + + components = [ + Component(name, unit, amount, DESTINATION.get(name, "quantity"), basis) + for name, unit, amount, basis in SOIL_GUARD_SOD["rows"] + ] + return components, [ + f"떼흙막이는 **{SOIL_GUARD_SOD['unit_label']}**이고 치수가 정본 평균 붙박이입니다" + "(상단 1.5 · 하단 1.1 · 높이 0.5 · 두께 0.2 · 기슭 0.54m) — 개소 제원을 안 봅니다" ] From 418c7c57b7a7ce88f23bfdc437d448edf890a39d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 12:00:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(B08):=20=EC=97=91=EC=85=80=20=EC=9E=90?= =?UTF-8?q?=EB=A6=84=C2=B7=EB=B0=98=EC=98=AC=EB=A6=BC=EC=9D=84=20=EA=B1=B7?= =?UTF-8?q?=EC=96=B4=20=EB=82=B4=EC=A7=80=20=EB=A7=90=EB=9D=BC=EB=8A=94=20?= =?UTF-8?q?=EA=B7=BC=EA=B1=B0=EB=A5=BC=20=EC=A3=BC=EC=84=9D=EC=97=90=20?= =?UTF-8?q?=EB=82=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 정본이 줄마다 INT(x*100)/100 으로 자르고 다음 줄이 그 잘린 값을 받으므로 표시용이 아니라 계산의 일부임. 안 자르면 막자갈 10.55 · 잔토 2.75 로 정본과 갈림. round(4.905) 가 짝수 반올림으로 4.90 이 되는 자리(물구멍관)도 함께 적음. Co-Authored-By: Claude Opus 5 (1M context) --- ...8_Quantity_Engine_UnitQuantity_Revetment.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index b4e4cd14..8a0fdf5c 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -171,18 +171,32 @@ EROSION_CHECK_WITHHELD: dict[str, str] = { } +# ── 엑셀 자름·반올림 ──────────────────────────────────────────────────────── +# ⚠⚠ **걷어 내지 말 것.** 「자릿수 맞추기」로 보여 지우고 싶어지는 자리인데, 정본이 +# **줄마다 자르고 다음 줄이 그 잘린 값을 받는다** — 표시용이 아니라 **계산의 일부**다. +# 입적 = INT(정면적 × 평균두께) ← 평균두께가 이미 잘린 값(1.09) +# 막자갈 = INT(입적 − (…)) ← 입적이 이미 잘린 값(20.56) +# 잔토 = ROUND(바닥파기 − (…)) ← 바닥파기가 이미 잘린 값(8.61) +# 안 자르면 막자갈이 10.55, 잔토가 2.75 로 나와 **정본과 갈린다.** +# 「정본과 같은 값」의 뜻을 **소수점까지**로 잡은 것이고, 거울 시험이 그것을 잡는다. + + def _floor2(value: float) -> float: """엑셀 `INT(x*100)/100` · `ROUNDDOWN(x,2)`.""" return math.floor(value * 100) / 100 def _round2(value: float) -> float: - """엑셀 `ROUND(x,2)` — 파이썬 기본(짝수 반올림)과 갈리는 자리라 규칙을 맞춘다.""" + """엑셀 `ROUND(x,2)`. + + ⚠ 파이썬 `round` 는 **짝수 반올림**이라 4.905 를 4.90 으로 내린다(엑셀은 4.91). + 물구멍관이 정확히 그 값이라 바꾸면 정본과 갈린다. + """ return float(Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) def _ceil2(value: float) -> float: - """엑셀 `ROUNDUP(x,2)`.""" + """엑셀 `ROUNDUP(x,2)` — 야면석만 이쪽이다(정본이 그 줄만 올림).""" return math.ceil(value * 100) / 100