Merge remote-tracking branch 'origin/sub_desktop_1' into main_laptop_1 — 166-2 설계내역서 손질(Store docstring · import 양쪽 살림)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
This commit is contained in:
2026-10-10 00:16:16 +09:00
co-authored by Claude Fable 5.1
10 changed files with 802 additions and 45 deletions
+55 -2
View File
@@ -5,7 +5,8 @@
— [단가 갱신] 때만 M01 로 새로 ·
[저장] 때 책에 없는 새 줄만 더함(있는 줄 안 바꿈)
user_values.json 사용자 값 {"materials": {자재 키: {applied?, supply?, quote?, quote_page?,
pick?}}, "estimate": {…}}
pick?}}, "estimate": {…},
"bill": {"qty": {줄 키: 수}, "rows": [손 줄]}}(166-2 설계내역서 손질)
— 고친 칸만(키가 있으면 그 값 · null = 빈칸 · 없으면 기본)
quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) ·
@@ -25,6 +26,7 @@ from typing import Any
from urllib.parse import unquote
from B09_Estimation.B09_Estimation_Calc_Base import SOURCES, SUPPLY
from B09_Estimation.B09_Estimation_Calc_Bill import SECTIONS
FOLDER = ("B09_Estimation", "v1")
BOOK = "price_book.json"
@@ -86,6 +88,56 @@ def _num(value: Any) -> float | None:
return float(value) if math.isfinite(value) else None
def _text(value: Any) -> str:
return str(value)[:200] if isinstance(value, (str, int, float)) else ""
def _clean_ref(ref: Any) -> dict[str, Any] | None:
"""손 줄 연결 — dg · sg = 호표 키 · logic = M01 로직(입력값 · 켜쌓기율 · 고름)."""
if not isinstance(ref, dict):
return None
tab = ref.get("tab")
if tab in ("dg", "sg") and isinstance(ref.get("key"), str) and ref["key"]:
return {"tab": tab, "key": ref["key"]}
if tab == "logic" and isinstance(ref.get("code"), str) and ref["code"]:
given = ref.get("given") if isinstance(ref.get("given"), dict) else {}
picks = ref.get("picks") if isinstance(ref.get("picks"), dict) else None
return {"tab": "logic", "code": ref["code"], "name": _text(ref.get("name")),
"given": {str(k): v for k, v in given.items() if isinstance(v, (str, int, float))},
"rate": _num(ref.get("rate")), "picks": picks} # fmt: skip
return None
def clean_bill(raw: Any) -> dict[str, Any]:
"""설계내역서 손질(166-2) — 수량 덮어쓰기(수만) · 손 줄(`hand:` 키 · 아는 공종만)."""
raw = raw if isinstance(raw, dict) else {}
qty = raw.get("qty") if isinstance(raw.get("qty"), dict) else {}
codes = {code for code, _name, _parent in SECTIONS}
rows = []
for row in raw.get("rows") if isinstance(raw.get("rows"), list) else []:
if not isinstance(row, dict) or not str(row.get("key") or "").startswith("hand:"):
continue
if row.get("section") not in codes:
continue
rows.append(
{
"key": str(row["key"]),
"section": row["section"],
"after": _text(row.get("after")),
**{k: _text(row.get(k)) for k in ("name", "spec", "unit")},
"qty": _num(row.get("qty")),
"ref": _clean_ref(row.get("ref")),
}
)
out: dict[str, Any] = {}
kept = {str(k): _num(v) for k, v in qty.items() if _num(v) is not None}
if kept:
out["qty"] = kept
if rows:
out["rows"] = rows
return out
def clean_user(raw: Any) -> dict[str, Any]:
"""사용자 값 다듬기 — 모르는 칸 버림 · 수 칸은 수 · null(빈칸)만."""
raw = raw if isinstance(raw, dict) else {}
@@ -116,7 +168,8 @@ def clean_user(raw: Any) -> dict[str, Any]:
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}
bill = clean_bill(raw.get("bill"))
return {"materials": materials, "estimate": estimate, **({"bill": bill} if bill else {})}
def read_user(project_root: str | Path) -> dict[str, Any]: