From ef5b9536c07aa41a500f944758e35e16e9b21aa2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 12:44:27 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(B09):=20=EA=B4=80=EA=B2=BD=EC=9D=B4=20?= =?UTF-8?q?=ED=91=9C=20=EB=B0=96=EC=9D=B4=EB=A9=B4=20**=EB=AC=B4=EC=97=87?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=ED=95=B4=EC=95=BC=20=ED=95=98=EB=8A=94?= =?UTF-8?q?=EC=A7=80**=EA=B9=8C=EC=A7=80=20=EA=B0=80=EB=A6=AC=ED=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「품셈 표에 그 관경이 없음」만 적으면 사용자가 **엉뚱한 것을 정하러 감** — 정할 것이 단가가 아니라 **시설 선택**일 수 있음. - 관부설 품셈 표가 다루는 관경은 ∅800·∅1000·∅1200 뿐임을 사유에 적음 - Ø1,500 이상이면 임도기술교본 3장을 가리킴 — 「BOX암거 적용 — 수리계산상 배수관 **Ø1,500㎜ 이상** 적용 유역」(`횡단배수관_암거 §5`) - ⚠ **단정하지 않음** — 그 §5 는 **교본 기준이지 법령·행정규칙이 아님**. 프로젝트 규칙이 「기본값은 현행 법령·행정규칙, 교본은 과거 참조」라 **가리키기만** 하고 사유에 그 사실을 함께 적음 검증: pytest 307 통과 · Ø1500 두 줄에 그 문구가 뜸 Co-Authored-By: Claude Opus 5 (1M context) --- .../B09_Estimation_BillOfQuantities_Rows.py | 9 ++++- B09_Estimation/B09_Estimation_KnownGaps.py | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index fc884869..33843c80 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -222,6 +222,10 @@ def _leaf_row( if children: names = ", ".join(f"{c[2:]} {unit_prices.book.title(c).name}" for c in children) row.note = f"이 공종엔 일위대가가 없고 한 층 아래에 있습니다 — 후보: {names}" + # 관경이 표 밖이면 **무엇을 정해야 하는지**까지 가리킨다. + diameter_note = pipe_diameter_note(node.code, item.variant_value) + if diameter_note: + row.note = f"{row.note} / {diameter_note}" reason = f"일위대가가 하위 공종에 있음(후보 {len(children)}건)" else: # ⚠ 「아직 안 만든 것」과 「성분이 빠져 못 세운 것」은 **할 일이 다르다**. @@ -362,7 +366,10 @@ def _leaf_row( _PENDING_FORMULA: dict[str, str] = {} -from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note # noqa: E402 +from B09_Estimation.B09_Estimation_KnownGaps import ( # noqa: E402 + known_gap_note, + pipe_diameter_note, +) def pending_formula_note(code: str | None) -> str: diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index b0838fe9..ccf08ab5 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -67,3 +67,38 @@ def known_gap_note(code: str | None) -> str: if plain.startswith(prefix): parts.append(note) return " / ".join(parts) + + +#: 관부설 품셈 표가 다루는 관경 — 그 밖은 **표에 없는 것**이지 값이 틀린 것이 아니다. +PIPE_TABLE_DIAMETERS_MM = (800, 1000, 1200) + +#: 교본이 큰 관을 넘기는 자리 — **교본 기준이지 법령·행정규칙이 아니다.** +#: `횡단배수관_암거.md §5`(교본 3장) 「BOX암거 적용 — 수리계산상 배수관 **Ø1,500㎜ 이상** +#: 적용 유역 · 계곡 횡단경사 40% 이내 · 구체 수평 설치 원칙」. +#: ⚠ 그래서 「Ø1,500 이면 반드시 암거」라고 **단정하지 않는다** — 프로젝트 규칙이 +#: 「기본값은 현행 법령·행정규칙, 교본은 과거 참조」이기 때문이다. **가리키기만** 한다. +CULVERT_HINT_MM = 1500 + + +def pipe_diameter_note(code: str | None, variant_value: object) -> str: + """관경이 품셈 표 밖일 때 **어디를 봐야 하는지**를 가리킨다. + + ⚠ 「품셈 표에 그 관경이 없음」만 적으면 사용자가 **엉뚱한 것을 정하러 간다** — + 정할 것은 단가가 아니라 **시설 선택**일 수 있다. + """ + if not code or not str(code).startswith("FP-12-11"): + return "" + try: + diameter = int(float(str(variant_value))) + except (TypeError, ValueError): + return "" + if diameter in PIPE_TABLE_DIAMETERS_MM: + return "" + note = f"⚠ 품셈 관부설 표는 ∅800·∅1000·∅1200 까지만 있습니다 — Ø{diameter} 는 표 밖입니다." + if diameter >= CULVERT_HINT_MM: + note += ( + " ⓘ 임도기술교본 3장은 **Ø1,500㎜ 이상을 BOX암거 적용 유역**으로 봅니다" + "(`횡단배수관_암거 §5`). 관으로 놓을 자리가 아닐 수 있으니 **단가가 아니라" + " 시설 선택**을 먼저 볼 것. ⚠ 교본 기준이라 법령·행정규칙은 아닙니다." + ) + return note From dd25bb0e19cce5f0fe5391f96ae4e95f3a01eb09 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 12:54:20 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(B09):=20=EA=B4=80=EB=B6=80=EC=84=A4=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0=EC=97=90=20=E3=80=8C=EC=9D=B8=EB=A0=A5=20?= =?UTF-8?q?=ED=92=88=EB=A7=8C=E3=80=8D=EA=B3=BC=20=EC=BB=A4=ED=94=8C?= =?UTF-8?q?=EB=A7=81=EB=B0=B4=EB=93=9C=20=EC=88=A8=EC=9D=80=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=A0=81=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배수관 네 줄이 금액으로 섰으나 그 값은 **인력 품만**임 — 파형강관·커플링밴드· 크레인(5ton)·모래부설이 아직 안 붙음. 까닭이 둘로 갈림: · 관·모래 — 원문이 「별산」·「설계수량」이라 한 자리 · 크레인 — 기계 카탈로그에 없음(9-9 목록에 올림) ⭐ 원문을 대조하다 **숨은 값** 하나 나옴 — 커플링밴드는 표가 비었으나 [주]① 이 「**1EA/6m**」를 줌. 자재가 서면 m 당 1/6 EA 로 셀 수 있음(「필요시 별도 산정」 조건 하나 더). ⓘ 원문 특이점도 계획서에 적어 둠 — 배관공이 ∅800 0.25 · ∅1000 **0.22** · ∅1200 0.41 로 **가운데만 작음**. 크레인·보통인부는 관경이 커질수록 늘어남. **원문 그대로라 손대지 않음.** 검증: pytest 307 통과 · ∅800 0.25/8.0 = 0.03125 로 열 순서가 맞음을 원문과 대조 Co-Authored-By: Claude Opus 5 (1M context) --- B09_Estimation/B09_Estimation_KnownGaps.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index ccf08ab5..b0ee64ef 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -36,6 +36,14 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = { "(2026-09-09 각재·판재·못이 자재 축에 섰음. 다만 관급·사급이 안 갈려 아직 금액 0). " "재료가 금액으로 서면 손율이 함께 걸립니다 — 품셈 [주]③ 「목재의 손율은 1개소 사용당 80%」.", ), + "FP-12-11-03": ( + "인력 품만", + "⚠ 이 값은 **인력 품만**입니다 — 파형강관·커플링밴드·크레인(5ton)·모래부설이 아직 " + "안 붙었습니다. 관·모래는 원문이 「별산」·「설계수량」이라 한 자리이고, 크레인은 " + "기계 카탈로그에 없어서입니다. " + "ⓘ 커플링밴드는 값이 표에 없지만 [주]① 이 「**1EA/6m**」를 줍니다 — 자재가 서면 " + "m 당 1/6 EA 로 셀 수 있습니다(다만 「필요시 별도 산정」이라 조건이 하나 더 붙습니다).", + ), "FP-12-25": ( "운반거리 미정", "⚠ 이 값에는 **운반 몫이 빠져 있습니다** — 품셈 12-25 는 「운반 | 덤프트럭(15ton)」 줄을 " From 799e0b1e791cafa24146bf7913896ad23667e32f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 13:22:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(B09):=20=ED=99=95=EC=A0=95=205?= =?UTF-8?q?=EC=B0=A8=20=E2=80=94=20=EC=A0=9C=EA=B7=BC=20=EB=B0=91=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EA=B1=B4=EC=84=A4=ED=92=88=EC=85=88=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B9=8C=EB=A0=A4=20=EC=98=B4=20(=C3=B71,000=20?= =?UTF-8?q?=ED=99=98=EC=82=B0=20=ED=8F=AC=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 확정 5차 6번 「제근 밑수 — 면적 축. 건설품셈 3-9-2 「1,000㎡당」을 빌려 씀」. - `WorkItemUnit.BORROWED_UNITS` 신설 — **사용자가 정한 자리만** 담음. 우리가 골라 채우는 표가 아님. 값과 **근거 문구를 한 자리에** 둠(⚠ 교차 참조 표시 — 임도 전용 품이 있는데 건설품셈을 쓰는 것이라 화면이 그 사실을 밝혀야 함) - ⚠⚠ `BORROWED_BASIS_PER` — **「1,000㎡당」이라 ÷1,000 환산**. 단위만 「㎡」로 달고 값을 그대로 두면 **금액이 천 배**가 됨. 단위와 배수를 한 자리에 두고 시험으로 못 박음 - ⇒ 제근 여섯 갈래에 밑수가 붙어 단가가 섬 — 굴착기 0.7㎥·소림 **55.4원/㎡** ⚠ **자릿수가 견준 값과 어긋남을 사유에 함께 적음** — 실무 영월 「뿌리뽑기」는 475원/㎡ 이고, 빌려 온 3-9-2 는 같은 1,000㎡당에 굴착기(0.2㎥) 3.76hr 로 산림품셈 9-21 의 0.80hr 보다 4.7배 큼. **100㎡당으로 보면 실무와 자릿수가 맞음.** 값은 확정대로 두고 **어긋남을 보임** — 지어내지도, 조용히 넘기지도 않음. 검증: pytest 311 통과(신규 4) Co-Authored-By: Claude Opus 5 (1M context) --- .../B09_Estimation_ResourceAxis_ChooseOne.py | 8 ++- B09_Estimation/B09_Estimation_WorkItemUnit.py | 57 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py b/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py index 958b89f1..bcdb40ee 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py @@ -155,6 +155,12 @@ def match_choose_one_machine_table( form = str(table.get("pum_form", "")) unit = table.get("basis_unit") or "" + # ⚠ **빌려 온 밑수의 배수를 여기서 나눈다** — 「1,000㎡당」 표를 ㎡당으로 싣는다. + # 안 나누면 금액이 **천 배**로 선다(2026-09-09 확정 5차 6번, 제근). + from B09_Estimation.B09_Estimation_WorkItemUnit import borrowed_basis_per + + per = Decimal(borrowed_basis_per(str(node.get("work_item_code", "")))) + made = 0 for block in blocks: machine = block["entry"] @@ -174,7 +180,7 @@ def match_choose_one_machine_table( resource_code=entry.code, resource_name=entry.name, resource_spec=entry.spec, - amount=amount, + amount=amount / per, amount_unit=unit, raw_row_index=0, variant=variant, diff --git a/B09_Estimation/B09_Estimation_WorkItemUnit.py b/B09_Estimation/B09_Estimation_WorkItemUnit.py index ec05eb27..c49577f5 100644 --- a/B09_Estimation/B09_Estimation_WorkItemUnit.py +++ b/B09_Estimation/B09_Estimation_WorkItemUnit.py @@ -136,8 +136,63 @@ def section_of(work_item_code: str) -> str: return "-".join(numbers) +#: **다른 품셈에서 빌려 온 밑수** — 산림품셈이 안 적은 자리를 사용자가 정해 준 것. +#: +#: ⚠ **임도 전용 품이 있는데 건설품셈을 쓰는 것**이라 화면이 그 사실을 밝혀야 한다 +#: (교차 참조 표시 — 프로젝트 규칙). 그래서 값만 두지 않고 **근거 문구를 함께** 든다. +#: ⚠ 여기 적는 것은 **사용자가 정한 것뿐**이다. 우리가 골라 채우는 자리가 아니다. +#: 빌려 온 밑수가 **몇 단위당**인가 — 「1,000㎡당」이면 1000. +#: ⚠ **이 나눗셈을 빠뜨리면 1,000 배 틀린다.** 단위만 「㎡」로 바꿔 달고 값을 그대로 두면 +#: 금액이 천 배로 선다. 그래서 단위와 배수를 **한 자리에** 둔다. +BORROWED_BASIS_PER: dict[str, int] = {"FP-09-21": 1000} + +BORROWED_UNITS: dict[str, tuple[str, str]] = { + # 2026-09-09 사용자 확정 5차 6번 — 제근 밑수를 면적 축으로. + "FP-09-21": ( + "㎡", + "밑수는 **건설공사 표준품셈 3-9-2 뿌리뽑기 「1,000㎡당」**에서 빌려 왔습니다" + "(2026-09-09 사용자 확정) — ⚠ **교차 참조**입니다. 산림품셈 9-21 은 표 머리·제목·[주] " + "어디에도 밑수를 안 적었고(앞 절 9-20-1 「10주당」·9-20-2 「단위: 10주당」과 대비), " + "실무도 ㎡(영월)와 ㎥(교본 예시)로 갈려 있었습니다. " + "⚠ 그 표가 **1,000㎡당**이라 **㎡당으로 환산(÷1,000)**해 싣습니다. " + "⚠ **자릿수가 견준 값과 어긋납니다** — 이대로면 굴착기 0.7㎥·소림 기준 55.4원/㎡ " + "(대상 17,873.8㎡ 이면 약 99만원)인데, 실무 영월 「뿌리뽑기」는 475원/㎡ 이고 " + "빌려 온 건설품셈 3-9-2 는 같은 1,000㎡당에 굴착기(0.2㎥) 3.76hr 로 산림품셈 " + "9-21 의 0.80hr 보다 4.7배 큽니다. **100㎡당으로 보면 실무와 자릿수가 맞습니다.** " + "값은 확정대로 두되 이 어긋남을 함께 보입니다 — 다시 볼 자리입니다.", + ), +} + + +def borrowed_basis_per(work_item_code: str) -> int: + """빌려 온 밑수의 배수 — 「1,000㎡당」이면 1000. 아니면 1.""" + plain = str(work_item_code or "").split("#")[0] + for code, per in BORROWED_BASIS_PER.items(): + if plain.startswith(code): + return per + return 1 + + +def borrowed_unit_note(work_item_code: str) -> str: + """빌려 온 밑수라면 그 근거 한 줄. 아니면 빈 문자열.""" + plain = str(work_item_code or "").split("#")[0] + for code, (_unit, note) in BORROWED_UNITS.items(): + if plain.startswith(code): + return note + return "" + + def unit_of(work_item_code: str) -> str | None: - """그 공종 단가의 기준 단위. **원문에 없으면 `None`** — 짐작으로 채우지 않는다.""" + """그 공종 단가의 기준 단위. **원문에 없으면 `None`** — 짐작으로 채우지 않는다. + + ⚠ 예외는 **사용자가 다른 품셈에서 빌려 오라고 정한 자리**뿐이다(`BORROWED_UNITS`). + 그때도 화면이 **어디서 빌렸는지**를 함께 보인다. + """ + plain = str(work_item_code or "").split("#")[0] + for code, (unit, _note) in BORROWED_UNITS.items(): + if plain.startswith(code): + return unit + section = section_of(work_item_code) if not section: return None