knowledge(마스터): 전력을 재료_유가전력으로 연결 — 자재품목 전력 2줄 뺌

- 품셈재료 「전력」 연결 = {이름: 전력량요금, 계약종별: 입력} — 유가전력 전력 줄을 가리킴
- 전력 쓰는 로직 8 줄에 입력 「전력 계약종별」(주택용 저압 · 일반용(갑)Ⅰ 저압 · 산업용(갑)Ⅰ 저압) — 품셈·계약예규에 계약종별 규정 없어 기본값 두지 않음
- 임시전력(갑)은 주택용 저압 요금 그대로(요금표 6.가) — 따로 고르기에 두지 않음
- master_material: 자재품목 아닌 줄은 「값」 한 칸으로 읽고 계약종별 후보를 고름 · check_master 연결 모양·끊김 검사도 맞춤
- 자재품목 MT000006 · MT000007 삭제 → _키대장.json 「폐기」 · 빌더 DROP 에 더해 다시 만들어도 같음
- _틀.md 재료 절 · 시험 둘 고침(전력 계약종별 · 연결 낮은 값은 시멘트로)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-20 19:11:34 +09:00
co-authored by Claude Opus 5
parent 7b017e61c6
commit 76ee8acb4b
11 changed files with 157 additions and 29 deletions
@@ -27,13 +27,15 @@ from compare_estx import ESTX, ROOT, SLOTS, Estx # noqa: E402
OUT = ROOT / "resources/master_data/재료_자재품목.json"
BASE = SLOTS[0] # Cost1 「가격정보」 — 관급이면 관급 열 · 아니면 비고
MARKET = SLOTS[1:5] # 물가지 넷 · 견적가(Cost6)는 마스터 아님
# 유류 — 값이 비고 글뿐이라 뺌 · 유가는 재료_오피넷유가 · 중유는 받지 않음(대장 「폐기」)
# 유류·전력 — 유가·전기요금은 재료_유가전력(유류는 값이 비고 글뿐 · 중유는 받지 않음 · 대장 「폐기」)
DROP = {
"M0003039885": "휘발유",
"M0003039886": "경유",
"M0004805582": "선박용경유",
"M0003039887": "중유",
"M0003039888": "등유",
"M0005250064": "전력",
"M0005250065": "전력(주택용저압)",
}
@@ -133,9 +135,10 @@ def main():
"비고": [
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
"값 열 다섯 = 물가지 넷(물가자료 · 유통물가 · 물가정보 · 거래가격) · 관급(조달) — 채택값·기본가 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
"유류(휘발유 · 경유 · 선박용경유 · 중유 · 등유)는 값이 비고 글뿐이라 뺌 — 유가는 재료_오피넷유가 · 중유는 받지 않음",
"유류·전력은 뺌 — 유가·전기요금은 재료_유가전력(유류는 값이 비고 글뿐 · 중유는 받지 않음)",
"원문번호(다산소프트 품번) 칸 없음 — 대조는 _키대장.json 에만",
"관급 = 원문 「가격정보」 칸의 조달청 관급 단가(철근 · 레미콘 · 아스콘 · H형강 등) · 기간은 비고 「조달 26.7」",
"조달청에만 있는 품목 380 줄은 관급 열만 채움 — 출처 「자재품목 조달청 …」 · 원문번호는 _키대장.json 에만",
"원문 「가격정보」 칸에 시중 값이 남은 줄은 값 열에 넣지 않고 비고 「가격정보 ○○」 로만 둠 — 출처가 조달인지 물가지인지 원문이 안 가름",
"구분 · 상세구분 = 원문에 대·중분류 칸이 없어 비움(지어내지 않음)",
"면수 = 물가지 값의 원문 면수 · 화면에는 접어 둠",
@@ -300,8 +300,9 @@ def _check_linked(where: str, row: dict) -> list[str]:
link = row.get("연결")
if isinstance(link, dict) and (
not link.get("이름")
or set(link) - {"이름", "규격", "지역"}
or set(link) - {"이름", "규격", "지역", "계약종별"}
or link.get("지역") not in (None, "입력")
or link.get("계약종별") not in (None, "입력")
):
out.append(f"{where} · 후보 조건 모양 「{link}")
elif link is not None and not isinstance(link, (str, dict)):
@@ -314,8 +315,8 @@ def check_links(whole: mf.Master) -> list[str]:
out = []
for key, row in whole.index.get("MP", {}).items():
link = row.get("연결")
if isinstance(link, str) and link not in whole.index.get("MT", {}):
out.append(f"{whole.label(key)} · 연결 「{link}」 이 자재품목에 없음")
if isinstance(link, str) and link not in whole.index.get(link[:2], {}):
out.append(f"{whole.label(key)} · 연결 「{link}」 이 자재품목·유가전력에 없음")
for key, row in whole.index.get("LB", {}).items():
for col in ("준용",):
ref = row.get(col)
@@ -567,6 +567,12 @@ def _check_node(row, node, known: set, master: Master, where: str, out: list, ca
continue
if isinstance(link, dict) and link.get("지역") == "입력" and REGION not in known:
out.append(f"{where} · 「{master.label(ref)}」 는 입력 「{REGION}」 필요")
if (
isinstance(link, dict)
and link.get("계약종별") == "입력"
and TARIFF not in known
):
out.append(f"{where} · 「{master.label(ref)}」 는 입력 「{TARIFF}」 필요")
if kind == "field" and part[1][0] == "ref":
for ref in _expand(row, part[1][1], master) or []:
try:
@@ -678,4 +684,4 @@ def find_loops(graph: dict[str, set]) -> list[list[str]]:
# 품셈재료 단가 — 서로 부르는 모듈이라 끝에서 읽음
from master_material import LINKED, REGION, SLOTS, element # noqa: E402, F401
from master_material import LINKED, REGION, SLOTS, TARIFF, element # noqa: E402, F401
@@ -14,8 +14,10 @@ from decimal import Decimal
LINKED = "MP" # 품셈재료 테이블ID
MARKET = "MT" # 자재품목 테이블ID
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
_UNITS = {"ea": "", "ton": "", "t": "", "l": "", "리터": "", "set": "", "세트": ""}
@@ -35,8 +37,10 @@ def plain_unit(s) -> str:
def _offers(master: mf.Master, key: str) -> list[tuple[Decimal, str]]:
"""자재품목 줄 하나의 (값, 출처) — 값이 있는 열마다 하나 · 출처는 그 열 이름."""
"""줄 하나의 (값, 출처) — 자재품목은 값이 있는 열마다 하나 · 그 밖(유가전력)은 「값」 한 칸."""
row = master.get(key)
if key[:2] != MARKET:
return [(row[""], f"{row['']}.값")] if row.get("") is not None else []
return [(row[slot], f"{row['']}.{slot}") for slot in SLOTS if row.get(slot) is not None]
@@ -59,9 +63,30 @@ 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["연결"]
if TARIFF not in env:
raise mf.FormulaError(f"{master.label(ref)}」 입력 「{TARIFF}」 없음")
want, kind = plain(cond["이름"]), plain(env[TARIFF])
hits = [
key
for key, one in master.index.get(PRICED, {}).items()
if one.get("구분") == "전력"
and plain(one.get("상세구분")) == kind
and plain(one.get("이름")).startswith(want)
and one.get("") is not None
]
if not hits:
raise mf.FormulaError(f"{master.label(ref)}」 계약종별 값 없음 — {env[TARIFF]}")
return hits
def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> 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}」 없음")
region = plain(env.get(REGION, "")) if cond.get("지역") == "입력" else ""