- 표 resources/master_data/자재조합.json → resources/master_template/자재조합.json(git mv) · 키 UM 은 표 안 「다음」 번호 · _키대장 · master_keys 의 UM 뺌 · check_master 틀이 새 자리를 같이 봄
- 서버: M01 /api/m01/matset* 뺌 · M02 저장소 M02_MasterTemplete_Matset.py(목록 · 새로 · 고치기 · 지우기 · 쓰는 열 검사) · 길 GET/POST /api/m02/material-combos · PUT/DELETE /api/m02/material-combos/{키} · 갈라 내기(branchMaterialCombo)도 이 저장소로
- 화면: M01 좌측 「자재 조합」 컨테이너 뺌 · M02 좌측 「자재 조합」 컨테이너(찾기 · 키 맨 앞 목록 · 새로 · 본떠 만들기 · 지우기) · 메인 칸 상세(이름 · 비고 · 본뜬키 · 담은 자재 표 · 자재 더하기 · 빼기 · 저장) · 쓰는 열이 있으면 지우기는 확인 창 없이 알림
- 시험: test_m01_matset → test_m02_matset · test_m02_combo_branch 사본 자리 · 실제 자재 코드
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwJQ5eRrCKf3nH9EFeKvJz
85 lines
3.9 KiB
Python
85 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""자재 조합 — 구조물 열 재료산출이 쓰는 자재 목록 묶음(키 UM · `_틀.md` 11장).
|
|
|
|
자리는 마스터 템플릿 `resources/master_template/자재조합.json`(구조물집계표조합.json 과 같은 결) ·
|
|
키는 표 안 「다음」 번호(`_키대장` 밖). 담은 자재 목록만 지님(수량 · 계산은 재료산출 몫) ·
|
|
갈라 내기(M02) 때 새 줄 · 본뜬키 = 원래 키. 저장 · 화면은 M02(`M02_MasterTemplete_Matset.py`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import master_combo as mcb
|
|
import master_keys as mk
|
|
|
|
GROUP = "자재조합"
|
|
FILE = "자재조합.json"
|
|
TID = "UM"
|
|
OWNERS = mcb.OWNERS
|
|
MAX_이름 = mcb.MAX_이름
|
|
PATH = Path(__file__).resolve().parents[2] / "master_template" / FILE
|
|
COLS = ("키", "이름", "담은자재", "본뜬키", "소유", "비고")
|
|
ITEM_COLS = ("코드", "이름", "규격", "단위")
|
|
|
|
|
|
def items(row: dict) -> list[dict]:
|
|
got = row.get("담은자재")
|
|
return got if isinstance(got, list) else []
|
|
|
|
|
|
def check_form(name: str, data: dict) -> list[str]:
|
|
"""파일 틀 + 줄 — 키 모양 · 열 한 벌 · 이름 · 소유 · 담은 자재(빈 목록 · 모양 · 같은 코드 두 번)."""
|
|
out = [] if data.get("그룹") == GROUP else [f"{name} · 머리 그룹이 「{GROUP}」 아님"]
|
|
rows = data.get("줄")
|
|
if not isinstance(rows, list):
|
|
return out + [f"{name} · 머리 「줄」 목록 없음"]
|
|
after = data.get("다음")
|
|
if not isinstance(after, int) or after < 1:
|
|
out.append(f"{name} · 머리 「다음」 이 1 이상 정수 아님")
|
|
after = None
|
|
seen_names: dict[str, str] = {}
|
|
for row in rows:
|
|
key = str(row.get("키"))
|
|
where = f"{name} · {key} {row.get('이름') or ''}".strip()
|
|
if not mk.KEY.fullmatch(key) or key[:2] != TID:
|
|
out.append(f"{where} · 키 모양이 「{TID}」 + 6자리 아님")
|
|
elif after and int(key[2:]) >= after:
|
|
out.append(f"{where} · 키 번호가 머리 「다음」({after}) 보다 작지 않음")
|
|
if tuple(row) != COLS:
|
|
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(COLS)}」")
|
|
plain = row.get("이름").strip() if isinstance(row.get("이름"), str) else ""
|
|
if not plain or len(plain) > MAX_이름:
|
|
out.append(f"{where} · 「이름」 이 없거나 {MAX_이름}자 넘음")
|
|
elif plain in seen_names:
|
|
out.append(f"{where} · 「이름」 이 같은 조합과 겹침 — {seen_names[plain]}")
|
|
seen_names.setdefault(plain, key)
|
|
if row.get("소유") not in OWNERS:
|
|
out.append(f"{where} · 소유 「{row.get('소유')}」 — {' · '.join(OWNERS)} 아님")
|
|
origin = row.get("본뜬키")
|
|
if origin is not None and not (mk.KEY.fullmatch(str(origin)) and origin[:2] == TID):
|
|
out.append(f"{where} · 본뜬키 모양 「{origin}」")
|
|
got = items(row)
|
|
if not got:
|
|
out.append(f"{where} · 빈 조합 — 담은 자재가 없음")
|
|
codes: set[str] = set()
|
|
for at, one in enumerate(got, start=1):
|
|
if not isinstance(one, dict) or set(one) - set(ITEM_COLS):
|
|
out.append(f"{where} 줄{at} · 담은 자재 줄 모양 「{one}」")
|
|
continue
|
|
code = str(one.get("코드") or "")
|
|
if not mk.KEY.fullmatch(code):
|
|
out.append(f"{where} 줄{at} · 자재 코드 모양 아님 「{code}」")
|
|
elif code in codes:
|
|
out.append(f"{where} 줄{at} · 같은 자재를 두 번 담음 「{code}」")
|
|
codes.add(code)
|
|
return out
|
|
|
|
|
|
def check_file(path: Path = PATH) -> list[str]:
|
|
"""마스터 템플릿 자재 조합 표 — 없으면 한 줄."""
|
|
if not path.is_file():
|
|
return [f"{path.name} · 마스터 템플릿에 없음"]
|
|
return check_form(path.name, json.loads(path.read_text(encoding="utf-8")))
|