knowledge(마스터): 재료 열 한 벌 정리 — 시중물가 → 자재품목 · 값 다섯 열 · 유가·환율도 같은 열
- 재료_시중물가.json → 재료_자재품목.json(테이블ID MT · 키 그대로) · 모든 줄이 같은 열 한 벌(키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수) - 값 묶음(시중·조달)을 풀어 다섯 값을 열로 · 관급 = 조달 값(4,552 줄 · 기간은 비고 「조달 26.7」) · 원문 가격정보만 남은 72 줄은 값 열에 안 넣고 비고 「가격정보 ○○」 · 구분·상세구분은 원문에 분류가 없어 비움 - 재료_오피넷유가 · 환율_한국은행환율도 열 한 벌(기준일 → 규격 · 유가 구분 = 전국평균/시도별 · 상세구분 = 지역) · 상품코드·지역코드 삭제 - 엔진 단가(연결된 줄의 낮은 값 · 출처 = 「<키>.<열>」) · check_master 열 검사 · M01 고르기(procure 걷어냄) · 빌더 · _틀.md · 화면 계약 · 시험 같이 고침 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
"""상용 적산 자료(ESTX 2026-07) 자재 → 재료_시중물가.json — 개발용 · 배포 전 판권 구매 뒤 갱신.
|
||||
|
||||
ESTX 자재 표 = StuffcDat(칸 뜻: 다산소프트/ESTX_값사전_자재노무기계.md).
|
||||
값 = 시중{가격정보·물가자료·유통물가·물가정보·거래가격 + 면수} · 조달{값 · 출처 · 기준}.
|
||||
Cost1..5 = 시중 다섯 · Page1..5 = 면수. TC(채택) · Cost6(견적가)는 마스터 아님 — 뺌.
|
||||
품명에 「관급」 인 줄의 Cost1 = 조달청 관급 단가 — 조달 묶음으로 옮김(기준 = Page1 「조달 26.7」 · 「~26.5.26」).
|
||||
나라장터 시설공통자재와 이름·규격·단위가 온전히 같고 서로 하나뿐인 줄은 조달 출처 「나라장터:<열쇠>」 — 값은 베끼지 않음.
|
||||
0 · 미저장 = 값 없음(null). 이름·규격은 원문 그대로(띄어쓰기·오자 포함). 코드 없는 빈 줄은 뺌.
|
||||
ESTX 에 대·중분류 칸이 없어 분류는 두지 않음.
|
||||
|
||||
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_재료_시중물가.py
|
||||
쓴 뒤 원본과 대조.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
from compare_estx import ESTX, ROOT, SLOTS, Estx, bare_spec, base_name # noqa: E402
|
||||
|
||||
OUT = ROOT / "resources/master_data/재료_시중물가.json"
|
||||
NARA = ROOT / "resources/master_data/재료_나라장터자재.json"
|
||||
MARKET = SLOTS[:5] # 견적가(Cost6)는 마스터 아님
|
||||
_NUM = re.compile(r"\d+(?:\.\d+)?")
|
||||
_UNIT = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
|
||||
|
||||
def num(v):
|
||||
if not v:
|
||||
return None
|
||||
return int(v) if float(v).is_integer() else v
|
||||
|
||||
|
||||
def _base(s) -> str:
|
||||
s = unicodedata.normalize("NFKC", str(s or "")).lower()
|
||||
return re.sub(r"\s+", "", re.sub(r"[φΦ∅⌀]", "ø", s))
|
||||
|
||||
|
||||
def _spec(s) -> str:
|
||||
return _base(s).replace("×", "x").replace("*", "x")
|
||||
|
||||
|
||||
def _unit(s) -> str:
|
||||
s = _base(s).replace("ℓ", "l")
|
||||
return _UNIT.get(s, s)
|
||||
|
||||
|
||||
def _nara_spec(n: dict) -> str:
|
||||
"""나라장터 규격 = 「품명, 규격」 — 앞 품명을 뗌."""
|
||||
head, _, tail = n["규격"].partition(",")
|
||||
return tail if tail and _base(head) == _base(n["이름"]) else n["규격"]
|
||||
|
||||
|
||||
def nara_links(rows: list[dict], nara: list[dict]) -> dict[str, str]:
|
||||
"""시중물가 열쇠 → 나라장터 열쇠 — 품명 뿌리 · 규격 수 모음이 맞는 짝 가운데
|
||||
이름·규격·단위가 온전히 같고 서로 하나뿐인 것만(관급 줄 · 애매한 것은 사람이 화면에서)."""
|
||||
by = collections.defaultdict(list)
|
||||
for e in rows:
|
||||
if e.get("Code") and "관급" not in e.get("Name", ""):
|
||||
by[base_name(e.get("Name"))].append(e)
|
||||
pairs = []
|
||||
for n in nara:
|
||||
if not n.get("값"):
|
||||
continue
|
||||
nums = sorted(_NUM.findall(bare_spec(n["이름"], n["규격"])))
|
||||
for e in by.get(base_name(n["이름"]), []):
|
||||
if (
|
||||
nums == sorted(_NUM.findall(bare_spec(e.get("Name"), e.get("Size"))))
|
||||
and _base(n["이름"]) == _base(e.get("Name"))
|
||||
and _spec(_nara_spec(n)) == _spec(e.get("Size"))
|
||||
and _unit(n["단위"]) == _unit(e.get("Unite"))
|
||||
):
|
||||
pairs.append((n["열쇠"], e["Code"]))
|
||||
cn = collections.Counter(n for n, _ in pairs)
|
||||
ce = collections.Counter(e for _, e in pairs)
|
||||
return {e: n for n, e in pairs if cn[n] == 1 and ce[e] == 1}
|
||||
|
||||
|
||||
def build(rows: list[dict], links: dict[str, str]) -> list[dict]:
|
||||
out = []
|
||||
for r in sorted(rows, key=lambda r: r["Index"]):
|
||||
code = r.get("Code")
|
||||
if not code:
|
||||
continue
|
||||
market = {slot: num(r.get(f"Cost{i}")) for i, slot in enumerate(MARKET, 1)}
|
||||
market["면수"] = {slot: r.get(f"Page{i}") or None for i, slot in enumerate(MARKET, 1)}
|
||||
supply = {"값": None, "출처": None, "기준": None}
|
||||
if "관급" in r.get("Name", "") and market["가격정보"] is not None:
|
||||
supply = {
|
||||
"값": market["가격정보"],
|
||||
"출처": "가격정보",
|
||||
"기준": market["면수"]["가격정보"],
|
||||
}
|
||||
market["가격정보"] = market["면수"]["가격정보"] = None
|
||||
elif code in links:
|
||||
supply["출처"] = f"나라장터:{links[code]}"
|
||||
out.append(
|
||||
{
|
||||
"열쇠": code,
|
||||
"이름": r.get("Name", ""),
|
||||
"규격": r.get("Size", ""),
|
||||
"단위": r.get("Unite", ""),
|
||||
"값": {"시중": market, "조달": supply},
|
||||
"출처": f"시중물가 {code}",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def check(rows: list[dict]) -> None:
|
||||
"""원본 대조 — 줄 수 · 출처별 값 합(가격정보 = 시중 + 조달) · 표본 쉰 줄."""
|
||||
data = json.loads(OUT.read_text(encoding="utf-8"))
|
||||
got = {r["열쇠"]: r for r in data["줄"]}
|
||||
raw = {r["Code"]: r for r in rows if r.get("Code")}
|
||||
assert len(got) == len(data["줄"]) == len(raw), (len(got), len(raw))
|
||||
print(
|
||||
f"줄 {len(got)} = 원본 코드 있는 줄 {len(raw)} (코드 없는 빈 줄 {len(rows) - len(raw)} 뺌)"
|
||||
)
|
||||
for i, slot in enumerate(MARKET, 1):
|
||||
a = math.fsum(r.get(f"Cost{i}") or 0 for r in raw.values())
|
||||
b = math.fsum(r["값"]["시중"][slot] or 0 for r in got.values())
|
||||
if slot == "가격정보":
|
||||
b += math.fsum(r["값"]["조달"]["값"] or 0 for r in got.values())
|
||||
n = sum(1 for r in got.values() if r["값"]["시중"][slot] is not None)
|
||||
assert abs(a - b) < 1e-3, (slot, a, b)
|
||||
print(f" {slot}: 시중 값 {n}줄 · 합 {b:,.1f} = 원본 {a:,.1f}")
|
||||
supply = collections.Counter(
|
||||
(r["값"]["조달"]["출처"] or "없음").split(":")[0] for r in got.values()
|
||||
)
|
||||
print(f" 조달: {dict(supply)}")
|
||||
random.seed(20260719)
|
||||
for code in random.sample(sorted(raw), 50):
|
||||
r, g = raw[code], got[code]
|
||||
assert (g["이름"], g["규격"], g["단위"]) == (
|
||||
r.get("Name", ""),
|
||||
r.get("Size", ""),
|
||||
r.get("Unite", ""),
|
||||
)
|
||||
moved = g["값"]["조달"]["출처"] == "가격정보"
|
||||
for i, slot in enumerate(MARKET, 1):
|
||||
have = g["값"]["조달"]["값"] if moved and i == 1 else g["값"]["시중"][slot]
|
||||
page = g["값"]["조달"]["기준"] if moved and i == 1 else g["값"]["시중"]["면수"][slot]
|
||||
assert (have or 0) == (r.get(f"Cost{i}") or 0), (code, slot)
|
||||
assert page == (r.get(f"Page{i}") or None), (code, slot)
|
||||
print(" 표본 50줄 이름·규격·단위·값 5·면수 5 같음")
|
||||
|
||||
|
||||
def main():
|
||||
rows = Estx().table("StuffcDat")
|
||||
head = {
|
||||
"그룹": "재료",
|
||||
"원문": "시중물가",
|
||||
"판": "2026-07",
|
||||
"원천": [
|
||||
{
|
||||
"경로": ESTX.relative_to(ROOT).as_posix(),
|
||||
"해시": hashlib.sha256(ESTX.read_bytes()).hexdigest(),
|
||||
"역할": "primary",
|
||||
}
|
||||
],
|
||||
"비고": [
|
||||
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
|
||||
"값 = 시중(물가지 다섯 · 면수) · 조달(값 · 출처 · 기준) — 기본가·채택값 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<키>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)",
|
||||
],
|
||||
}
|
||||
nara = mk.old_view(NARA.name)["줄"]
|
||||
dump_master(OUT, head, build(rows, nara_links(rows, nara)))
|
||||
check(rows)
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 나라장터 연결 → 키
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,139 @@
|
||||
"""상용 적산 자료(ESTX 2026-07) 자재 → 재료_자재품목.json — 개발용 · 배포 전 판권 구매 뒤 갱신.
|
||||
|
||||
ESTX 자재 표 = StuffcDat(칸 뜻: 다산소프트/ESTX_값사전_자재노무기계.md).
|
||||
열 한 벌 = 키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수.
|
||||
Cost2..5 = 물가지 넷 · Page2..5 = 면수. TC(채택) · Cost6(견적가)는 마스터 아님 — 뺌.
|
||||
품명에 「관급」 인 줄의 Cost1(가격정보) = 조달청 관급 단가 — 「관급」 열 · 기간은 비고 「조달 26.7」.
|
||||
그 밖의 줄에 남은 Cost1 은 출처가 조달인지 물가지인지 원문이 안 가름 — 값 열에 넣지 않고 비고 「가격정보 ○○」 로만.
|
||||
0 · 미저장 = 값 없음(null). 이름·규격은 원문 그대로(띄어쓰기·오자 포함). 코드 없는 빈 줄은 뺌.
|
||||
ESTX 에 대·중분류 칸이 없어 구분·상세구분은 비움(지어내지 않음).
|
||||
|
||||
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_재료_자재품목.py
|
||||
쓴 뒤 원본과 대조.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
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)는 마스터 아님
|
||||
|
||||
|
||||
def num(v):
|
||||
if not v:
|
||||
return None
|
||||
return int(v) if float(v).is_integer() else v
|
||||
|
||||
|
||||
def note(base, page, guard: str | None) -> str | None:
|
||||
"""비고 — 관급이면 「조달 <기간>」 · 남은 가격정보는 「가격정보 <값> · <면수>」."""
|
||||
if guard is not None:
|
||||
return f"조달 {str(guard).removeprefix('조달 ')}".strip()
|
||||
if base is None:
|
||||
return None
|
||||
return " · ".join(x for x in (f"가격정보 {base:,}", page) if x)
|
||||
|
||||
|
||||
def build(rows: list[dict]) -> list[dict]:
|
||||
out = []
|
||||
for r in sorted(rows, key=lambda r: r["Index"]):
|
||||
code = r.get("Code")
|
||||
if not code:
|
||||
continue
|
||||
base, page = num(r.get("Cost1")), r.get("Page1") or None
|
||||
public = "관급" in r.get("Name", "") and base is not None
|
||||
면수 = {slot: r.get(f"Page{i}") or None for i, slot in enumerate(MARKET, 2)}
|
||||
out.append(
|
||||
{
|
||||
"열쇠": code,
|
||||
"구분": None,
|
||||
"상세구분": None,
|
||||
"이름": r.get("Name", ""),
|
||||
"규격": r.get("Size", ""),
|
||||
"단위": r.get("Unite", ""),
|
||||
**{slot: num(r.get(f"Cost{i}")) for i, slot in enumerate(MARKET, 2)},
|
||||
"관급": base if public else None,
|
||||
"출처": f"자재품목 {code}",
|
||||
"비고": note(base, page, page if public else None),
|
||||
"면수": 면수 if any(면수.values()) else None,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def check(rows: list[dict]) -> None:
|
||||
"""원본 대조 — 줄 수 · 값 열마다 합 · 관급·가격정보 가름 · 표본 쉰 줄."""
|
||||
data = json.loads(OUT.read_text(encoding="utf-8"))
|
||||
got = {r["열쇠"]: r for r in data["줄"]}
|
||||
raw = {r["Code"]: r for r in rows if r.get("Code")}
|
||||
assert len(got) == len(data["줄"]) == len(raw), (len(got), len(raw))
|
||||
print(
|
||||
f"줄 {len(got)} = 원본 코드 있는 줄 {len(raw)} (코드 없는 빈 줄 {len(rows) - len(raw)} 뺌)"
|
||||
)
|
||||
for i, slot in enumerate((*MARKET, "관급"), 2):
|
||||
a = math.fsum(r.get(f"Cost{i}") or 0 for r in raw.values())
|
||||
if slot == "관급": # Cost1 가운데 관급 줄만
|
||||
a = math.fsum(r.get("Cost1") or 0 for r in raw.values() if "관급" in r.get("Name", ""))
|
||||
b = math.fsum(r[slot] or 0 for r in got.values())
|
||||
n = sum(1 for r in got.values() if r[slot] is not None)
|
||||
assert abs(a - b) < 1e-3, (slot, a, b)
|
||||
print(f" {slot}: 값 {n}줄 · 합 {b:,.1f} = 원본 {a:,.1f}")
|
||||
left = [r for r in got.values() if str(r["비고"] or "").startswith("가격정보")]
|
||||
print(
|
||||
f" 비고에만 남긴 가격정보 {len(left)}줄 · 관급 기간 비고 {sum(1 for r in got.values() if str(r['비고'] or '').startswith('조달'))}줄"
|
||||
)
|
||||
random.seed(20260719)
|
||||
for code in random.sample(sorted(raw), 50):
|
||||
r, g = raw[code], got[code]
|
||||
assert (g["이름"], g["규격"], g["단위"]) == (
|
||||
r.get("Name", ""),
|
||||
r.get("Size", ""),
|
||||
r.get("Unite", ""),
|
||||
)
|
||||
public = "관급" in r.get("Name", "")
|
||||
assert (g["관급"] or 0) == ((r.get("Cost1") or 0) if public else 0), code
|
||||
for i, slot in enumerate(MARKET, 2):
|
||||
assert (g[slot] or 0) == (r.get(f"Cost{i}") or 0), (code, slot)
|
||||
assert (g["면수"] or {}).get(slot) == (r.get(f"Page{i}") or None), (code, slot)
|
||||
print(" 표본 50줄 이름·규격·단위·값 5·면수 4 같음")
|
||||
|
||||
|
||||
def main():
|
||||
rows = Estx().table("StuffcDat")
|
||||
head = {
|
||||
"그룹": "재료",
|
||||
"원문": "자재품목",
|
||||
"판": "2026-07",
|
||||
"원천": [
|
||||
{
|
||||
"경로": ESTX.relative_to(ROOT).as_posix(),
|
||||
"해시": hashlib.sha256(ESTX.read_bytes()).hexdigest(),
|
||||
"역할": "primary",
|
||||
}
|
||||
],
|
||||
"비고": [
|
||||
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
|
||||
"값 열 다섯 = 물가지 넷(물가자료 · 유통물가 · 물가정보 · 거래가격) · 관급(조달) — 채택값·기본가 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
|
||||
"관급 = 원문 「가격정보」 칸의 조달청 관급 단가(철근 · 레미콘 · 아스콘 · H형강 등) · 기간은 비고 「조달 26.7」",
|
||||
"원문 「가격정보」 칸에 시중 값이 남은 72 줄은 값 열에 넣지 않고 비고 「가격정보 ○○」 로만 둠 — 출처가 조달인지 물가지인지 원문이 안 가름",
|
||||
"구분 · 상세구분 = 원문에 대·중분류 칸이 없어 비움(지어내지 않음)",
|
||||
"면수 = 물가지 값의 원문 면수 · 화면에는 접어 둠",
|
||||
],
|
||||
}
|
||||
dump_master(OUT, head, build(rows))
|
||||
check(rows)
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -43,7 +43,7 @@ BOOKS = (
|
||||
"준용대상",
|
||||
"나라장터자재",
|
||||
"오피넷유가",
|
||||
"시중물가",
|
||||
"자재품목",
|
||||
"한국은행환율",
|
||||
"조달청제비율",
|
||||
"품셈재료",
|
||||
@@ -89,6 +89,39 @@ LABOR_COLS = (
|
||||
"준용",
|
||||
)
|
||||
LABOR_STATUS = ("공표", "산정", "미공표", "미확보")
|
||||
# 원문별 열 한 벌 — 모든 줄이 이 칸을 이 차례로 가짐(없으면 null)
|
||||
COLS = {
|
||||
"자재품목": (
|
||||
"키",
|
||||
"원문번호",
|
||||
"구분",
|
||||
"상세구분",
|
||||
"이름",
|
||||
"규격",
|
||||
"단위",
|
||||
"물가자료",
|
||||
"유통물가",
|
||||
"물가정보",
|
||||
"거래가격",
|
||||
"관급",
|
||||
"출처",
|
||||
"비고",
|
||||
"면수",
|
||||
),
|
||||
"오피넷유가": (
|
||||
"키",
|
||||
"원문번호",
|
||||
"구분",
|
||||
"상세구분",
|
||||
"이름",
|
||||
"규격",
|
||||
"단위",
|
||||
"값",
|
||||
"출처",
|
||||
"비고",
|
||||
),
|
||||
}
|
||||
COLS["한국은행환율"] = COLS["오피넷유가"]
|
||||
|
||||
|
||||
def load(names: list[str] | None = None, folder: Path = MASTER) -> dict[str, dict]:
|
||||
@@ -181,13 +214,16 @@ def check_form(name: str, data: dict) -> list[str]:
|
||||
out.append(f"{where} · {merged} 「{row.get(merged)}」 이 머리에 없음")
|
||||
if division and not number.startswith(division + " "):
|
||||
out.append(f"{where} · 원문번호 앞에 부문 「{division}」 없음")
|
||||
for need in ROW_KEYS[kind]:
|
||||
cols = COLS.get(data.get("원문"))
|
||||
for need in cols or ROW_KEYS[kind]:
|
||||
if need not in row:
|
||||
out.append(f"{where} · 칸 「{need}」 없음")
|
||||
if "출처" in row and not _source_ok(row["출처"]):
|
||||
out.append(f"{where} · 출처 모양 「{row['출처']}」")
|
||||
if data.get("그룹") == "인력":
|
||||
out += _check_labor(where, row)
|
||||
if cols and tuple(row) != cols:
|
||||
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(cols)}」")
|
||||
if kind == "품셈재료":
|
||||
out += _check_linked(where, row)
|
||||
if kind == "요소" and isinstance(row.get("값"), dict):
|
||||
@@ -214,7 +250,7 @@ def _check_labor(where: str, row: dict) -> list[str]:
|
||||
|
||||
|
||||
def _check_linked(where: str, row: dict) -> list[str]:
|
||||
"""품셈재료 — 값 칸 없음 · 요구절은 출처 모양 · 연결 = 시중물가 키 · 후보 조건 · null."""
|
||||
"""품셈재료 — 값 칸 없음 · 요구절은 출처 모양 · 연결 = 자재품목 키 · 후보 조건 · 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)):
|
||||
@@ -232,22 +268,17 @@ def _check_linked(where: str, row: dict) -> list[str]:
|
||||
|
||||
|
||||
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}」 이 시중물가에 없음")
|
||||
out.append(f"{whole.label(key)} · 연결 「{link}」 이 자재품목에 없음")
|
||||
for key, row in whole.index.get("LB", {}).items():
|
||||
for col in ("준용",):
|
||||
ref = row.get(col)
|
||||
if ref not in (None, "") and str(ref) not in whole.index.get("LB", {}):
|
||||
out.append(f"{whole.label(key)} · {col} 「{ref}」 없는 인력 키")
|
||||
nara = whole.index.get("MN", {})
|
||||
for key, row in whole.index.get("MT", {}).items():
|
||||
source = str(row["값"]["조달"].get("출처") or "")
|
||||
if source.startswith("나라장터:") and source.split(":", 1)[1] not in nara:
|
||||
out.append(f"{whole.label(key)} · 조달 「{source}」 이 나라장터자재에 없음")
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -669,4 +669,4 @@ def find_loops(graph: dict[str, set]) -> list[list[str]]:
|
||||
|
||||
|
||||
# 품셈재료 단가 — 서로 부르는 모듈이라 끝에서 읽음
|
||||
from master_material import LINKED, REGION, element # noqa: E402, F401
|
||||
from master_material import LINKED, REGION, SLOTS, element # noqa: E402, F401
|
||||
|
||||
@@ -18,8 +18,8 @@ KEY = re.compile(r"[A-Z]{2}\d{6}")
|
||||
# (그룹, 원문) → 테이블ID · 인력·기계는 원문 구분 없이 한 테이블
|
||||
TABLES = {
|
||||
("인력", None): "LB",
|
||||
("재료", "시중물가"): "MT",
|
||||
("재료", "나라장터자재"): "MN",
|
||||
("재료", "자재품목"): "MT",
|
||||
("재료", "나라장터자재"): "MN", # 자재품목과 잇지 않음 — 로직 호표가 키로 바로 가리킴
|
||||
("재료", "오피넷유가"): "MO",
|
||||
("재료", "품셈재료"): "MP",
|
||||
("기계", None): "EQ",
|
||||
@@ -54,7 +54,10 @@ def load_book(path: Path = BOOK) -> dict:
|
||||
|
||||
def save_book(book: dict, path: Path = BOOK) -> None:
|
||||
"""키 한 줄씩 — 머리 「다음」 뒤에 키 차례대로."""
|
||||
lines = ["{", ' "다음": ' + json.dumps(book["다음"], ensure_ascii=False) + ",", ' "키": {']
|
||||
lines = ["{", ' "다음": ' + json.dumps(book["다음"], ensure_ascii=False) + ","]
|
||||
if book.get("폐기"):
|
||||
lines.append(' "폐기": ' + json.dumps(book["폐기"], ensure_ascii=False) + ",")
|
||||
lines.append(' "키": {')
|
||||
items = sorted(book["키"].items())
|
||||
for i, (key, v) in enumerate(items):
|
||||
comma = "," if i < len(items) - 1 else ""
|
||||
@@ -187,7 +190,7 @@ def _old_ref(book: dict, text: str) -> str:
|
||||
|
||||
|
||||
def _convert(book: dict, row: dict, names: dict) -> dict:
|
||||
"""줄 속 옛 참조 — 준용 · 후보 글(키 옆 이름) · 품셈재료 연결 · 시중물가 조달 나라장터 연결."""
|
||||
"""줄 속 옛 참조 — 준용 · 후보 글(키 옆 이름) · 품셈재료 연결."""
|
||||
for col in ("준용",):
|
||||
value = row.get(col)
|
||||
if isinstance(value, str) and ":" in value:
|
||||
@@ -202,13 +205,6 @@ def _convert(book: dict, row: dict, names: dict) -> dict:
|
||||
link = row.get("연결")
|
||||
if isinstance(link, str) and not KEY.fullmatch(link):
|
||||
row["연결"] = lookup(book).get(("MT", link), link)
|
||||
value = row.get("값")
|
||||
supply = value.get("조달") if isinstance(value, dict) else None
|
||||
if isinstance(supply, dict) and str(supply.get("출처") or "").startswith("나라장터:"):
|
||||
code = supply["출처"].split(":", 1)[1]
|
||||
if not KEY.fullmatch(code): # 묶음은 새로 만듦 — 옛 줄과 나눠 쓰지 않게
|
||||
source = "나라장터:" + lookup(book).get(("MN", code), code)
|
||||
row["값"] = {**value, "조달": {**supply, "출처": source}}
|
||||
return row
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""품셈재료 단가 — 연결을 따라 시중물가 줄의 시중·조달 값 가운데 낮은 값 (`_틀.md` 4장 재료 값).
|
||||
"""품셈재료 단가 — 연결을 따라 자재품목 줄의 값 열 다섯 가운데 낮은 값 (`_틀.md` 4장 재료 값).
|
||||
|
||||
⚠ 낮은 값 고르기는 **시험 계산용**. 최저가·견적 고르기는 프로젝트 몫 — 마스터는 값을 모으기만 함.
|
||||
|
||||
`master_formula` 가 요소 값을 풀 때 부름. 서로 부르는 모듈이라 `master_formula` 는 끝에서 읽음.
|
||||
"""
|
||||
@@ -11,7 +13,7 @@ import unicodedata
|
||||
from decimal import Decimal
|
||||
|
||||
LINKED = "MP" # 품셈재료 테이블ID
|
||||
MARKET = ("가격정보", "물가자료", "유통물가", "물가정보", "거래가격")
|
||||
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
|
||||
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
|
||||
_UNITS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
|
||||
@@ -32,21 +34,9 @@ def plain_unit(s) -> str:
|
||||
|
||||
|
||||
def _offers(master: mf.Master, key: str) -> list[tuple[Decimal, str]]:
|
||||
"""시중물가 줄 하나의 (값, 출처) — 시중 다섯 칸 · 조달(나라장터 연결은 그 줄 값)."""
|
||||
value = master.get(key)["값"]
|
||||
out = [
|
||||
(value["시중"][slot], f"{key}.시중.{slot}")
|
||||
for slot in MARKET
|
||||
if value["시중"].get(slot) is not None
|
||||
]
|
||||
supply = value["조달"]
|
||||
if supply.get("값") is not None:
|
||||
out.append((supply["값"], f"{key}.조달"))
|
||||
elif str(supply.get("출처") or "").startswith("나라장터:"):
|
||||
ref = supply["출처"].split(":", 1)[1]
|
||||
if master.get(ref).get("값") is not None:
|
||||
out.append((master.get(ref)["값"], ref))
|
||||
return out
|
||||
"""자재품목 줄 하나의 (값, 출처) — 값이 있는 열마다 하나 · 출처는 그 열 이름."""
|
||||
row = master.get(key)
|
||||
return [(row[slot], f"{key}.{slot}") for slot in SLOTS if row.get(slot) is not None]
|
||||
|
||||
|
||||
def _market_index(master: mf.Master) -> list[tuple[str, str, str, str, str]]:
|
||||
|
||||
Reference in New Issue
Block a user