refactor(M02): 자재 조합(UM) 지움 — 좌측 컨테이너 · 재료산출 옆 판 · 길(material-combos · 갈라 내기 · 키 적기 자재조합) · 저장소 · 틀 검사 · 복사 길의 UM 줄 · 시스템 층 표 · fixture · 시험

- 자재 고르기 창은 M02_Material_Pick.ts 로 옮겨 남김
- storage 층(회사 · 개인 · 프로젝트) 자재조합.json 은 지우지 않음(읽기만 끊음)
- check_master 틀 검사 · _틀.md 11장 · _화면_계약.md 자재 조합 절 정리

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-04 17:39:16 +09:00
co-authored by Claude Opus 5.5
parent 8fde124df5
commit 54ce67ceb8
51 changed files with 129 additions and 1645 deletions
-10
View File
@@ -298,13 +298,3 @@
- `이름` = 글자 · 빈 이름 안 됨 · 60자까지(`master_combo.MAX_이름`) · 같은 파일 안에서 안 겹침.
- 검사(`check_master.py 조합` · 엔진 `scripts/master_combo.py`) — 없는 로직 키 · 단가산출 로직이 아닌 키 · 같은 로직 두 번 · 빈 조합 · **조합이 조합을 담음**(1단만) · 메모가 글자·`null` 아님 · 이름이 글자 아니거나 없음·너무 김·겹침.
- 「이 로직을 쓰는 조합」 은 로직 줄에 칸으로 박지 않고 물을 때 훑어 찾음(`master_combo.using`) — 조합은 관리자가 그때그때 만드는 것이라 박아 두면 낡음. 소요량 「용도」 와 결은 같되 자리가 다름.
## 11. 자재 조합 파일
마스터 템플릿 `resources/master_template/자재조합.json` 하나(이 폴더 밖 · `구조물집계표조합.json` 과 같은 자리) — 구조물 열 재료산출이 쓰는 **자재 목록 묶음**. 머리 `그룹` · `판` · `비고` · `다음` · `줄`(원문 없음). 검사는 `check_master.py 틀` 이 같이 봄(`master_material_combo.check_file`).
- 열 한 벌 — `키` · `이름` · `담은자재` · `본뜬키` · `소유` · `비고`(차례까지 이대로 · 엔진 `scripts/master_material_combo.py`).
- `키` = `UM` + 6자리 — 표 머리 `다음` 번호(`_키대장` 밖) · 지운 키는 다시 안 씀 · 줄에 `원문번호` 를 두지 않음.
- `담은자재` 줄 = `{코드, 이름, 규격, 단위}` 뿐 · 빈 목록 안 됨 · 같은 코드 두 번 안 됨 · 수량 · 계산은 두지 않음(재료산출 몫).
- `본뜬키` = 갈라 낸 원래 자재 조합 키 · 없으면 `null` · `소유` = `현장` · `공용`.
- 새 줄은 M02 갈라 내기(`POST /api/m02/structures/{열}/material-combo`)가 만듦 — 같은 자재 집합이 있으면 그 키.
-4
View File
@@ -196,10 +196,6 @@
- 못 도는 줄은 통째로 멈추지 않고 그 줄만 `ok` false · `까닭` · `멈춤` 에 한 줄 — 나머지 줄의 합계는 그대로 냄.
- 답 — 200 · 400(틀린 몸 · 이름 없음 · 모르는 소유) · 404(없는 키) · 409(낡은 판본) · 422 `{detail: {errors: […]}}`(검사 걸림 — 없는 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음).
### 자재 조합
M01 에 없음 — 마스터 템플릿 표(`resources/master_template/자재조합.json` · 키 `UM` · `_틀.md` 11장) · 길과 화면은 M02(`/api/m02/material-combos…` · 좌측 「자재 조합」 컨테이너).
## 10. 재료 · 기계 고르기
재료 줄은 키 대신 기본 검색어로 품목을 찾음 — 파일 모양 · 찾기 규칙 · 자동값 차례 · 단위 환산은 `_틀.md` 7장.
@@ -30,7 +30,6 @@ sys.path.insert(0, str(ROOT / "resources/knowledge/original/_pipeline"))
import master_combo as mcb # noqa: E402
import master_formula as mf # noqa: E402
import master_keys as mk # noqa: E402
import master_material_combo as mmc # noqa: E402
# (2) 본문은 따로 모듈 — 부르는 쪽은 `cm.check_body` · `cm.DIVISIONS` 그대로
from check_master_body import DIVISIONS, check_body # noqa: E402, F401
@@ -549,7 +548,6 @@ def main(argv: list[str]) -> int:
report = {}
if mode in ("틀", "전부"):
report["틀"] = [x for name, data in files.items() for x in check_form(name, data)]
report["틀"] += mmc.check_file() # 자재 조합은 마스터 템플릿 표
if mode in ("본문", "전부"):
report["본문"] = check_body(files)
if mode in ("로직", "전부"):
@@ -1,84 +0,0 @@
# -*- 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")))