- POST /api/m02/structures/{열}/combo — 고친 담은 로직으로 새 UA(같은 로직 집합이면 있는 키) · 집계표 그 열만 새 키 · 판 어긋나면 조합 만들기 전 409 · SS 장부 맞춤
- 일위대가 조합 줄에 본뜬키 칸(원래 키) · 기존 37 줄 null · M01 고치기에서 안 보내도 유지
- 자재 조합 표 resources/master_data/자재조합.json(키 UM · 대장 등록 · 틀 검사) · POST /structures/{열}/material-combo · GET /material-combos/{키}
- 구조물집계표조합 칸 자재조합 · 분류 더하기로 복사한 열은 저장 때 원본 열의 일위대가 · 자재조합 키와 구조물 도면 · 면적산출 · 재료산출 내용을 새 번호로 받음
- Api_Fetch branchCombo · branchMaterialCombo · fetchMaterialCombo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vX53nJGC8ucBRFJns2n7B
152 lines
6.1 KiB
Python
152 lines
6.1 KiB
Python
"""조합 갈라 내기 — 구조물 열 하나가 가리키는 조합을 고친 내용으로 새로 만들어 그 열만 새 키를
|
|
가리키게 함(PLAN 15장 · 검토 `14_조합편집검토.md` 1장).
|
|
· 일위대가 조합(UA · `일위대가조합.json`) — 새 줄은 M01 조합 저장소가 만듦(부르기만)
|
|
· 자재 조합(UM · `자재조합.json`) — M01 마스터 저장(`save`)으로 더함(키 = 대장 다음 번호)
|
|
|
|
조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것의 집합(차례 · 메모 무시)이
|
|
이미 있는 조합과 같으면 새로 안 만들고 그 키 · 새 줄의 본뜬키 = 원래 키 · 소유 현장.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
|
|
_OWNER = "현장"
|
|
MATERIAL_FILE = "자재조합.json"
|
|
|
|
|
|
def _m01(fn, *args):
|
|
"""M01 막힘은 M02 막힘으로 — 상태 · 까닭 그대로."""
|
|
try:
|
|
return fn(*args)
|
|
except m01.StoreError as e:
|
|
raise store.StoreError(e.status, e.detail) from e
|
|
|
|
|
|
def _free_name(name: str, rows: list[dict]) -> str:
|
|
taken = {str(r.get("이름") or "") for r in rows}
|
|
n, out = 2, name
|
|
while out in taken:
|
|
out, n = f"{name} ({n})", n + 1
|
|
return out
|
|
|
|
|
|
def _branch(
|
|
column: str,
|
|
version: str,
|
|
field: str,
|
|
file: str,
|
|
slot: str,
|
|
ident: str,
|
|
items: list[dict],
|
|
source: str,
|
|
make: Callable[[str, list[dict]], dict],
|
|
name: str,
|
|
) -> dict[str, Any]:
|
|
"""집계표 판 검사 → 같은 내용 찾기 · 없으면 `make(이름, 줄들)` 로 새 줄 → 집계표 그 열 `field`.
|
|
`slot` = 담은 칸(담은로직 · 담은자재) · `ident` = 담은 줄의 알맹이 칸(로직 · 코드)."""
|
|
table = store.read("table", store.STRUCTURE_TABLE)
|
|
if version and table["판"] != version:
|
|
raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": table["판"]})
|
|
col = next((c for c in store._structure_columns() if c["id"] == column), None)
|
|
if col is None:
|
|
raise store.StoreError(404, f"{store.STRUCTURE_TABLE}에 없는 구조물 열 「{column}」")
|
|
if not isinstance(items, list) or not items:
|
|
raise store.StoreError(400, "담은 것이 비었음")
|
|
|
|
def body(got) -> frozenset[str]:
|
|
return frozenset(str(x.get(ident) or "") for x in got or [] if isinstance(x, dict))
|
|
|
|
rows = _m01(m01.read, file)[0].get("줄") or []
|
|
same = [r for r in rows if body(r.get(slot)) == body(items)]
|
|
hit = next((r for r in same if source and r.get("키") == source), same[0] if same else None)
|
|
if hit:
|
|
key, made_name, made = str(hit["키"]), str(hit.get("이름") or ""), False
|
|
else:
|
|
made_name = _free_name(name, rows)
|
|
key, made = make(made_name, rows), True
|
|
# ponytail: 조합 쓰기와 집계표 쓰기는 한 묶음 아님 — 사이에 집계표가 바뀌면 409 뒤 주인 없는
|
|
# 조합 하나가 남음(다시 하면 같은 내용이라 그 키를 씀)
|
|
doc = table["문서"]
|
|
for c in doc.get("열", []):
|
|
if isinstance(c, dict) and c.get("id") == column:
|
|
c[field] = key
|
|
info = store.write("table", store.STRUCTURE_TABLE, table["판"], doc) # SS 도 여기서 맞춤
|
|
return {"키": key, "이름": made_name, "새로만듦": made, "집계표판": info["판"]}
|
|
|
|
|
|
def branch_combo(
|
|
column: str, source: str, items: list[dict], name: str, version: str = ""
|
|
) -> dict[str, Any]:
|
|
"""일위대가 조합 갈라 내기 — 구분 · 상세구분 · 단위 · 출처 = 본뜬 것."""
|
|
was = _m01(combo_store.combo, source)["combo"]
|
|
items = [
|
|
{"로직": str(x.get("로직") or ""), "메모": x.get("메모")}
|
|
for x in items or []
|
|
if isinstance(x, dict)
|
|
]
|
|
|
|
def make(made_name: str, _rows) -> str:
|
|
row = {
|
|
"이름": made_name,
|
|
**{k: was.get(k) for k in ("구분", "상세구분", "단위")},
|
|
"담은로직": items,
|
|
"본뜬키": source,
|
|
"출처": was.get("출처"),
|
|
}
|
|
return str(_m01(combo_store.combo_new, row, _OWNER)["key"])
|
|
|
|
name = name.strip() or str(was.get("이름") or source)
|
|
return _branch(
|
|
column, version, "일위대가", combo_store.FILE, "담은로직", "로직", items, source, make, name
|
|
)
|
|
|
|
|
|
def _material_rows() -> list[dict]:
|
|
return _m01(m01.read, MATERIAL_FILE)[0].get("줄") or []
|
|
|
|
|
|
def material_combo(key: str) -> dict[str, Any]:
|
|
row = next((r for r in _material_rows() if r.get("키") == key), None)
|
|
if row is None:
|
|
raise store.StoreError(404, f"없는 자재 조합 「{key}」")
|
|
return row
|
|
|
|
|
|
def branch_material_combo(
|
|
column: str, source: str, items: list[dict], name: str, version: str = ""
|
|
) -> dict[str, Any]:
|
|
"""자재 조합 갈라 내기 — `source` 가 빈 글이면 이 열의 첫 자재 조합(본뜬키 null)."""
|
|
was = material_combo(source) if source else {}
|
|
items = [
|
|
{k: x.get(k) for k in ("코드", "이름", "규격", "단위")}
|
|
for x in items or []
|
|
if isinstance(x, dict)
|
|
]
|
|
|
|
def make(made_name: str, rows: list[dict]) -> str:
|
|
row = {"이름": made_name, "담은자재": items, "본뜬키": source or None, "소유": _OWNER}
|
|
before = {r.get("키") for r in rows}
|
|
_m01(
|
|
m01.save,
|
|
[
|
|
{
|
|
"file": MATERIAL_FILE,
|
|
"version": _m01(m01.read, MATERIAL_FILE)[1],
|
|
"changes": [{"op": "add", "row": {**row, "비고": None}}],
|
|
}
|
|
],
|
|
)
|
|
return str(next(r["키"] for r in _material_rows() if r.get("키") not in before))
|
|
|
|
heads = next((c for c in store._structure_columns() if c["id"] == column), {})
|
|
default = " ".join(h for h in store._heads(heads) if h) if heads else column
|
|
name = name.strip() or str(was.get("이름") or default or column)
|
|
return _branch(
|
|
column, version, "자재조합", MATERIAL_FILE, "담은자재", "코드", items, source, make, name
|
|
)
|