feat(M02): 조합 갈라 내기 길 · 자재 조합(UM) 표 · 본뜬키 칸 · 분류 더하기 열 복사
- 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
This commit is contained in:
@@ -30,6 +30,7 @@ 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
|
||||
from pum_md_tool import _NUM, md_lines # noqa: E402
|
||||
|
||||
BOOKS = (
|
||||
@@ -249,12 +250,17 @@ def check_combo_form(name: str, data: dict) -> list[str]:
|
||||
out.append(f"{where} · 소유 「{row.get('소유')}」 — {' · '.join(mcb.OWNERS)} 아님")
|
||||
if not _source_ok(row.get("출처")):
|
||||
out.append(f"{where} · 출처 모양 「{row.get('출처')}」")
|
||||
origin = row.get("본뜬키")
|
||||
if origin is not None and not (mk.KEY.fullmatch(str(origin)) and origin[:2] == mcb.TID):
|
||||
out.append(f"{where} · 본뜬키 모양 「{origin}」")
|
||||
return out
|
||||
|
||||
|
||||
def check_form(name: str, data: dict) -> list[str]:
|
||||
if name == mcb.FILE:
|
||||
return check_combo_form(name, data)
|
||||
if name == mmc.FILE:
|
||||
return mmc.check_form(name, data)
|
||||
if name in SPECIAL_FILES:
|
||||
return []
|
||||
m = _FILE.match(name)
|
||||
|
||||
@@ -18,8 +18,8 @@ FILE = "일위대가조합.json"
|
||||
TID = "UA"
|
||||
OWNERS = ("현장", "공용")
|
||||
MAX_이름 = 60 # 조합 이름 최대 글자 수 — 관리자가 짓는 짧은 이름이라 이보다 길면 저장 막음
|
||||
# 줄 열 한 벌 — 차례까지 이대로(`_틀.md` 10장)
|
||||
COLS = ("키", "이름", "구분", "상세구분", "단위", "담은로직", "출처", "소유", "비고")
|
||||
# 줄 열 한 벌 — 차례까지 이대로(`_틀.md` 10장) · 본뜬키 = 갈라 낸 원래 조합 키(없으면 null)
|
||||
COLS = ("키", "이름", "구분", "상세구분", "단위", "담은로직", "본뜬키", "출처", "소유", "비고")
|
||||
ITEM_COLS = ("로직", "메모")
|
||||
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@ TABLES = {
|
||||
("로직", "건설품셈"): "GC",
|
||||
("로직", "자체"): "GX",
|
||||
("일위대가조합", None): "UA",
|
||||
("자재조합", None): "UM",
|
||||
}
|
||||
GROUP_OF = {tid: group for (group, _), tid in TABLES.items()}
|
||||
IDS = tuple(GROUP_OF)
|
||||
# 줄에 원문번호를 두지 않는 테이블 — 자재품목은 번호가 남의 품번이라 이름 몫을 못 하고,
|
||||
# 조합은 원문이 없어 번호 자체가 없음(대조는 대장에만)
|
||||
NO_NUMBER = ("MT", "UA")
|
||||
NO_NUMBER = ("MT", "UA", "UM")
|
||||
|
||||
|
||||
def table_id(group: str, book: str | None) -> str:
|
||||
@@ -181,10 +182,12 @@ ROW_ORDER = {
|
||||
"상세구분",
|
||||
"단위",
|
||||
"담은로직",
|
||||
"본뜬키",
|
||||
"출처",
|
||||
"소유",
|
||||
"비고",
|
||||
),
|
||||
"자재조합": ("키", "이름", "담은자재", "본뜬키", "소유", "비고"),
|
||||
}
|
||||
OLD_CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈)_(\D*)(\d+)장\.json$")
|
||||
CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈|자체)_(\d\d)장_.+\.json$")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""자재 조합 — 구조물 열 재료산출이 쓰는 자재 목록 묶음(키 UM · `_틀.md` 11장).
|
||||
|
||||
담은 자재 목록만 지님(수량 · 계산은 재료산출 몫). 일위대가 조합(`master_combo`)과 같은 결 —
|
||||
갈라 내기(M02) 때 새 줄 · 본뜬키 = 원래 키.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import master_combo as mcb
|
||||
import master_keys as mk
|
||||
|
||||
GROUP = "자재조합"
|
||||
FILE = "자재조합.json"
|
||||
TID = "UM"
|
||||
OWNERS = mcb.OWNERS
|
||||
MAX_이름 = mcb.MAX_이름
|
||||
COLS = mk.ROW_ORDER[GROUP] # 키 · 이름 · 담은자재 · 본뜬키 · 소유 · 비고
|
||||
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} · 머리 「줄」 목록 없음"]
|
||||
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자리 아님")
|
||||
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
|
||||
Reference in New Issue
Block a user