feat(master_data): 재료 마스터 모양 확정 반영 — 시중물가 시중·조달 두 묶음 · 재료_품셈재료 연결 · 낮은 값과 출처

- 재료_시중물가: 채택 · 견적가 칸 뺌 · 값 = 시중{다섯 칸 + 면수} · 조달{값·출처·기준}
- 관급 줄 가격정보 4,552 → 조달(출처 가격정보 · 기준 = 원천 면수) · 나라장터 온전 짝 54 → 조달 출처 「나라장터:<열쇠>」(값 안 베낌)
- 재료_나라장터자재 머리 「자료」 = 조달 자료
- 재료_자체 → 재료_품셈재료: 값·출처 칸 없음 · 요구절 · 연결(열쇠 5 · 후보 조건 3 · 못 이음 352)
- 엔진(master_material): 연결된 줄의 시중·조달 가운데 낮은 값 · 호표 줄 출처 · 후보 조건은 로직 입력 「자재지역」 — 줄 없으면 「지역 값 없음」 · 규격 여럿이면 「후보 여럿」
- 로직: 재료:자체 → 재료:품셈재료 516줄 · 자재지역 입력 24 로직 · check_master 품셈재료 틀·연결 검사 · M01 prices 출처
- 일괄 시험 계산(울진): 계산됨 1,109 · 값 없음 232 · 후보 여럿 8 · 미공표 2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-19 23:38:09 +09:00
co-authored by Claude Opus 5
parent dd120ce44b
commit 4277906258
49 changed files with 25263 additions and 24933 deletions
+40 -1
View File
@@ -39,6 +39,7 @@ BOOKS = (
"시중물가",
"한국은행환율",
"조달청제비율",
"품셈재료",
"자체",
)
DIVISIONS = {
@@ -57,6 +58,7 @@ ROW_KEYS = {
"요소": ("열쇠", "이름", "", "출처"),
"": ("열쇠", "이름", "기준", "출처", "조건", "값칸", "", ""),
"로직": ("열쇠", "이름", "결과단위", "출처", "소유", "입력", "중간", "끝수"),
"품셈재료": ("열쇠", "이름", "규격", "단위", "요구절", "연결"),
}
@@ -116,6 +118,8 @@ def check_form(name: str, data: dict) -> list[str]:
out.append(f"{name} · 머리 「{head}」 없음")
table = data.get("그룹") in mf.TABLE_GROUPS
kind = "" if table else "로직" if data.get("그룹") == "로직" else "요소"
if data.get("원문") == "품셈재료":
kind = "품셈재료"
rows = data.get("" if table else "")
if not isinstance(rows, list):
return out + [f"{name} · 머리 「{'' if table else ''}」 목록 없음"]
@@ -134,6 +138,8 @@ def check_form(name: str, data: dict) -> list[str]:
out.append(f"{where} · 칸 「{need}」 없음")
if "출처" in row and not _source_ok(row["출처"]):
out.append(f"{where} · 출처 모양 「{row['출처']}")
if kind == "품셈재료":
out += _check_linked(where, row)
if kind == "요소" and isinstance(row.get(""), dict):
for bundle in ("단위", "출처"):
if isinstance(row.get(bundle), dict) and set(row[bundle]) != set(row[""]):
@@ -145,6 +151,39 @@ def check_form(name: str, data: dict) -> list[str]:
return out
def _check_linked(where: str, row: dict) -> list[str]:
"""품셈재료 — 값 칸 없음 · 요구절은 출처 모양 · 연결 = 시중물가 열쇠 · 후보 조건 · null."""
out = [f"{where} · 값 칸을 두지 않음"] if "" in row else []
need = row.get("요구절")
if not (isinstance(need, list) and need and all(_source_ok(x) for x in need)):
out.append(f"{where} · 요구절 모양 「{need}")
link = row.get("연결")
if isinstance(link, dict) and (
not link.get("이름")
or set(link) - {"이름", "규격", "지역"}
or link.get("지역") not in (None, "입력")
):
out.append(f"{where} · 후보 조건 모양 「{link}")
elif link is not None and not isinstance(link, (str, dict)):
out.append(f"{where} · 연결 모양 「{link}")
return out
def check_links(whole: mf.Master) -> list[str]:
"""품셈재료 연결 · 시중물가 조달 연결이 가리키는 줄."""
out = []
for key, row in whole.index.get(("재료", "품셈재료"), {}).items():
link = row.get("연결")
if isinstance(link, str) and link not in whole.index.get(("재료", "시중물가"), {}):
out.append(f"재료:품셈재료:{key} · 연결 「{link}」 이 시중물가에 없음")
nara = whole.index.get(("재료", "나라장터자재"), {})
for key, row in whole.index.get(("재료", "시중물가"), {}).items():
source = str(row[""]["조달"].get("출처") or "")
if source.startswith("나라장터:") and source.split(":", 1)[1] not in nara:
out.append(f"재료:시중물가:{key} · 조달 「{source}」 이 나라장터자재에 없음")
return out
def _check_table(where: str, table: dict) -> list[str]:
out, conds, cols = [], table.get("조건", {}), table.get("값칸", {})
allowed = set(conds) | set(cols) | {"단위", ""} | {c + "원문" for c in cols}
@@ -288,7 +327,7 @@ def check_logics(files: dict[str, dict], whole: mf.Master) -> list[str]:
out += [f"{name} · {x}" for x in found]
graph[f"{data.get('원문')}:{row.get('열쇠')}"] = calls
out += [f"돌고 도는 참조 · {''.join(loop)}" for loop in mf.find_loops(graph)]
return out + duplicate_keys(files)
return out + duplicate_keys(files) + check_links(whole)
def duplicate_keys(files: dict[str, dict]) -> list[str]: