feat(B09): 확정 5차 — 제근 밑수를 건설품셈에서 빌려 옴 (÷1,000 환산 포함)

사용자 확정 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 13:22:10 +09:00
co-authored by Claude Opus 5
parent dd25bb0e19
commit 799e0b1e79
2 changed files with 63 additions and 2 deletions
@@ -155,6 +155,12 @@ def match_choose_one_machine_table(
form = str(table.get("pum_form", "")) form = str(table.get("pum_form", ""))
unit = table.get("basis_unit") or "" 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 made = 0
for block in blocks: for block in blocks:
machine = block["entry"] machine = block["entry"]
@@ -174,7 +180,7 @@ def match_choose_one_machine_table(
resource_code=entry.code, resource_code=entry.code,
resource_name=entry.name, resource_name=entry.name,
resource_spec=entry.spec, resource_spec=entry.spec,
amount=amount, amount=amount / per,
amount_unit=unit, amount_unit=unit,
raw_row_index=0, raw_row_index=0,
variant=variant, variant=variant,
+56 -1
View File
@@ -136,8 +136,63 @@ def section_of(work_item_code: str) -> str:
return "-".join(numbers) 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: 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) section = section_of(work_item_code)
if not section: if not section:
return None return None