From ac66a25cc7d2d624930ed4dff8ec81ec87e48ca8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 00:23:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(b09):=20=E3=89=B0=20=ED=91=9C=ED=86=A0?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=8B=B5=EC=99=B8=EA=B5=AC=EA=B0=84=209-1?= =?UTF-8?q?5-2=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=E2=80=94=20?= =?UTF-8?q?=EA=B3=84=EC=88=98=ED=91=9C=EB=A5=BC=20=EB=B6=88=EB=8F=84?= =?UTF-8?q?=EC=A0=80=20=EC=8B=9D=20+=20T=20=EB=A1=9C=20=C2=B7=20=EC=98=81?= =?UTF-8?q?=EC=9B=94=20=ED=98=B8=ED=91=9C=20=EC=96=B4=EA=B8=8B=EB=82=A8(E?= =?UTF-8?q?=20=EC=9E=90=EB=A6=AC=EC=97=90=20e)=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit · 계수표(T·L·E·q0·e·f·V1·V2·t) 칸을 기호로 읽어 불도저 식(8-2-1 · dozer_hourly_output) 그대로 Q1 47.92 ㎥/hr · Q = Q1 ÷ T 0.2 = 239.60 ㎡/hr · 기종은 q0 3.2 · V1 40 · V2 46(1단)로 8-2-1 표에서 되짚음 → 무한궤도 19ton([주]① 과 맞음) · D 줄 1/Q hr/㎡ · 원문 대 실무 어긋남 기록 — 영월 호표 Q=576.95 는 E 자리에 e(0.96)를 넣은 값으로 정확히 역산됨 · 다른 실무엔 이 호표 없음 → 원문이 또렷해 원문 E=0.4 로 셈(공종 사유 한 줄) 새 제목 B·D 618원/㎡ · 바뀐 제목 0 · 프로젝트 내역 표토제거 9,954.32㎡ 가 금액으로 섬(+6,151,768) · 잴 시험 둘 빨강→초록 · 끄기 셋 빨강 · 전체 시험 1703 + 364 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B09_Estimation/B09_Estimation_KnownGaps.py | 7 ++ .../B09_Estimation_TopsoilRemoval.py | 112 ++++++++++++++++++ B09_Estimation/B09_Estimation_UnitPrice.py | 4 + resources/tester/test_b09_topsoil_removal.py | 44 +++++++ 4 files changed, 167 insertions(+) create mode 100644 B09_Estimation/B09_Estimation_TopsoilRemoval.py create mode 100644 resources/tester/test_b09_topsoil_removal.py diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index ebc77595..af1cca58 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -57,6 +57,13 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = { # 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만. # 봉상후렉시블 셋의 표 나머지 줄은 판정표 자동 목록(`_unread_rows`)이 맡음 — 줄이 아닌 [주] 만 여기. "FP-12-12": ("원문 [주]", "ⓘ [주] 「성토부 날개벽 설치시 인건비 30% 할증」 은 안 걺(선택)."), + # 원문 대 실무 어긋남 기록(2026-09-15 브레인 규칙 — 원문이 또렷하면 원문 · 어긋남은 늘 기록). + "FP-09-15-02": ( + "실무 어긋남", + "ⓘ 실무 영월 「표토제거 답외구간」 호표 Q=576.95 는 E 자리에 e(0.96)를 넣은 값으로 역산됨" + "(60×3.07×0.77×0.96÷(1.18×0.2)) — 원문 9-15-2 가 E=0.4 로 또렷해 원문으로 셈 · 다른 실무(봉화·대흥·" + "소광·거창)엔 이 호표 없음.", + ), "FP-12-34-01": ( "원문 머리", "ⓘ 원문 머리 「(단위: 개소당)」 ↔ 인력 콘크리트공 0.17·보통인부 0.29 가 12-16 맨홀 ㎥당(0.17인/㎥" diff --git a/B09_Estimation/B09_Estimation_TopsoilRemoval.py b/B09_Estimation/B09_Estimation_TopsoilRemoval.py new file mode 100644 index 00000000..d55b517a --- /dev/null +++ b/B09_Estimation/B09_Estimation_TopsoilRemoval.py @@ -0,0 +1,112 @@ +"""B09 원가계산 — **표토제거 답외구간** 9-15-2 (2026-09-14 브레인 ㉰ 첫째). + +B08 준비공 「표토제거」 줄이 부르는 코드인데 표가 계수표(T·L·E·q0·e·f·V1·V2·t)라 일위대가가 안 섰음. + + q = q0 × e · ㎝ = L/V1 + L/V2 + t · Q1 = 60 × q × f × E / ㎝ (㎥/hr) · Q = Q1 / T (㎡/hr) + [주]① 무한궤도 불도저(19ton) · ③ 건설품셈 8-2-1 불도저 참조 → 불도저 식(`dozer_hourly_output`) 그대로 + T 로 나눔 + 기종은 표의 q0·V1·V2(1단)로 8-2-1 표에서 되짚음(`resolve_dozer`) — [주]① 19ton 과 맞는지 시험이 봄 + +⚠ 실무 영월 호표 Q=576.95 는 E 자리에 e(0.96)를 넣은 값 — 원문이 또렷해 원문 E 로 셈(까닭은 `KnownGaps`). +""" + +from __future__ import annotations + +import re +from decimal import Decimal +from typing import Any + +CODE = "FP-09-15-02" +TABLE = "F0282" +_RE_SYMBOL = re.compile(r"^([A-Za-z]\d?)\s*\(") +_RE_GEAR = re.compile(r"(\d+)\s*단") + + +def _factors(node: dict[str, Any]): + """(불도저 계수, T) — 칸이 모자라거나 기종이 안 좁혀지면 까닭 글.""" + from B09_Estimation.B09_Estimation_MachineProductivity import parse_measure + from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import DozerFactors, resolve_dozer + + table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == TABLE), {}) + values: dict[str, Decimal] = {} + gear = 1 + for row in table.get("raw_row") or []: + found = _RE_SYMBOL.match(str(row[0]).strip()) if row else None + value = parse_measure(str(row[1])) if found and len(row) > 1 else None + if value is not None: + values[found.group(1)] = value + shift = _RE_GEAR.search(str(row[1])) + gear = int(shift.group(1)) if shift else gear + missing = [k for k in ("T", "L", "E", "q0", "e", "f", "V1", "V2") if k not in values] + if missing: + return f"9-15-2 표 칸 없음: {', '.join(missing)}" + machine = resolve_dozer(values["q0"], values["V1"], values["V2"], gear) + if machine is None: + return f"삽날 {values['q0']}㎥ · {values['V1']}/{values['V2']}m/분({gear}단) 으로 불도저가 안 좁혀짐" + factors = DozerFactors( + work_item_code=CODE, + blade_capacity_m3=values["q0"], + distance_factor=values["e"], + volume_factor=values["f"], + efficiency=values["E"], + haul_distance_m=values["L"], + forward_speed_m_min=values["V1"], + reverse_speed_m_min=values["V2"], + machine_code=machine[0], + machine_name=machine[1], + ) + return factors, values["T"] + + +def topsoil_output(node: dict[str, Any] | None = None) -> tuple[Decimal, Decimal]: + """(Q1 ㎥/hr, Q ㎡/hr) — 둘 다 소수 2자리로 확정한 뒤 씀(명세 7장).""" + from B09_Estimation.B09_Estimation_MachineProductivity import fix2 + from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import dozer_hourly_output + from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master + + if node is None: + node = next(n for n in load_work_item_master()["work_items"] if n["work_item_code"] == CODE) + found = _factors(node) + if isinstance(found, str): + raise ValueError(found) + factors, thickness = found + q1 = dozer_hourly_output(factors) + return q1, fix2(q1 / thickness) + + +def attach_topsoil_removal(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None: + """`B-FP-09-15-02` — 불도저 1/Q hr/㎡ 한 줄(D). 기계 층이 없거나 표가 달라지면 까닭만.""" + from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle + + node = nodes_by_code.get(CODE) + title_code = f"B-{CODE}" + if node is None or title_code in build.book.titles: + return + found = _factors(node) + if isinstance(found, str): + build.component_gaps[CODE] = found + return + factors, thickness = found + hourly = f"X-{factors.machine_code}" + if hourly not in build.book.titles: + build.component_gaps[CODE] = f"{factors.machine_name} 시간당 사용료가 안 섬" + return + q1, q = topsoil_output(node) + build.book.add_title( + PriceTitle( + code=title_code, + kind=PriceKind.UNIT_PRICE, + name=str(node.get("name") or CODE), + spec="표토제거", + unit="㎡", + ) + ) + build.book.add_output_detail( + title_code, + hourly, + Decimal(1) / q, + f"{factors.formula_text} → Q = Q1 {q1} ÷ T {thickness}m = {q} ㎡/hr" + " (산림품셈 9-15-2 [주]②③ · 건설 8-2-1)", + output=q, + ) + if CODE in build.skipped: + build.skipped.remove(CODE) diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 2c824a43..4b80d58a 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -1095,6 +1095,10 @@ def build_unit_prices( from B09_Estimation.B09_Estimation_Explosives import attach_explosives attach_explosives(build, nodes_by_code) + # 표토제거 답외구간(9-15-2) — 계수표를 불도저 식 + T 로(2026-09-14 ㉰). + from B09_Estimation.B09_Estimation_TopsoilRemoval import attach_topsoil_removal + + attach_topsoil_removal(build, nodes_by_code) # ⚠ **마지막에 한 번** — 조합 사용 공종의 본체 기계를 잡재료 16% 층으로 바꿔 단다 # (품셈 제8장 [주]⑤). 조립 도중에 바꾸면 어느 공종이 조합인지 아직 모른다. build.combined_swapped = _apply_combined_misc_rate( diff --git a/resources/tester/test_b09_topsoil_removal.py b/resources/tester/test_b09_topsoil_removal.py new file mode 100644 index 00000000..caab61cc --- /dev/null +++ b/resources/tester/test_b09_topsoil_removal.py @@ -0,0 +1,44 @@ +"""표토제거 답외구간 9-15-2 — 2026-09-14 브레인 ㉰ 첫째(B08 준비공 표토제거 줄이 부르는 코드). + +원문 L5288: T 0.2m · L 20m · E 0.4 · q0 3.2㎥ · e 0.96 · f 1/1.3 · V1 40m/분(1단) · V2 46m/분(1단) · t 0.25분 + [주]① 무한궤도 불도저(19ton) ② q = 3.2 × e · ㎝ = L/V1 + L/V2 + 0.25 · Q1 = 60 × q × f × E / ㎝ ㎥/시간 · Q = Q1/T ㎡/시간 + ③ 건설품셈 8-2-1 불도저 참조 → 불도저 식(`dozer_hourly_output`)을 그대로 쓰고 T 로 나눔 +⚠ 실무 영월 「표토제거 답외구간」 호표 Q=576.95 는 **E 자리에 e(0.96)를 넣은 값**으로 정확히 역산됨 + (60 × 3.07 × 0.77 × 0.96 ÷ (1.18 × 0.2) = 576.95) · 다른 실무(봉화·대흥·소광·거창)엔 이 호표 없음 → + 그 현장 실수 · 원문이 또렷하므로 원문 E=0.4 로 셈(2026-09-15 브레인 규칙 · 어긋남 기록) +""" + +from __future__ import annotations + +from decimal import Decimal + +from B09_Estimation.B09_Estimation_UnitPrice import cached_build + + +def test_표토제거_답외구간_일위대가가_불도저_19톤_1_Q_로_섬() -> None: + from B09_Estimation.B09_Estimation_TopsoilRemoval import topsoil_output + + q1, q = topsoil_output() + assert q1 == Decimal("47.92") and q == Decimal("239.60"), (q1, q) + book = cached_build().book + title = book.titles["B-FP-09-15-02"] + assert title.unit == "㎡" + rows = [ + r for d in book.details["B-FP-09-15-02"] for r in [d, *book.details.get(d.ref_code, [])] + ] + dozer = next(r for r in rows if r.ref_code == "X-0101-0019") + assert dozer.quantity == Decimal(1) / q and dozer.output == q, dozer + + +def test_식_줄에_T_로_나눈_것과_실무_어긋남이_적힘() -> None: + book = cached_build().book + notes = " ".join( + r.note + for d in book.details["B-FP-09-15-02"] + for r in [d, *book.details.get(d.ref_code, [])] + ) + assert "T" in notes and "9-15-2" in notes, notes + from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note + + note = known_gap_note("FP-09-15-02") + assert "576.95" in note and "0.96" in note and "E=0.4" in note, note