feat(821-2): M01 기계 화면 — 목록 열 줄이기 · 상세 페이지 · 새 기계(비슷한 기계에서 시작) · 저장 때 계수 서버 재계산
- 목록 = 이름 · 규격 · 구분 · 가격 · 손료계수 · 주연료 · 조종원 · 시간당 경비(서버 값) · 별칭 · 비고 (단위 · 출처 열 없음 · 가로 넘침 없음) · 다른 요소 표 그대로 - 상세 = 기계 양식 + 원문번호 · 원문 · 구분 · 상세구분 · 별칭 · 비고 · 쓰는 곳 · 지우기(쓰는 곳 보이고 확인) · 주소 ?machine= 로 바로 열림 · 뒤로 가기 = 목록 · 저장 안 한 고침 나가기 경고 - 고침 = 초안 → 페이지 [저장](자동저장 없음) · 서버 M01_MasterData_Machine.fix = 품셈 입력이 바뀌면 계수 짝으로 다시 셈 · 단위 · 출처 칸 채움(새 칸 = 마스터 단위 · 「자체」) - API /api/m01/machine/lists · hourly · uses · find · row (시스템 관리자만) · 시험 test_821_2_machine_save.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""M01 기계 화면(PLAN 821-2) — 양식 목록 · 시간당 경비(서버 값) · 쓰는 곳 · 비슷한 기계 찾기 ·
|
||||
저장 전 기계 줄 손질(계수 서버 재계산 · 단위 · 출처 채움).
|
||||
|
||||
계수 = `master_machine_coef`(TS 짝 `common_util/common_util_machine_coef.ts`) ·
|
||||
시간당 = 서버 기계경비식(`M02_Machine_Hourly.hourly` · 전국평균 · 기본 옵션) ·
|
||||
마스터 파일은 읽기만 — 쓰기는 `Store_Make.save`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import master_machine_coef as mc # noqa: E402 (`store` 가 scripts 를 길에 둠)
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
FILE = "기계.json"
|
||||
OWN_SOURCE = "자체"
|
||||
_EQ = re.compile(r"EQ\d{6}")
|
||||
_cache: dict[str, Any] = {"ver": None, "hourly": {}, "uses": None}
|
||||
|
||||
|
||||
def _rows() -> list[dict]:
|
||||
return store.read(FILE)[0].get("줄") or []
|
||||
|
||||
|
||||
def _fresh() -> None:
|
||||
"""마스터를 다시 읽었으면(저장 뒤) 셈 보관을 비움."""
|
||||
ver = id(store.loaded()[1])
|
||||
if _cache["ver"] != ver:
|
||||
_cache.update(ver=ver, hourly={}, uses=None)
|
||||
|
||||
|
||||
def _plain(v):
|
||||
if isinstance(v, Decimal):
|
||||
return int(v) if v == v.to_integral_value() else float(v)
|
||||
return v
|
||||
|
||||
|
||||
# ── 양식 목록 ────────────────────────────────────────────────────────
|
||||
def lists() -> dict:
|
||||
"""연료(유가 전국평균) · 직종(인력) · 전력 단가 · 시간환산 · 원문 · 구분 — 양식 고르기."""
|
||||
files = store.loaded()[0]
|
||||
oil = files.get("재료_유가전력.json", {}).get("줄") or []
|
||||
fuels = [
|
||||
{
|
||||
"value": str(r["원문번호"]).split(":")[0],
|
||||
"text": str(r["원문번호"]).split(":")[0],
|
||||
"price": _plain(r.get("값")),
|
||||
}
|
||||
for r in oil
|
||||
if r.get("구분") == "유류" and str(r.get("원문번호", "")).endswith(":전국평균")
|
||||
]
|
||||
power = next(
|
||||
(_plain(r.get("값")) for r in oil if r.get("원문번호") == "일반용갑I저압:전력량_평균"), 0
|
||||
)
|
||||
jobs = [
|
||||
{
|
||||
"value": str(r["원문번호"]),
|
||||
"text": f"{r['원문번호']} {r.get('이름') or ''}",
|
||||
"price": _plain(r.get("값")),
|
||||
}
|
||||
for r in files.get("인력.json", {}).get("줄") or []
|
||||
if isinstance(r.get("값"), (int, float, Decimal))
|
||||
]
|
||||
head = store.read(FILE)[0]
|
||||
hour = Decimal(1) / Decimal(8)
|
||||
for t in files.get("계수_건설품셈_08장_건설기계.json", {}).get("표") or []:
|
||||
if t.get("키") == "CC000336":
|
||||
got = next((x for x in t.get("줄") or [] if x.get("구분") == "시간환산"), None)
|
||||
if got:
|
||||
hour = Decimal(got["분자"]) / Decimal(got["분모"])
|
||||
return {
|
||||
"fuels": fuels,
|
||||
"jobs": jobs,
|
||||
"power": power,
|
||||
"hour": float(hour),
|
||||
"books": list(head.get("원문") or {}),
|
||||
"groups": {k: (v or {}).get("원문") for k, v in (head.get("구분") or {}).items()},
|
||||
}
|
||||
|
||||
|
||||
# ── 시간당 경비 ──────────────────────────────────────────────────────
|
||||
def hourly(keys: list[str]) -> dict[str, dict]:
|
||||
"""키마다 {노무비 · 재료비 · 경비 · 계} 또는 {reason} — 마스터 판이 같으면 한 번만 셈."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
_fresh()
|
||||
out = {}
|
||||
for key in keys[:100]:
|
||||
if key not in _cache["hourly"]:
|
||||
got = mh.hourly(key)
|
||||
_cache["hourly"][key] = (
|
||||
{k: got[k] for k in ("노무비", "재료비", "경비", "계", "로직")}
|
||||
if got.get("ok")
|
||||
else {"reason": got.get("reason")}
|
||||
)
|
||||
out[key] = _cache["hourly"][key]
|
||||
return out
|
||||
|
||||
|
||||
# ── 쓰는 곳 ─────────────────────────────────────────────────────────
|
||||
def _logic_index() -> tuple[dict[str, list[str]], dict[str, list[str]]]:
|
||||
"""(원문번호 → 로직 「키 이름」 · 기계 키 → 로직) — 기계 고르기 · 식 안 EQ 키."""
|
||||
_fresh()
|
||||
if _cache["uses"] is None:
|
||||
by_code: dict[str, list[str]] = {}
|
||||
by_key: dict[str, list[str]] = {}
|
||||
for data in store.loaded()[0].values():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄") or []:
|
||||
label = f"로직 {row.get('키')} {row.get('이름') or ''}".strip()
|
||||
for i in row.get("입력") or []:
|
||||
if i.get("이름") == "기계":
|
||||
for c in i.get("고르기") or []:
|
||||
by_code.setdefault(str(c), []).append(label)
|
||||
text = json.dumps(row, ensure_ascii=False, default=str)
|
||||
for k in set(_EQ.findall(text)):
|
||||
by_key.setdefault(k, []).append(label)
|
||||
_cache["uses"] = (by_code, by_key)
|
||||
return _cache["uses"]
|
||||
|
||||
|
||||
def uses(key: str) -> list[str]:
|
||||
"""그 기계를 쓰는 곳 — 로직(기계 고르기 · 식) · M02 단가산출(사본 · 줄에 키)."""
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
row = next((r for r in _rows() if r.get("키") == key), None)
|
||||
if row is None:
|
||||
raise store.StoreError(404, f"없는 기계 「{key}」")
|
||||
by_code, by_key = _logic_index()
|
||||
code = str(row.get("원문번호") or "")
|
||||
out = list(dict.fromkeys([*(by_code.get(code, []) if code else []), *by_key.get(key, [])]))
|
||||
mark = f'"{key}'.encode()
|
||||
for where, path in layers.layer_files("sangeun"):
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
if mark in raw:
|
||||
try:
|
||||
name = json.loads(raw).get("이름") or path.stem
|
||||
except ValueError:
|
||||
name = path.stem
|
||||
out.append(f"M02 단가산출 {where} {name}")
|
||||
return out
|
||||
|
||||
|
||||
# ── 비슷한 기계 찾기 · 한 줄 ─────────────────────────────────────────
|
||||
def find(q: str, limit: int = 30) -> list[dict]:
|
||||
words = [w for w in q.lower().split() if w]
|
||||
out = []
|
||||
for r in _rows():
|
||||
hay = " ".join(str(r.get(k) or "") for k in ("이름", "규격", "별칭", "상세구분", "키"))
|
||||
hay = hay.lower()
|
||||
if all(w in hay for w in words):
|
||||
out.append(_brief(r))
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def _brief(r: dict) -> dict:
|
||||
keep = ("키", "원문번호", "원문", "구분", "상세구분", "이름", "규격", "값", "별칭", "비고")
|
||||
return {k: r[k] for k in keep if k in r}
|
||||
|
||||
|
||||
def row(key: str) -> dict:
|
||||
r = next((x for x in _rows() if x.get("키") == key), None)
|
||||
if r is None:
|
||||
raise store.StoreError(404, f"없는 기계 「{key}」")
|
||||
return {"version": store.read(FILE)[1], "row": r}
|
||||
|
||||
|
||||
# ── 저장 전 손질(`Store_Make.save` 가 부름) ──────────────────────────
|
||||
def _units() -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for r in _rows():
|
||||
for k, u in (r.get("단위") or {}).items():
|
||||
out.setdefault(k, u)
|
||||
return out
|
||||
|
||||
|
||||
def _same_inputs(a: dict, b: dict) -> bool:
|
||||
return all(
|
||||
float(a[k]) == float(b[k]) if isinstance(b.get(k), (int, float, Decimal)) else False
|
||||
for k in mc.INPUTS
|
||||
)
|
||||
|
||||
|
||||
def fix(part: dict) -> None:
|
||||
"""기계 줄 고침 · 더함 — 계수는 서버가 짝으로 다시 셈(품셈 입력이 저장 값과 같으면
|
||||
저장 값 그대로) · 단위 · 출처 묶음 = 값 칸과 같은 칸
|
||||
(있던 칸 그대로 · 새 칸 = 마스터 단위 · 출처 「자체」)."""
|
||||
if part.get("file") != FILE:
|
||||
return
|
||||
stored = {str(r.get("키")): r for r in _rows()}
|
||||
units = _units()
|
||||
for ch in part.get("changes") or []:
|
||||
row = ch.get("row")
|
||||
if (
|
||||
ch.get("op") == "delete"
|
||||
or not isinstance(row, dict)
|
||||
or not isinstance(row.get("값"), dict)
|
||||
):
|
||||
continue
|
||||
old = stored.get(str(ch.get("key") or "")) if ch.get("op") == "edit" else None
|
||||
values = row["값"]
|
||||
old_values = (old or {}).get("값") or {}
|
||||
if all(isinstance(values.get(k), (int, float)) for k in mc.INPUTS):
|
||||
if old and _same_inputs(values, old_values):
|
||||
values.update({k: old_values[k] for k in mc.COEFS if k in old_values})
|
||||
elif (got := mc.coef_of(values)) is not None:
|
||||
values.update(got)
|
||||
was_unit = (old or {}).get("단위") or {}
|
||||
was_src = (old or {}).get("출처") or {}
|
||||
row["단위"] = {k: was_unit.get(k, units.get(k, "")) for k in values}
|
||||
row["출처"] = {k: was_src.get(k, OWN_SOURCE) for k in values}
|
||||
Reference in New Issue
Block a user