Files
Aislo/B09_Estimation/B09_Estimation_Store.py
T
eomsangdonandClaude Opus 5.5 58283a578f feat(b09): 총괄 요율 자동 고름 · 사용자 값 앞섬 · 고르는 값 칸(166-1)
- Rates.rate_table: M01 조달청 제비율 압축 표(구간 위 끝 · 토목) → payload rate_table
- Calc_Total 짝(py · ts): pick_rates 자동 고름(직접공사비 × 공기 · 추정가격 · 안전 대상액 + 기초액 · 연도 · 환경 공종) · 사용자 값 앞섬
- 총괄 탭: 추정가격 · 공사기간 · 환경보전비 공종 칸 · 요율 출처 칸(자동 근거 풍선 · 누르면 자동으로)
- Store: environment_kind 칸
- 시험: test_166_1 새 · test_145_4 node 입력을 stdin 으로(명령 줄 길이)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
2026-10-10 00:06:34 +09:00

115 lines
4.1 KiB
Python

"""B09 원가 저장 자리(PLAN 144-1) — 프로젝트 안 `B09_Estimation/v1/`.
price_book.json 굳힌 단가 한 벌 {"made": 시각, "rows": {키: 입력 줄},
"driver_factor": …, "fx": […]}
— [단가 갱신] 때만 M01 로 새로 ·
[저장] 때 책에 없는 새 줄만 더함(있는 줄 안 바꿈)
user_values.json 사용자 값 {"materials": {자재 키: {applied?, supply?}}, "estimate": {…}}
— 고친 칸만(키가 있으면 그 값 · null = 빈칸 · 없으면 기본)
결과(표)는 저장하지 않음 — GET 때 서버가 셈(B08 꼴).
"""
from __future__ import annotations
import json
import math
import os
from pathlib import Path
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY
FOLDER = ("B09_Estimation", "v1")
BOOK = "price_book.json"
USER = "user_values.json"
#: 원가계산 사용자 입력 칸 — 요율 고르는 값 · 손입력(엑셀 총괄 · 원가계산서 · 내역 손입력 칸)
ESTIMATE_FIELDS = (
"estimated_price", # 추정가격(원) — 요율 구간 고름
"duration_days", # 공사기간(일)
"work_type", # 공종(글)
"environment_kind", # 환경보전비 공종 — road 도로 · 그 밖 = 그 밖의 토목(166-1)
"safety_base", # 산업안전보건관리비 기초액(원)
"profit_adjust", # 이윤 보정(원 · 빼는 값)
"vat_adjust", # 부가가치세 조정(원)
"indirect_material", # 간접재료비(원)
"scrap", # 작업설 부산물(원 · 빼는 값)
"procurement_fee_pct", # 조달수수료율(%)
)
TEXT_FIELDS = ("work_type", "environment_kind")
def folder(project_root: str | Path) -> Path:
return Path(project_root).joinpath(*FOLDER)
def _read(path: Path) -> dict[str, Any] | None:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
return data if isinstance(data, dict) else None
def _write(path: Path, data: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temp = path.with_suffix(".tmp")
temp.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
os.replace(temp, path)
def read_book(project_root: str | Path) -> dict[str, Any] | None:
book = _read(folder(project_root) / BOOK)
if book is None or not isinstance(book.get("rows"), dict):
return None
return book
def write_book(project_root: str | Path, book: dict[str, Any]) -> None:
_write(folder(project_root) / BOOK, book)
def _num(value: Any) -> float | None:
if isinstance(value, bool) or not isinstance(value, (int, float)):
return None
return float(value) if math.isfinite(value) else None
def clean_user(raw: Any) -> dict[str, Any]:
"""사용자 값 다듬기 — 모르는 칸 버림 · 수 칸은 수 · null(빈칸)만."""
raw = raw if isinstance(raw, dict) else {}
materials: dict[str, dict[str, Any]] = {}
for key, cell in (raw.get("materials") or {}).items():
if not isinstance(cell, dict):
continue
out: dict[str, Any] = {}
if "applied" in cell:
out["applied"] = _num(cell["applied"])
if cell.get("supply") in SUPPLY:
out["supply"] = cell["supply"]
if out:
materials[str(key)] = out
estimate: dict[str, Any] = {}
given = raw.get("estimate") if isinstance(raw.get("estimate"), dict) else {}
for field in ESTIMATE_FIELDS:
if field not in given:
continue
value = given[field]
estimate[field] = (str(value) if value else None) if field in TEXT_FIELDS else _num(value)
rates = given.get("rates")
if isinstance(rates, dict):
estimate["rates"] = {str(k): _num(v) for k, v in rates.items()}
return {"materials": materials, "estimate": estimate}
def read_user(project_root: str | Path) -> dict[str, Any]:
return clean_user(_read(folder(project_root) / USER))
def write_user(project_root: str | Path, user: dict[str, Any]) -> dict[str, Any]:
clean = clean_user(user)
_write(folder(project_root) / USER, clean)
return clean