feat(master_data): 자재품목 구분 · 상세구분 채움 — 품명 글자 규칙 · 23 구분

- scripts/build_자재품목_분류.py: 품명 규칙(위에서 첫 일치) · 조달청 줄은 조달청 품명 표 · 다시 돌리면 같은 결과
- 재료_자재품목.json: 구분 · 상세구분 두 칸만 바뀜 (값 · 이름 · 키 그대로) · 못 가린 줄은 미분류
- ref/_묶음_자재품목.md: 구분별 상세구분 · 줄 수
- 시험: test_자재품목_분류.py 추가 · test_masterdata_check 의 「구분 비어 있음」 확인을 「채워져 있음」으로

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
2026-09-20 21:38:56 +09:00
co-authored by Claude Sonnet 5
parent 565833bcb8
commit c249f3be66
5 changed files with 31283 additions and 30595 deletions
+1 -2
View File
@@ -710,8 +710,7 @@ def test_자재품목_열_한_벌():
book = mk.load_book()
fuel = [f"MT{i:06d}" for i in range(1, 8)] # 6·7 = 전력 — 재료_유가전력으로 감
assert all(k not in rows and k not in book["키"] and k in book["폐기"] for k in fuel)
assert {r["구분"] for r in rows.values()} == {None}
assert {r["상세구분"] for r in rows.values()} == {None}
assert all(r["구분"] for r in rows.values()) # 채움 — build_자재품목_분류.py
def test_품셈재료_자재지역_후보(whole):
@@ -0,0 +1,121 @@
"""자재품목 구분 · 상세구분 — 다시 돌려도 같은 결과 · 품셈재료가 부르는 재료가 제 묶음에 듦."""
from __future__ import annotations
import collections
import json
import re
import sys
from pathlib import Path
MD = Path(__file__).resolve().parents[1] / "master_data"
sys.path.insert(0, str(MD / "scripts"))
import build_자재품목_분류 as B # noqa: E402
ROWS = json.loads((MD / "재료_자재품목.json").read_text(encoding="utf-8"))["줄"]
PM = json.loads((MD / "재료_품셈재료.json").read_text(encoding="utf-8"))["줄"]
# 품셈재료 이름(뿌리) → 자재품목에서 들어가야 할 구분
기대 = {
"시멘트": "시멘트·골재",
"모래": "시멘트·골재",
"규사": "시멘트·골재",
"모르타르": "시멘트·골재",
"이형철근": "철근·강재",
"철망": "철근·강재",
"철선": "철근·강재",
"결속선": "철근·강재",
"아연도철선": "철근·강재",
"폼타이(Form Tie)": "거푸집·가설재",
"문양거푸집(판넬)": "거푸집·가설재",
"철물(강관비계)": "볼트·앵커·철물",
"브라켓(낙하물방지망)": "볼트·앵커·철물",
"부직포": "매트·토목섬유",
"드레인 보드(연직배수재)": "매트·토목섬유",
"벤토나이트매트": "매트·토목섬유",
"VR관(소켓식)": "관·이음관",
"흄관": "관·이음관",
"파형강관": "관·이음관",
"커플링밴드(파형강관)": "관·이음관",
"고무링(VR관)": "관·이음관",
"아스팔트": "아스콘·포장",
"접착제(JOINT FILLER)": "도료·방수·접착",
"에폭시 접착제": "도료·방수·접착",
"프라이머": "도료·방수·접착",
"조합페인트": "도료·방수·접착",
"징크프라이머": "도료·방수·접착",
"벤토나이트": "도료·방수·접착",
"벤토나이트알갱이": "도료·방수·접착",
"종자(초류)": "식생·종자",
"비료": "식생·종자",
"우드칩(목재칩)": "목재",
"시멘트벽돌": "콘크리트제품",
"콘크리트못": "볼트·앵커·철물",
"와셔": "볼트·앵커·철물",
"카트리지": "볼트·앵커·철물",
"카페트": "건축마감",
"싱글코아바렐": "기계·설비",
"더블코아바렐": "기계·설비",
"드라이브파이프": "기계·설비",
"메탈크라운비트": "기계·설비",
"윙비트": "기계·설비",
"버튼비트": "기계·설비",
"폭약": "잡재료",
"아세틸렌": "잡재료",
"용접봉": "잡재료",
"물": "잡재료",
"고철": "잡재료",
}
def _root(n: str) -> str:
return re.sub(r"\s+", "", re.split(r"[\(,\-]", n.strip())[0])
def test_규칙_다시_돌려도_같은_결과():
new, res = B.main()
assert new == (MD / "재료_자재품목.json").read_text(encoding="utf-8")
assert len(res) == len(ROWS)
def test_두_칸_채움_·_구분_서른_안쪽():
by = collections.defaultdict(set)
for x in ROWS:
assert x["구분"], x["키"]
assert (x["상세구분"] is None) == (x["구분"] == "미분류"), x["키"]
by[x["구분"]].add(x["상세구분"])
assert len(by) - 1 <= 30
assert sum(1 for x in ROWS if x["구분"] == "미분류") < len(ROWS) * 0.06
def test_조달청_줄은_조달청_품명_표를_따름():
for x in ROWS:
if x["출처"].startswith("자재품목 조달청"):
assert (x["구분"], x["상세구분"]) == B.조달청_분류[re.sub(r"\s+", "", x["이름"])]
def test_품셈재료_연결_줄이_제_묶음():
by = {x["키"]: x for x in ROWS}
for p in PM:
if isinstance(p["연결"], str):
r = _root(p["이름"])
want = 기대.get(p["이름"]) or 기대.get(r)
assert want, p["이름"]
assert by[p["연결"]]["구분"] == want, (p["이름"], by[p["연결"]]["이름"])
def test_품셈재료_이름과_같은_품명_줄이_한_묶음():
idx = collections.defaultdict(list)
for x in ROWS:
idx[_root(x["이름"])].append(x)
for p in PM:
want = 기대.get(p["이름"])
if not want:
continue
hit = idx.get(_root(p["이름"]), [])
assert hit, p["이름"]
assert {x["구분"] for x in hit} == {want}, (
p["이름"],
collections.Counter(x["구분"] for x in hit),
)