refactor(B08,B09): 폴더째 old_code 로 옮기고 빈 화면 둘만 남김 (PLAN 7-3)

B08_Quantity 86 · B09_Estimation 111 파일을 old_code/ 로 옮김(지우지 않음).
화면은 메뉴·주소·단계 막대만 남은 빈 틀 둘 — main.py 라우터 13 개는 끊음.
B07 이 빌려 쓰던 비탈 길이·면적은 필요한 함수만 B07_DesignDetail_Engine_SlopeGeometry
로 옮겨 적음(면적 적분·노면 면적·측점 묶음은 안 옮김) · 시험 하나를 새로 둠.
B07 구조물도 조립(Cad_StandardSheet)은 2026-09-13 에 이미 도면 목록에서 빠져
부르는 곳이 없어 old_code 로 같이 보냄 — 구조물 그림은 되살리지 않음.
B06 구조물 몫 조회는 빈 값으로 두어 화면이 그대로 서게 함.
B08·B09 를 부르던 시험 25 개도 old_code/resources/tester 로 옮김.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
2026-09-22 12:27:45 +09:00
co-authored by Claude Opus 5
parent 185f800d23
commit 8472fc9f40
231 changed files with 1617 additions and 1216 deletions
@@ -0,0 +1,90 @@
"""B09 원가계산 — **풀베기 묘목찾기 = 조림목 본수 칸** (2026-09-15 브레인 차례 ② 판정 ㉠ · 지금 칸을 만듦).
산림 6-2-2 줄베기 · 6-2-3 모두베기 [주]④⑤ 「묘목찾기와 줄베기(모두베기) 품을 **합하여** 적용」 — 묘목찾기는
「낫 | 인/100본 | 0.07 | 보통인부」 라 조림목 본수(본/ha · [주]② 둘레베기 참고 · 설계 입력)가 있어야 ha당이 섬.
⇒ 본수가 들면 갈래마다 보통인부 0.07 × 본수 ÷ 100 · 비면 「일부만」 으로 막고 사유(반만 선 틀린 값보다 안 선 값).
⚠ 제안값 없음 — 부록 예시 2,700본/ha 는 같은 부록이 품도 표와 다르게 적은 자료(묘목찾기 0.10 ↔ 표 0.07).
"""
from __future__ import annotations
import re
from decimal import Decimal, InvalidOperation
from typing import Any
#: 공종 → 표 번호(`_JudgedTable` 도구 줄 표 · 인/100본 줄이 있는 둘).
SEEDLING_TABLES = {"FP-06-02-02": "F0155", "FP-06-02-03": "F0156"}
NAME = "묘목찾기"
MISSING = (
"조림목 본수(본/ha) 미입력 — 원문 [주]④⑤ 가 묘목찾기({amount}인/100본)와 합하여 적용하라 함"
" · 「산출 조건」 에서 넣으면 섬"
)
def parse_seedlings_per_ha(text: str | None) -> Decimal | None:
"""조림목 본수 칸 — 빈 칸은 `None` · 0 이하·수가 아닌 값은 `ValueError`(조용히 접지 않음)."""
cleaned = re.sub(r"[\s,]", "", str(text or ""))
if not cleaned:
return None
try:
value = Decimal(cleaned)
except InvalidOperation as exc:
raise ValueError(f"조림목 본수는 수로 넣어야 합니다: {text}") from exc
if value <= 0:
raise ValueError(f"조림목 본수는 0 보다 커야 합니다: {text}")
return value
def _per_hundred(node: dict[str, Any], table_id: str) -> tuple[Decimal, str] | None:
"""(인/100본 값, 인력 이름) — 표에서 읽음. 칸이 달라지면 `None`."""
from B09_Estimation.B09_Estimation_ResourceAxis import parse_amount
table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == table_id), {})
for row in table.get("raw_row") or []:
cells = ["".join(str(c).split()) for c in row]
if "인/100본" in cells:
unit = cells.index("인/100본")
amount = parse_amount(cells[unit + 1]) if len(cells) > unit + 2 else None
if amount is not None and cells[unit + 2]:
return amount, cells[unit + 2]
return None
def attach_seedling_finding(
build: Any, nodes: dict[str, dict[str, Any]], seedlings_per_ha: Decimal | None
) -> None:
"""본수가 들면 갈래 제목마다 묘목찾기 인력을 붙이고, 없으면 일부만 + 사유."""
from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind
book = build.book
for code, table_id in SEEDLING_TABLES.items():
found = _per_hundred(nodes.get(code) or {}, table_id)
titles = [t for t in book.titles if t.startswith(f"B-{code}#")]
labels = [
label for label in build.unattached.get(code) or [] if "".join(label.split()) != NAME
]
labor = next(
(
c
for c, t in book.titles.items()
if found and t.kind is PriceKind.LABOR and "#" not in c and t.name == found[1]
),
None,
)
if found is None or labor is None or not titles:
build.component_gaps[code] = "묘목찾기 줄 칸이 달라져 못 읽음"
build.partial_ratio.setdefault(code, Decimal(0))
continue
amount, name = found
if seedlings_per_ha is None:
reason = MISSING.format(amount=amount)
build.component_gaps[code] = reason
build.partial_ratio.setdefault(code, Decimal(0))
labels.append(reason)
else:
note = f"묘목찾기 {amount}인/100본 × 조림목 {seedlings_per_ha}본/ha ÷ 100 — [주]④⑤ 합하여 적용"
for title in titles:
book.add_detail(
PriceDetail(title, labor, amount * seedlings_per_ha / 100, note=note)
)
build.unattached[code] = labels