From 92fd9092128013a299ff09f81d56b5e77a533c5c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 12:16:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=83=88=20=EC=B9=B8=20=EB=8B=A4?= =?UTF-8?q?=EC=84=AF=EC=9D=84=20=EA=B3=A8=EB=A7=89=EC=9D=B4=C2=B7=EA=B0=9C?= =?UTF-8?q?=EA=B1=B0=C2=B7=EB=AC=BC=EA=B5=AC=EB=A9=8D=EC=97=90=20=EB=AC=BC?= =?UTF-8?q?=EB=A6=BC=20=E2=80=94=20=EB=B0=A9=EC=88=98=EB=A1=9C=EA=B0=80=20?= =?UTF-8?q?=EC=A0=95=EB=A9=B4=EC=A0=81=EC=9D=84=20=EA=B0=80=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 골막이 (erosion_check) - spillway 「있음/없음」 — 「없음」이면 파형강관 0.5024㎡ 를 안 뺌. 정면적이 밑수라 일곱 줄이 +2.1~3.1 % 움직임. 「있음」이 정본과 같은 값(정본은 방수로 치수 0 인 개소에서도 뺐음). 안 고르면 정본 쪽으로 서고 사유가 뜸. - stone_kind · back_len_cm 를 읽음. 「칸이 없다」던 사유가 「안 골랐다」로 바뀜. 돌 무게 헬퍼 한 벌 (stone_weight_per_m2) - 확정 5차 큰 것 7 규칙을 돌쌓기·골막이가 같이 씀. 계산식이 기본, 야면석만 관측표. 두 벌로 두면 같은 확정이 구조물마다 갈림. 물구멍 헬퍼 한 벌 (weep_hole_spec) - weep_hole_diameter_mm · weep_hole_area_m2 를 읽고 비우면 Ø50 · 2㎡당. 지름은 규격(spec)으로 실어 단가가 갈리게 함. - 정본이 개소당 관 길이를 평균두께(0.83m)로 잡는 것을 주석에 남김 — 우리 상수 0.5m 와 갈리는 자리이고 미결임. 개거 — ditch_spec 칸이 와서 두 규격을 고를 수 있음. 시험 — 등록부 정책 예외에 새 칸 넷(ditch_spec · weep_hole 둘 · fill_slope_ratio)과 사토장 둘을 넣음. 613 통과. Co-Authored-By: Claude Opus 5 (1M context) --- .../B08_Quantity_Engine_UnitQuantity.py | 98 +++++++++++++------ ..._Quantity_Engine_UnitQuantity_Revetment.py | 58 ++++++----- 2 files changed, 103 insertions(+), 53 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 87e08afa..b89f9d6e 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -99,6 +99,55 @@ STONE_WEIGHT_FORMULA_BASIS = ( "⭐ 확정 5차 — 뒷길이 × 0.77(채움률) × 2.65(비중). ⚠ 잠정 — 실무자 협의 후 명확히 할 값" ) +#: 물구멍 칸 — 2026-09-09 에 등록부에 생겼다(랩탑 메인 `0b0763ed`). **빈 칸이 기본**이다. +#: ⚠ 기본 Ø50 은 **국가기준**이고 2㎡당 1개소는 실무 관측값이다(법은 「2~3㎡당 1개소 이상」). +WEEP_HOLE_DIAMETER_KEY = "weep_hole_diameter_mm" +WEEP_HOLE_AREA_KEY = "weep_hole_area_m2" +WEEP_HOLE_DIAMETER_DEFAULT_MM = 50 + + +def weep_hole_spec(options: dict[str, Any]) -> tuple[float, int, str]: + """(개소당 벽면적 ㎡, 관 지름 ㎜, 근거). 안 정하면 기본으로 서고 그 사실이 근거에 남는다.""" + area = _num(options.get(WEEP_HOLE_AREA_KEY), 0.0) + diameter = _num(options.get(WEEP_HOLE_DIAMETER_KEY), 0.0) + area_given = area > 0 + dia_given = diameter > 0 + if not area_given: + area = STONE_MASONRY["weep_hole_area_m2"] + if not dia_given: + diameter = WEEP_HOLE_DIAMETER_DEFAULT_MM + basis = ( + f"{area:g}㎡당 1개소 · Ø{int(diameter)}" + + ("" if area_given else " · ⚠ 면적 안 정함 — 실무 관측 2㎡(법은 2~3㎡당 1개소 이상)") + + ("" if dia_given else " · ⚠ 지름 안 정함 — 국가기준 Ø50") + ) + return area, int(diameter), basis + + +def stone_weight_per_m2( + back_cm: int, kind: str, observed: float | None +) -> tuple[str, float | None, str, str]: + """(줄 이름, ton/㎡, 근거, source) — ⭐ 확정 5차 큰 것 7. + + **계산식이 기본**이고 야면석만 관측표다. 돌쌓기·골막이가 같은 규칙을 쓰도록 여기 한 벌만 둔다. + """ + if kind in STONE_WEIGHT_OBSERVED_KINDS: + basis = ( + f"× {observed} ton/㎡ (뒷길이 {back_cm}㎝) · {STONE_WEIGHT_BASIS}" + if observed is not None + else "" + ) + return kind, observed, basis, STONE_WEIGHT_SOURCE + ton = back_cm / 100.0 * STONE_PACKING_RATIO * STONE_DENSITY_TON_M3 + basis = ( + f"× {ton:.3f} ton/㎡ = 뒷길이 {back_cm / 100.0:g} × {STONE_PACKING_RATIO}" + f" × {STONE_DENSITY_TON_M3}" + + (f" · {kind}" if kind else " · ⚠ 돌 종류를 안 골랐음") + + f" · {STONE_WEIGHT_FORMULA_BASIS}" + ) + return kind or "돌", ton, basis, "" + + #: 뒷채움(막자갈) 폭 — ⭐ 사용자 확정 5차 작은 것 3 「막자갈 = (상 + 하) ÷ 2 × H」. #: ⚠⚠ **여기 상·하는 벽 두께가 아니라 뒷채움 폭이다.** 벽 두께(0.75/0.9)를 끌어다 쓰면 #: H=2.0 에서 실무 0.75 가 1.66(입적)으로 튄다 — 옛 식이 곧 그 자리였다. @@ -874,35 +923,18 @@ def stone_masonry( ) ) - # 돌 무게 — ⭐ 확정 5차 큰 것 7. **계산식이 기본**이고 야면석만 관측표다. + # 돌 무게 — ⭐ 확정 5차 큰 것 7. **계산식이 기본**이고 야면석만 관측표다(헬퍼 한 벌). # ⚠ 안 고른 경우도 **계산식**으로 선다 — 정본 여섯 탭이 종류를 안 적고 그 식을 쓰고, # 그래야 정본 H=2.0 의 「1.92 톤」과 맞는다(관측표 0.88 로 서면 1.84 로 4 % 낮다). - # 이름은 정본 줄 이름 그대로 **「돌」**이고, 종류를 고르면 그 이름으로 바뀐다. - if kind_label in STONE_WEIGHT_OBSERVED_KINDS: - # 야면석은 공극이 커 계산식이 안 맞는다 — 원문 관측표를 그대로 쓴다. - stone_name = kind_label - stone_ton = table["stone_ton_per_m2"] - weight_basis = ( - f"돌쌓기 × {stone_ton} ton/㎡ (뒷길이 {back_cm}㎝) · {STONE_WEIGHT_BASIS}" - if stone_ton is not None - else "" + stone_name, stone_ton, weight_tail, weight_source = stone_weight_per_m2( + back_cm, kind_label, table["stone_ton_per_m2"] + ) + weight_basis = f"돌쌓기 {weight_tail}" if weight_tail else "" + if not kind_label: + notes.append( + "돌 종류를 안 골라 **계산식**(뒷길이 × 0.77 × 2.65)으로 섰습니다 — " + "야면석이면 계산식이 안 맞아 관측표로 갈립니다" ) - weight_source = STONE_WEIGHT_SOURCE - else: - stone_name = kind_label or "돌" - stone_ton = back_m * STONE_PACKING_RATIO * STONE_DENSITY_TON_M3 - weight_basis = ( - f"돌쌓기 × {stone_ton:.3f} ton/㎡ = 뒷길이 {back_m:g} × {STONE_PACKING_RATIO}" - f" × {STONE_DENSITY_TON_M3}" - + (f" · {kind_label}" if kind_label else " · ⚠ 돌 종류를 안 골랐음") - + f" · {STONE_WEIGHT_FORMULA_BASIS}" - ) - weight_source = "" - if not kind_label: - notes.append( - "돌 종류를 안 골라 **계산식**(뒷길이 × 0.77 × 2.65)으로 섰습니다 — " - "야면석이면 계산식이 안 맞아 관측표로 갈립니다" - ) if stone_ton is None: # 원본 표가 비어 있는 칸이다 — 지어내지 않고 알린다(PLAN 8-8 ㉮). notes.append(f"뒷길이 {back_cm}㎝ 의 돌중량이 원본 표에 없어 {stone_name}을 내지 못함") @@ -973,16 +1005,20 @@ def stone_masonry( # 지금 쓰는 13-4 계열에는 제잡비 행 자체가 없어 겹치지 않는다(전수 확인). # ⚠ 관종·지름은 미확정 — 법은 「지름 3~6㎝ 파이프」, 실무 관측은 Ø50. 규격이 정해지면 # 이름에 붙인다(`물구멍 Ø50`). 지어내지 않고 규격 없는 이름으로 둔다. + # ⚠ 「미확정」만 적으면 사용자가 무엇을 정해야 하는지 모른다 — + # **지금 무슨 값으로 돌고 있는지**를 함께 적는다(원단위 미확보와 같은 방식). + # ⓘ 정본은 개소당 관 길이를 **평균두께**로 잡는다(H=2.0 에서 0.83m). 우리는 상수 0.5m 라 + # 그만큼 짧게 선다 — 관은 벽을 가로지르므로 정본 쪽이 이치에 맞는다(미결). + hole_area, hole_dia, hole_basis = weep_hole_spec(options) components.append( Component( "물구멍관", "m", - masonry_area / constants["weep_hole_area_m2"] * constants["weep_hole_length_m"], + masonry_area / hole_area * constants["weep_hole_length_m"], DESTINATION["물구멍관"], - # ⚠ 「미확정」만 적으면 사용자가 무엇을 정해야 하는지 모른다 — - # **지금 무슨 값으로 돌고 있는지**를 함께 적는다(원단위 미확보와 같은 방식). - "돌쌓기 ÷ 2㎡/개소 × 0.5 m/개소 · ⚠ 잠정: 관 Ø 미정(법 3~6㎝ / 실무 Ø50) ·" - " 간격 2.0㎡당 1개소(법 2~3㎡당 1개소 이상)", + f"돌쌓기 ÷ {hole_area:g}㎡/개소 × {constants['weep_hole_length_m']:g} m/개소" + f" · {hole_basis}", + spec=f"Ø{hole_dia}", ) ) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py index 8a0fdf5c..c8e16357 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revetment.py @@ -210,6 +210,8 @@ def erosion_check_dam( STONE_MASONRY, _back_length, fill_concrete_mpa, + stone_weight_per_m2, + weep_hole_spec, ) form = str(options.get("form") or "").strip() @@ -239,8 +241,8 @@ def erosion_check_dam( 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}㎝ 로 섰습니다 " - "— 그 값이 두께식·고임돌·야면석·채움콘크리트 계수를 모두 가릅니다" + f"⚠ 뒷길이를 안 골라 기본 {back_cm}㎝ 로 섰습니다 " + "— 그 값이 두께식·고임돌·돌 무게·채움콘크리트 계수를 모두 가릅니다" ) back_m = back_cm / 100.0 @@ -252,13 +254,21 @@ def erosion_check_dam( slope_note = f"1:{slope:g} — 정본 「반수면비탈」 붙박이(안 정함)" # ① 정면적(사다리꼴) − ② 방수로 파형강관 단면 + # ⚠ 「없음」이면 안 뺀다 — 정면적이 밑수라 **열한 줄이 통째로 움직인다.** + # ⓘ 정본은 방수로 치수가 0 인 개소에서도 뺐다. 그러므로 **정본과 같은 값은 「있음」**이고, + # 「없음」은 정본보다 크게 나온다. 안 고르면 정본 쪽(「있음」)으로 서고 그 사실을 알린다. trapezoid = _floor2((top_m + bottom_m) / 2 * height_m) + spillway = str(options.get("spillway") or "").strip() pipe = const["spillway_pipe_r_m"] ** 2 * const["spillway_pipe_pi"] - front_area = trapezoid - pipe - notes.append( - f"방수로 파형강관 {pipe:.4f}㎡ 를 정면적에서 뺐습니다 — 정본 붙박이(Ø0.8) " - "⚠ 「방수로 없음」을 고를 칸이 등록부에 없어 늘 뺍니다" - ) + if spillway == "없음": + front_area = trapezoid + notes.append("방수로 「없음」이라 파형강관 단면을 안 뺐습니다 — 정본보다 정면적이 큽니다") + else: + front_area = trapezoid - pipe + notes.append( + f"방수로 파형강관 {pipe:.4f}㎡ 를 정면적에서 뺐습니다 — 정본 붙박이(Ø0.8)" + + ("" if spillway else " · ⚠ 방수로를 안 골라 정본대로 뺐습니다") + ) masonry = _floor2(front_area * _round2(math.hypot(slope, 1.0))) top_t = _floor2(back_m + const["thickness_top_per_m"] * height_m) @@ -285,16 +295,20 @@ def erosion_check_dam( ), ] - stone_ton = coeff["stone_ton_per_m2"] + # 돌 무게 — ⭐ 확정 5차 큰 것 7. 돌쌓기와 **같은 헬퍼**를 쓴다(규칙이 하나여야 한다). + kind = str(options.get("stone_kind") or "").strip() + stone_name, stone_ton, weight_tail, _src = stone_weight_per_m2( + back_cm, kind, coeff["stone_ton_per_m2"] + ) if stone_ton is None: - notes.append(f"뒷길이 {back_cm}㎝ 에 야면석 중량 칸이 비어 있어 돌을 못 세웠습니다") + notes.append(f"뒷길이 {back_cm}㎝ 에 {stone_name} 중량 칸이 비어 있어 돌을 못 세웠습니다") else: rows.append( ( - "야면석", + stone_name, "ton", _ceil2(base_area * stone_ton), - f"(돌쌓기+돌붙임) {base_area:.4f}㎡ × {stone_ton} ton/㎡ (뒷길이 {back_cm}㎝)", + f"(돌쌓기+돌붙임) {base_area:.4f}㎡ {weight_tail}", ) ) wedge_per = coeff["wedge_stone_m3_per_m2"] @@ -312,15 +326,14 @@ def erosion_check_dam( "입적 − ((돌쌓기+돌붙임) × 뒷길이 × 2/3 + 고임돌)", ) ) + hole_area, hole_dia, hole_basis = weep_hole_spec(options) 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/개소", + _round2(masonry / hole_area * STONE_MASONRY["weep_hole_length_m"]), + f"돌쌓기 ÷ {hole_area:g}㎡/개소 × {STONE_MASONRY['weep_hole_length_m']:g}m/개소" + f" · {hole_basis} (Ø{hole_dia})", ) ) @@ -360,9 +373,10 @@ def erosion_check_dam( spec=f"{mpa}", ) ) - if not options.get("stone_kind"): + if not kind: notes.append( - "돌 종류 칸이 골막이 등록부에 없어 **야면석**으로 섰습니다 — 정본 탭이 그 돌입니다" + "돌 종류를 안 골라 **계산식**(뒷길이 × 0.77 × 2.65)으로 섰습니다 " + "— 정본 탭은 야면석이고, 야면석은 계산식이 안 맞아 관측표로 갈립니다" ) return components, notes @@ -389,8 +403,8 @@ def erosion_check_dam( #: 본체 표를 못 찾았으므로 **「원문에 없음」으로 두고 그 사실을 화면에 적는다** — #: 「콘크리트 개거인데 콘크리트가 0」이 우리 결함으로 읽히지 않게. #: -#: ⚠ **규격이 붙박이다** — 정본에 「150×200」 하나뿐이고 `open_ditch` 등록부에 **옵션이 하나도 -#: 없다**(규격 칸 없음). 칸이 생기면 이 표를 열면 된다. +#: ✅ **규격 칸이 생겼다**(2026-09-09 랩탑 메인 `0b0763ed`) — 두 규격을 고를 수 있고, +#: 비우면 「콘크리트 개거 150×200」으로 선다. OPEN_DITCH_FORMS: dict[str, dict[str, Any]] = { "콘크리트 개거 150×200": { "rows": ( @@ -500,8 +514,8 @@ def open_ditch(length_m: float, options: dict[str, Any]) -> tuple[list[Component for name, unit, per_m, basis in table["rows"] ] notes: list[str] = [] - if "ditch_spec" not in options: - notes.append(f"규격이 「{spec}」 붙박이입니다 — 개거 제원에 규격 칸이 없습니다") + if not options.get("ditch_spec"): + notes.append(f"규격을 안 골라 「{spec}」으로 섰습니다 — 제원에서 고를 수 있습니다") if spec == "콘크리트 개거 150×200": notes.append( "⚠ 이 표에는 **콘크리트 본체 줄이 없습니다** — 정본 원문 그대로입니다"