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,291 @@
"""B09 제비율 요율표 화면 자료 — STmate 「원가계산기준」(wM_KAN_RATE) 첫 탭 차례 (PLAN 12장).
⚠ 묶음 제목·산식 표기는 DFM `TWM_KAN_RATE` 첫 탭(☞ 공사원가계산 제잡비율) 차례,
값·구간은 우리 데이터.
⚠ 요율 칸마다 **주소**(`RateOverride.address_of`)와 **기본값(마스터)·지금 값(프로젝트)** 을
함께 실음 — 화면이 「기본값 ↔ 고친 값」을 갈라 보이고 [수 정]에서 그 칸만 고쳐 사유와 함께 돌려줌.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Any
from B09_Estimation.B09_Estimation_CostSheet import SAFETY_LABELS, WORK_TYPE_LABELS
from B09_Estimation.B09_Estimation_RateOverride import address_of
from B09_Estimation.B09_Estimation_Rates import RateDataset, _bracket_bounds, _duration_bounds
_EOK = Decimal(100_000_000)
def _won(amount: Decimal) -> str:
"""원 → 「50억」·「5천만」 표기."""
if amount >= _EOK:
return f"{amount / _EOK:g}"
return f"{amount / 10_000_000:g}천만"
def amount_label(label: str) -> str:
"""구간 키 → 사람 표기. 모르는 키는 그대로(지어내지 않음)."""
bounds = _bracket_bounds(label)
if bounds is None:
return label
low, high = bounds
if low == 0:
return f"{_won(high)} 미만"
if high == Decimal("Infinity"):
return f"{_won(low)} 이상" + (" " + label.split("_", 3)[-1] if label.count("_") > 2 else "")
return f"{_won(low)}~{_won(high)} 미만"
def duration_label(label: str) -> str:
bounds = _duration_bounds(label)
if bounds is None:
return label
low, high = bounds
if low == 0:
return f"{high}일 이하"
if high >= 10**9:
return f"{low}일 이상"
return f"{low}~{high}"
def _type(value: str, safety: bool = False) -> str:
return {**WORK_TYPE_LABELS, **(SAFETY_LABELS if safety else {})}.get(value, value)
class _Cells:
"""요율 칸 만들기 — 지금 판(덮어쓴 복사본)과 마스터 판을 같은 주소로 맞대 봄."""
def __init__(self, current: RateDataset, master: RateDataset) -> None:
self.current, self.master = current.variables, master.variables
def cell(self, variable: str, table: str | None, row: dict[str, Any] | None) -> dict[str, Any]:
address = address_of(variable, table, row)
value = self._rate(self.current, address)
default = self._rate(self.master, address)
return {
"value": value,
"default": default,
"overridden": value != default,
"address": address,
}
@staticmethod
def _rate(variables: dict[str, Any], address: dict[str, Any]) -> Any:
variable = variables[address["variable"]]
if not address["table"]:
return variable.get("rate_percent")
for row in variable[address["table"]]:
if all(str(row.get(k)) == str(v) for k, v in address["match"].items()):
return row.get("rate_percent")
return None
def _pivot(rows: list[dict[str, Any]], row_keys: tuple[str, ...], col_key: str):
"""세로 줄을 (구간…) × 공종 표로 편다 — DFM 표 모양(행 구간 · 열 공종)."""
columns = list(dict.fromkeys(str(row[col_key]) for row in rows))
grid: dict[tuple[str, ...], dict[str, Any]] = {}
for row in rows:
grid.setdefault(tuple(str(row[k]) for k in row_keys), {})[str(row[col_key])] = row
return columns, grid
def rate_sections(dataset: RateDataset, master: RateDataset | None = None) -> list[dict[str, Any]]:
"""묶음 목록 `{title, formula, columns, rows}` — DFM 첫 탭 위→아래 차례.
`rows` 의 요율 칸은 `{value, default, overridden, address}` · 이름 칸은 글자.
"""
v = dataset.variables
c = _Cells(dataset, master or dataset)
sections: list[dict[str, Any]] = []
profit = {row["estimated_price_bracket"]: row for row in v["rate_profit"]["brackets"]}
sections.append(
{
"title": "일반관리비 · 이윤",
"formula": "일반관리비 (재+노+경) × 율 · 이윤 (노+경+일) × 율",
"columns": ["공사규모(추정가격)", "일반관리비(%)", "이윤(%)"],
"rows": [
[
amount_label(row["estimated_price_bracket"]),
c.cell("rate_overhead", "civil_landscape_industrial", row),
c.cell("rate_profit", "brackets", profit[row["estimated_price_bracket"]])
if row["estimated_price_bracket"] in profit
else "",
]
for row in v["rate_overhead"]["civil_landscape_industrial"]
],
}
)
for variable, title, formula in (
("rate_indirect_labor", "간접노무비", "(직접노무비) × 율(%)"),
("rate_other_expense", "기타경비", "(재료비+노무비) × 율(%)"),
):
columns, grid = _pivot(
v[variable]["brackets"], ("direct_cost_bracket", "duration_bracket"), "work_type"
)
sections.append(
{
"title": title,
"formula": formula,
"columns": ["공사규모(직접공사비)", "공사기간", *(_type(col) for col in columns)],
"rows": [
[
amount_label(size),
duration_label(days),
*(
c.cell(variable, "brackets", cells[col]) if col in cells else ""
for col in columns
),
]
for (size, days), cells in grid.items()
],
}
)
sections.append(
{
"title": "고용보험료",
"formula": "(노무비) ×",
"columns": ["등급", "추정금액", "요율(%)"],
"rows": [
[
str(row["grade"]),
amount_label(row["estimated_amount_bracket"]),
c.cell("rate_goyong", "brackets", row),
]
for row in v["rate_goyong"]["brackets"]
],
}
)
columns, grid = _pivot(
v["rate_safety_pct"]["brackets"], ("target_amount_bracket",), "work_type"
)
sections.append(
{
"title": "산업안전보건관리비",
"formula": "(재료비+직접노무비+관급자재) × 율 + 기초액",
"columns": [
"대상액",
*(f"{_type(col, safety=True)}(%)" for col in columns),
"기초액(원)",
],
"rows": [
[
amount_label(size),
*(
c.cell("rate_safety_pct", "brackets", cells[col]) if col in cells else ""
for col in columns
),
str(
next(
(
cells[col].get("base_amount_krw")
for col in columns
if cells.get(col, {}).get("base_amount_krw")
),
"",
)
),
]
for (size,), cells in grid.items()
],
}
)
sections.append(
{
"title": "환 경 보 전 비",
"formula": "(재료비+직접노무비+산출경비) × 율",
"columns": ["구 분", "요율(%)"],
"rows": [
[_type(row["work_type"]), c.cell("rate_environment", "all_work_types", row)]
for row in v["rate_environment"]["all_work_types"]
],
}
)
sections.append(
{
"title": "공사이행보증수수료",
"formula": "[추가금액 + (직접공사비-기준금액) × 율] × 공기(년)",
"columns": ["공사규모(직접공사비)", "수수료 산식"],
"rows": [
[
amount_label(row["direct_cost_bracket"]),
str(row.get("formula", ""))
.replace("direct_cost", "직공비")
.replace("duration_years", "공기(년)"),
]
for row in v["rate_performance_guarantee_fee"]["brackets"]
],
}
)
sections.append(
{
"title": "건설하도급대금지급보증서발급수수료",
"formula": "(직접공사비) ×",
"columns": ["공사규모", "요율(%)"],
"rows": [
[
amount_label(row["estimated_price_bracket"]),
c.cell("rate_subcontract_payment_guarantee", "brackets", row),
]
for row in v["rate_subcontract_payment_guarantee"]["brackets"]
],
}
)
equipment = v["rate_equipment_payment_guarantee"]
sections.append(
{
"title": "건설기계대여대금 지급보증서 발급금액",
"formula": "(직접공사비) ×",
"columns": ["구 분", "요율(%)"],
"rows": [
*(
[
_type(row["work_type"]),
c.cell("rate_equipment_payment_guarantee", "general_construction", row),
]
for row in equipment["general_construction"]
),
*(
[
"전문건설 — " + row["work_type"],
c.cell("rate_equipment_payment_guarantee", "specialty_construction", row),
]
for row in equipment["specialty_construction"]
),
],
}
)
year = int(str(dataset.effective_date)[:4] or 0)
pension_row = next((r for r in v["rate_pension"]["annual_rates"] if r["year"] == year), None)
sections.append(
{
"title": "퇴직부금비 · 법 정 부 담 금 · 보험료",
"formula": "",
"columns": ["항목", "산식", "요율(%)"],
"rows": [
[
"퇴직공제부금비",
"(직접노무비) ×",
c.cell("rate_retirement_mutual_aid", None, None),
],
["석면분담금", "(노무비) ×", c.cell("rate_asbestos_contribution", None, None)],
[
"임금채권부담금",
"(노무비) ×",
c.cell("rate_wage_claim_contribution", None, None),
],
["산재보험료", "(노무비) ×", c.cell("rate_sanjae", None, None)],
["건강보험료", "(직접노무비) ×", c.cell("rate_health", None, None)],
["장기요양보험료", "(건강보험료) ×", c.cell("rate_care", None, None)],
[
"연금보험료",
"(직접노무비) ×",
c.cell("rate_pension", "annual_rates", pension_row) if pension_row else "",
],
["부가가치세", "(공급가액) ×", c.cell("rate_vat", None, None)],
],
}
)
return sections