feat(M01): PLAN 7-4b 뒤 절반 ③ — 고르기 조건에 지역·계약종별 자리를 넓혀 모래·자갈·전력 17줄을 품셈재료 간접 없이 품

master_material.py — PICK_KEYS 에 「지역」·「계약종별」 더함. `_region`·`_tariff`
두 함수를 품셈재료 행이 아니라 조건 묶음 자체를 받게 고쳐 옛길(품셈재료 「연결」
간접)과 새길(호표 「요소」 에 조건을 바로 박음) 이 같은 코드를 씀(sub3 가 분석해
둔 방식 그대로 — 이름 뿌리 endswith·단위 같음·지역 이름 포함 매칭). check_pick —
지역·계약종별 조건은 구분 없이 이름만 확인 · 값이 "입력" 아닌 채 섞이면 틀로 잡음.

build_로직_재료_고르기.py — 품셈재료 연결이 {이름·지역:입력} 이나 {이름·계약종별:입력}
인 줄(모래 MP000004·자갈 MP000006·전력 MP000358)도 그 조건 묶음을 그대로 호표에
박도록 넓힘 — 다시 돌려 로직 4개 파일 17줄 반영(모래 8·자갈 1·전력 8). 전력은
지시대로 재료_유가전력.json(MO 테이블)을 그대로 가리킴 — 자재품목은 안 거침.

test_masterdata_check.py — PICK_KEYS 늘어난 만큼 cond_text 글자 열쇠 시험 고침 ·
지역 값이 "입력" 아닌 조건은 틀로 잡히는 것 확인.
test_m01_api.py — 전력 계약종별 시험을 mf.element 직접 호출로 바꿈(화면 단가
미리보기 `_pick_brief`(M01_MasterData_Store.py, 담당 밖)는 구분·상세구분 자리로만
찾아 지역·계약종별 조건은 아직 못 다뤄 None — 아는 구멍으로 남김 · 계산 자체는
정상 확인).

typecheck 통과 · 관련 시험 108 통과 · 로직 1354개 전수 대조 금액 달라짐 0·새 멈춤 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-22 13:53:19 +09:00
co-authored by Claude Sonnet 5
parent 597bb35458
commit 3e3bb8ef09
8 changed files with 81 additions and 45 deletions
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
"""로직 호표의 재료 줄을 「고르기 조건」 으로 바꿈 — 요소 「MP……」·「MT……」 를 조건 묶음으로.
· 바탕
· 바탕
① 재료_품셈재료.json 의 「고르기」 칸 (`scripts/build_품셈재료_고르기.py` 가 채움)
② 자재품목 줄을 바로 가리키는 「MT……」 — 그 줄의 구분·상세구분·규격 + 대표 = 그 줄
③ 연결이 자재품목 키 하나인 품셈재료 「MP……」 — 그 자재품목 줄로 ② 와 같게
④ 연결이 지역·계약종별 조건 묶음(모래 · 자갈 · 전력)인 품셈재료 — 그 묶음을 호표에
그대로 박음(PLAN 7-4b 뒤 절반 · `master_material._region`·`_tariff` 가 품셈재료
간접 없이 바로 풂)
②③ 은 대표 줄이 옛 요소와 같은 줄이라 금액이 그대로임(고르기 화면만 열림)
· 고르기 칸이 비고 연결도 지역·계약종별을 계산 때 푸는 줄(모래 · 자갈 · 전력)은 옛 방식 그대로 둠
· 줄 텍스트의 「"요소": "MP……"」 한 줄만 바꿈 — 나머지 글자·차례 그대로
· 다시 돌려도 같은 결과(이미 바뀐 줄은 건드릴 것이 없음)
@@ -38,12 +40,18 @@ def _cond_of(row: dict) -> dict | None:
def _picks() -> dict[str, dict]:
"""요소 키 → 고르기 조건 — 품셈재료 고르기 칸 · 자재품목 줄 · 연결이 키 하나인 품셈재료."""
"""요소 키 → 고르기 조건 — 품셈재료 고르기 칸 · 자재품목 줄 · 연결이 키 하나인 품셈재료 ·
연결이 지역·계약종별 조건 묶음인 품셈재료(그 묶음을 그대로 호표에 박음 — PLAN 7-4b 뒤 절반)."""
market = {str(r[""]): r for r in json.loads(MARKET.read_text(encoding="utf-8"))[""]}
picks = {key: cond for key, row in market.items() if (cond := _cond_of(row))}
for row in json.loads(SRC.read_text(encoding="utf-8"))[""]:
link = row.get("연결")
cond = row.get("고르기") or (picks.get(link) if isinstance(link, str) else None)
if isinstance(link, str):
cond = row.get("고르기") or picks.get(link)
elif isinstance(link, dict) and (link.get("지역") == "입력" or link.get("계약종별") == "입력"):
cond = link
else:
cond = row.get("고르기")
if cond:
picks[str(row[""])] = cond
return picks
@@ -16,7 +16,7 @@ LINKED = "MP" # 품셈재료 테이블ID
MARKET = "MT" # 자재품목 테이블ID
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
PICK_KEYS = ("구분", "상세구분", "이름", "규격", "대표") # 재료 고르기 조건 칸 (`_틀.md` 7장)
PICK_KEYS = ("구분", "상세구분", "이름", "규격", "대표", "지역", "계약종별") # 재료 고르기 조건 칸 (`_틀.md` 7장)
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
_UNITS = {"ea": "", "ton": "", "t": "", "l": "", "리터": "", "set": "", "세트": ""}
@@ -64,11 +64,11 @@ def _market_index(master: mf.Master) -> list[tuple[str, str, str, str, str]]:
return master._market
def _tariff(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
"""후보 조건{이름 · 계약종별: 입력} — 유가전력 전력 줄 가운데 그 계약종별·이름으로 시작하는 줄."""
cond = row["연결"]
def _tariff(master: mf.Master, label_ref, cond: dict, env: dict) -> list[str]:
"""후보 조건{이름 · 계약종별: 입력} — 유가전력 전력 줄 가운데 그 계약종별·이름으로 시작하는 줄.
`label_ref` 는 알림 글에 쓸 자리(호표 줄의 고르기 조건 · 또는 품셈재료 옛 키)."""
if TARIFF not in env:
raise mf.FormulaError(f"{master.label(ref)}」 입력 「{TARIFF}」 없음")
raise mf.FormulaError(f"{master.label(label_ref)}」 입력 「{TARIFF}」 없음")
want, kind = plain(cond["이름"]), plain(env[TARIFF])
hits = [
key
@@ -79,29 +79,26 @@ def _tariff(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
and one.get("") is not None
]
if not hits:
raise mf.FormulaError(f"{master.label(ref)}」 계약종별 값 없음 — {env[TARIFF]}")
raise mf.FormulaError(f"{master.label(label_ref)}」 계약종별 값 없음 — {env[TARIFF]}")
return hits
def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
def _region(master: mf.Master, label_ref, cond: dict, env: dict, unit) -> list[str]:
"""후보 조건{이름 · 규격 · 지역: 입력} — 뿌리가 이름으로 끝나고 이름에 지역 글자 · 단위 같음."""
cond = row["연결"]
if cond.get("계약종별") == "입력":
return _tariff(master, ref, row, env)
if cond.get("지역") == "입력" and REGION not in env:
raise mf.FormulaError(f"{master.label(ref)}」 입력 「{REGION}」 없음")
raise mf.FormulaError(f"{master.label(label_ref)}」 입력 「{REGION}」 없음")
region = plain(env.get(REGION, "")) if cond.get("지역") == "입력" else ""
want, spec, unit = plain(cond["이름"]), plain_spec(cond.get("규격")), plain_unit(row["단위"])
want, spec, unit_p = plain(cond["이름"]), plain_spec(cond.get("규격")), plain_unit(unit)
hits = [
(s, key)
for root, name, s, u, key in _market_index(master)
if root.endswith(want) and region in name and u == unit and (not spec or s == spec)
if root.endswith(want) and region in name and u == unit_p and (not spec or s == spec)
]
hits = [(s, key) for s, key in hits if _offers(master, key)]
if not hits:
raise mf.FormulaError(f"{master.label(ref)}」 지역 값 없음 — {env.get(REGION)}")
raise mf.FormulaError(f"{master.label(label_ref)}」 지역 값 없음 — {env.get(REGION)}")
if len({s for s, _ in hits}) > 1:
raise mf.FormulaError(f"{master.label(ref)}」 후보 여럿 — 규격 선택 필요 ({len(hits)}줄)")
raise mf.FormulaError(f"{master.label(label_ref)}」 후보 여럿 — 규격 선택 필요 ({len(hits)}줄)")
return [key for _, key in hits]
@@ -183,8 +180,18 @@ def check_unit(master: mf.Master, where: str, cond: dict, unit: str | None) -> l
def check_pick(master: mf.Master, where: str, cond: dict, unit: str | None = None) -> list[str]:
"""고르기 조건 검사 — 칸 이름 · 구분·상세구분이 자재품목에 있는지 · 대표 줄 · 후보 0.
값에 `{입력}` 이 든 칸(관경 등)은 계산 때 채워지므로 검사에서 뺌."""
if set(cond) - set(PICK_KEYS) or not cond.get("구분"):
값에 `{입력}` 이 든 칸(관경 등)은 계산 때 채워지므로 검사에서 뺌.
지역·계약종별 조건은 구분 없이 이름 하나로 자재품목·유가전력을 넓게 찾음(_region · _tariff)
— 여기서는 이름 칸만 있는지 봄(후보 0 은 계산 때 판단)."""
if set(cond) - set(PICK_KEYS):
return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — {' · '.join(PICK_KEYS)}"]
if ("지역" in cond and cond["지역"] != "입력") or (
"계약종별" in cond and cond["계약종별"] != "입력"
):
return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — 지역·계약종별은 늘 「입력」"]
if cond.get("지역") == "입력" or cond.get("계약종별") == "입력":
return [] if cond.get("이름") else [f"{where} · 재료 고르기 조건 「{cond}」 — 이름 없음"]
if not cond.get("구분"):
return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — {' · '.join(PICK_KEYS)}"]
cond = static(cond)
if not cond.get("구분"): # 구분 자체가 `{입력}` — 계산 때 채워져 여기서 좁힐 것이 없음
@@ -207,7 +214,17 @@ def element(
master: mf.Master, ref, env: dict, unit: str | None = None
) -> tuple[object, str | None]:
"""요소 값과 출처 — 자재품목은 제 줄, 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값`.
`ref` 가 고르기 조건 묶음이면 대표 줄(없으면 단위 같은 조건 안 첫 줄)로 풂."""
`ref` 가 고르기 조건 묶음이면 대표 줄(없으면 단위 같은 조건 안 첫 줄)로 풂.
조건에 지역·계약종별 이 「입력」 이면 구분·상세구분 없이 이름 하나로 넓게 찾음(_region · _tariff)
— 호표 줄에 바로 써도 품셈재료 간접 없이 풀림(PLAN 7-4b 뒤 절반)."""
if isinstance(ref, dict) and (ref.get("계약종별") == "입력" or ref.get("지역") == "입력"):
keys = (
_tariff(master, ref, ref, env)
if ref.get("계약종별") == "입력"
else _region(master, ref, ref, env, unit)
)
offers = [x for key in keys for x in _offers(master, key)]
return min(offers, key=lambda x: x[0]) if offers else (None, None)
if isinstance(ref, dict):
ref = pick(master, ref, env, unit)
row = master.get(ref)
@@ -218,7 +235,12 @@ def element(
link = row.get("연결")
if not link:
return None, None
keys = [link] if isinstance(link, str) else _candidates(master, ref, row, env)
if isinstance(link, str):
keys = [link]
elif link.get("계약종별") == "입력":
keys = _tariff(master, ref, link, env)
else:
keys = _region(master, ref, link, env, row["단위"])
offers = [x for key in keys for x in _offers(master, key)]
return min(offers, key=lambda x: x[0]) if offers else (None, None)