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
@@ -311,7 +311,7 @@ def evaluate(node, env: dict, master: Master, depth: int = 0):
raise FormulaError(f"모르는 이름 「{node[1]}")
return env[node[1]]
if kind == "ref":
value = master.get(fill(node[1], env)).get("")
value = element(master, fill(node[1], env), env)[0]
if value is None:
raise FormulaError(f"{node[1]}」 값 없음 — 관리자가 채울 값")
if isinstance(value, list):
@@ -417,6 +417,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
lines = []
for item in row.get("호표", []):
qty = evaluate(parse(item["수량"]), env, master, depth)
source = None
if item["종류"] == "로직":
bundle = evaluate(parse(item["요소"]), env, master, depth)
price = bundle[""]
@@ -426,15 +427,17 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
else {item["비목"]: qty * price}
)
else:
node = parse(item["요소"])
if node[0] == "field": # 값 묶음 요소의 칸 — 「재료:시중물가:M….채택」
price = master.get(fill(node[1][1], env))[""][node[2]]
else:
price = master.get(fill(item["요소"], env))[""]
ref = fill(item["요소"], env)
try:
price, source = element(master, ref, env)
except FormulaError:
if qty != 0:
raise
price = None
if price is None and qty == 0:
price = Decimal(0) # 안 쓰는 재료 줄 — 값이 비어도 금액 0
if price is None:
raise FormulaError(f"{fill(item['요소'], env)}」 값 없음 — 관리자가 채울 값")
raise FormulaError(f"{ref}」 값 없음 — 관리자가 채울 값")
split = {item["비목"]: qty * price}
for k, v in split.items():
sums[k] += v
@@ -446,6 +449,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
"단가": price,
"금액": qty * price,
"비목": split,
**({"출처": source} if source else {}),
}
)
for extra in row.get("덧줄", []):
@@ -511,9 +515,12 @@ def _check_node(row, node, known: set, master: Master, where: str, out: list, ca
refs = _expand(row, part[1], master) or []
for ref in refs:
try:
master.get(ref)
link = master.get(ref).get("연결")
except FormulaError as e:
out.append(f"{where} · {e}")
continue
if isinstance(link, dict) and link.get("지역") == "입력" and REGION not in known:
out.append(f"{where} · 「{ref}」 는 입력 「{REGION}」 필요")
if kind == "field" and part[1][0] == "ref":
for ref in _expand(row, part[1][1], master) or []:
try:
@@ -621,3 +628,7 @@ def find_loops(graph: dict[str, set]) -> list[list[str]]:
if node not in state:
visit(node, [node])
return loops
# 품셈재료 단가 — 서로 부르는 모듈이라 끝에서 읽음
from master_material import LINKED, REGION, element # noqa: E402, F401