wip(M01): 자재 조합 M01 길 초안 — 합치기 전 임시(sub4 자재조합 표 위에 얹을 예정)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoyUwuTFiDJfvCfHpvKKzp
This commit is contained in:
@@ -15,6 +15,7 @@ from pydantic import BaseModel
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M01_MasterData import M01_MasterData_Store_Matset as matset_store
|
||||
|
||||
router = APIRouter(prefix="/api/m01", tags=["M01 MasterData"])
|
||||
|
||||
@@ -114,6 +115,20 @@ class PreviewCombo(BaseModel):
|
||||
inputs: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
class NewMatset(BaseModel):
|
||||
"""자재 조합 — 담은 자재 목록만(코드 · 이름 · 규격 · 단위 · 수량 · 단가는 안 둠)."""
|
||||
|
||||
matset: dict[str, Any]
|
||||
owner: str = matset_store.OWNERS[0]
|
||||
|
||||
|
||||
class EditMatset(BaseModel):
|
||||
key: str
|
||||
version: str
|
||||
matset: dict[str, Any]
|
||||
owner: str = ""
|
||||
|
||||
|
||||
class DraftLogic(BaseModel):
|
||||
text: str # 줄로 적은 식 — 줄마다 「이름 = 식」
|
||||
decided: dict[str, Any] = {} # 변수마다 정한 것 {이름: {무엇, 단위, 비목, 요소·찾기·값}}
|
||||
@@ -349,3 +364,29 @@ def post_combo_delete(body: DeleteCombo) -> dict:
|
||||
def post_combo_preview(body: PreviewCombo) -> dict:
|
||||
"""담은 로직마다 값을 뽑아 비목별로 갈라 합계 — 저장하지 않음."""
|
||||
return _call(combo_store.combo_preview, body.combo, body.inputs)
|
||||
|
||||
|
||||
# ── 자재 조합(계약 9장 끝) ─────────────────────────────────────────────
|
||||
@router.get("/matsets")
|
||||
def get_matsets(q: str = "", owner: str = "") -> dict:
|
||||
return {"matsets": _call(matset_store.matsets, q, owner)}
|
||||
|
||||
|
||||
@router.get("/matset")
|
||||
def get_matset(key: str) -> dict:
|
||||
return _call(matset_store.matset, key)
|
||||
|
||||
|
||||
@router.post("/matset/new")
|
||||
def post_matset_new(body: NewMatset) -> dict:
|
||||
return _call(matset_store.matset_new, body.matset, body.owner)
|
||||
|
||||
|
||||
@router.post("/matset/edit")
|
||||
def post_matset_edit(body: EditMatset) -> dict:
|
||||
return _call(matset_store.matset_edit, body.key, body.version, body.matset, body.owner)
|
||||
|
||||
|
||||
@router.post("/matset/delete")
|
||||
def post_matset_delete(body: DeleteCombo) -> dict:
|
||||
return _call(matset_store.matset_delete, body.key, body.version)
|
||||
|
||||
@@ -218,15 +218,15 @@ def combo_edit(key: str, version: str, combo_: dict, owner: str = "") -> dict:
|
||||
return {**done, "combo": _find(store.read(FILE)[0], key)}
|
||||
|
||||
|
||||
def combo_users(key: str) -> list[str]:
|
||||
"""이 조합을 「일위대가」 로 쓰는 구조물 열 이름들 — 집계표 · SS 장부를 함께 봄."""
|
||||
def users_of(field: str, key: str) -> list[str]:
|
||||
"""집계표 · SS 장부에서 열 칸 `field`(「일위대가」 · 「자재조합」)가 `key` 인 열 이름들."""
|
||||
names: dict[str, str] = {}
|
||||
for path in USERS:
|
||||
if not path.is_file():
|
||||
continue
|
||||
doc = json.loads(path.read_text(encoding="utf-8"))
|
||||
for col in doc.get("열") or doc.get("줄") or []:
|
||||
if col.get("일위대가") != key:
|
||||
if col.get(field) != key:
|
||||
continue
|
||||
head = col.get("머리") or [col.get(k) for k in ("종류", "분류1", "분류2", "분류3")]
|
||||
cid = str(col.get("id") or col.get("열"))
|
||||
@@ -234,6 +234,10 @@ def combo_users(key: str) -> list[str]:
|
||||
return list(names.values())
|
||||
|
||||
|
||||
def combo_users(key: str) -> list[str]:
|
||||
return users_of("일위대가", key)
|
||||
|
||||
|
||||
def combo_delete(key: str, version: str) -> dict:
|
||||
data, _ = _read()
|
||||
_find(data, key)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""M01 자재 조합 저장소 — 목록 · 하나 읽기 · 만들기 · 고치기 · 지우기.
|
||||
|
||||
계약: `_화면_계약.md` 9장 끝 · 파일 모양: `_틀.md` 11장 · 엔진: `scripts/master_matset.py`.
|
||||
조합은 담은 자재 목록(코드 · 이름 · 규격 · 단위)만 지님 — 수량 · 단가는 두지 않음(재료산출 몫).
|
||||
자리는 `자재조합.json` 한 파일 · 키는 `_키대장.json` 의 `UM` 다음 번호(서버가 줌).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combos
|
||||
|
||||
cm = store.cm
|
||||
msk = cm.msk
|
||||
|
||||
FILE = msk.FILE
|
||||
OWNERS = msk.OWNERS
|
||||
|
||||
|
||||
def _path() -> Path:
|
||||
return store.FOLDER / FILE
|
||||
|
||||
|
||||
def _make_file() -> None:
|
||||
"""파일이 없으면 머리만 두고 만듦."""
|
||||
if not _path().is_file():
|
||||
head = {"그룹": msk.GROUP, "판": str(date.today().year), "줄": []}
|
||||
_path().write_text(store.dump(head), encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def _read() -> tuple[dict, str]:
|
||||
_make_file()
|
||||
return store.read(FILE)
|
||||
|
||||
|
||||
def _keys() -> list[str]:
|
||||
return [str(r.get("키")) for r in _read()[0].get("줄") or []]
|
||||
|
||||
|
||||
def _find(data: dict, key: str) -> dict:
|
||||
for row in data.get("줄") or []:
|
||||
if str(row.get("키")) == key:
|
||||
return row
|
||||
raise store.StoreError(404, f"없는 자재 조합 「{key}」")
|
||||
|
||||
|
||||
def _clean(one: dict, owner: str, key: str | None = None) -> dict:
|
||||
"""서버가 잡는 칸 — 키 · 소유 · 열 한 벌(안 준 칸은 null · 자재 줄은 네 칸만)."""
|
||||
if not isinstance(one, dict):
|
||||
raise store.StoreError(400, "자재 조합 줄이 묶음 아님")
|
||||
name = one.get("이름")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
raise store.StoreError(400, "자재 조합 줄에 「이름」 이 없거나 글자 아님")
|
||||
if owner not in OWNERS:
|
||||
raise store.StoreError(400, f"모르는 소유 「{owner}」 — {' · '.join(OWNERS)}")
|
||||
origin = one.get("본뜬키")
|
||||
if origin is not None and (origin == key or origin not in _keys()):
|
||||
raise store.StoreError(400, f"본뜬키 「{origin}」 — 비었거나 있는 다른 자재 조합 키여야 함")
|
||||
got = one.get("담은자재")
|
||||
if isinstance(got, list): # 화면이 덤을 실어도 자재 줄은 네 칸만 · 안 준 칸은 빈 글
|
||||
got = [{c: m.get(c, "") for c in msk.ITEM_COLS} if isinstance(m, dict) else m for m in got]
|
||||
rest = {k: v for k, v in one.items() if k not in ("키", "원문번호", "소유")}
|
||||
row = {**({"키": key} if key else {}), "소유": owner, **rest, "담은자재": got or []}
|
||||
for col in msk.COLS:
|
||||
row.setdefault(col, None)
|
||||
return {k: row[k] for k in msk.COLS if k in row}
|
||||
|
||||
|
||||
def _write(changes: list[dict], version: str | None = None) -> dict:
|
||||
_make_file()
|
||||
return store.save(
|
||||
[{"file": FILE, "version": version or store.read(FILE)[1], "changes": changes}]
|
||||
)[0]
|
||||
|
||||
|
||||
def matsets(q: str = "", owner: str = "") -> list[dict]:
|
||||
"""목록 — 찾기(이름 · 키)와 소유로 거름 · 줄마다 자재 수."""
|
||||
out = []
|
||||
for _, row in msk.rows(store.loaded()[0]):
|
||||
if owner and row.get("소유") != owner:
|
||||
continue
|
||||
if not store._hit(row, q):
|
||||
continue
|
||||
out.append(
|
||||
{k: row.get(k) for k in msk.COLS if k != "담은자재"}
|
||||
| {"file": FILE, "count": len(msk.items(row))}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def matset(key: str) -> dict:
|
||||
data, version = _read()
|
||||
return {"file": FILE, "version": version, "matset": _find(data, key)}
|
||||
|
||||
|
||||
def matset_new(one: dict, owner: str = OWNERS[0]) -> dict:
|
||||
"""새 자재 조합 — 키는 서버가 `UM` 다음 번호로 줌."""
|
||||
before = set(_keys())
|
||||
done = _write([{"op": "add", "row": _clean(one, owner)}])
|
||||
key = next((k for k in reversed(_keys()) if k not in before), "")
|
||||
return {**done, "key": key, "matset": _find(_read()[0], key) if key else {}}
|
||||
|
||||
|
||||
def matset_edit(key: str, version: str, one: dict, owner: str = "") -> dict:
|
||||
data, _ = _read()
|
||||
was = _find(data, key)
|
||||
if store.read(FILE)[1] != version:
|
||||
raise store.StoreError(409, {"stale": [FILE]})
|
||||
fixed = _clean(one, owner or str(was.get("소유") or OWNERS[0]), key)
|
||||
done = _write([{"op": "edit", "key": key, "row": fixed}], version)
|
||||
return {**done, "matset": _find(_read()[0], key)}
|
||||
|
||||
|
||||
def matset_delete(key: str, version: str) -> dict:
|
||||
"""지우기 — 구조물 열이 「자재조합」 칸으로 쓰면 409 + 구조물 이름들(지우지 않음)."""
|
||||
_find(_read()[0], key)
|
||||
if users := combos.users_of("자재조합", key):
|
||||
msg = f"구조물 {len(users)} 곳이 쓰는 자재 조합이라 못 지움 — {' · '.join(users)}"
|
||||
raise store.StoreError(409, {"errors": [msg], "users": users})
|
||||
if store.read(FILE)[1] != version:
|
||||
raise store.StoreError(409, {"stale": [FILE]})
|
||||
return _write([{"op": "delete", "key": key}], version)
|
||||
Reference in New Issue
Block a user