Files
Aislo/resources/tester/test_830_2_material_merge.py
T
eomsangdonandClaude Opus 5.5 b9a8311c6a feat(830-11): 재료 지역 열 → 시도 · 시군구 두 칸 · 행정구역 표 따로(_행정구역.json)
- 행정구역 표 = 시도 17(짧은 이름) + 전국 · 시도별 시군구 229(자치 227 + 제주 행정시 2 · 2026-07-01 인천 제물포 · 영종 · 검단 반영) · 옛 이름(강원도 · 전라북도 · 청원 · 마산 · 진해 · 연기 · 인천 남구 · 중구 · 동구 · 경북 군위) · 별칭(영종도 · 용유도 · 세종시 · 제주도)
- 풀이 master_region.split — 시도만 = 시군구 「전체」 · 시군구만 = 시도 찾음 · 읍면 = 시군구(읍면) · 여러 시도 = 쉼표 · 제외 조건 = 끝에 「 제외」 · 조합 이름 = 비고 · 잘린 끝 낱말 = 하나뿐인 시군구(동두 → 동두천 · 금 → 금천)
- 지역 글 242 종 모두 풀림(못 푼 것 0) · 옛 이름 바꾼 줄 648 · 줄 · 키 · 값 그대로(24,027 · 비고만 조합 121 줄)
- 쓰는 곳: 지역 글 = material_merge.where(시도 + 시군구) — M01 찾기(label · 검색어 · 자재지역) · 표 찾기 · 후보 글 · 불러오기(M02_Mat_Place · Hopyo_Match · Hopyo_Material — 가져온 지역 낱말도 짧은 이름으로 · 시군구가 있으면 그것으로 맞댐)
- M01 화면: 시도 = 고르기(목록 밖 값 「대전,충남」 은 끝에) · 시군구 = 글 · 새 줄 - · -
- 로직 1,520 시험 계산 전후 다름 0 · check_master 틀 · 본문 · 로직 · 조합 · 목차 0 · 단위 27 · 배 7(전과 같음)
- 빌더 adopt 가 로직 장 파일 「차례」 를 하나씩 미는 것은 이번 일 밖 — 되돌림(로직 파일 손대지 않음)
- 새 지킴 test_830_11_material_sido · 낡은 시험 고침: test_830_10 · test_830_2(열 · 지역 기대를 두 칸으로 · 830-11 새 겹침 38 묶음 = 청원 → 청주 4 · 조합 비고 34) · test_m02_hopyo_match(강원도(강릉시) → 강원 강릉)
- 틀 문서 _틀.md 11 절 행정구역 표

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 09:56:54 +09:00

154 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""830-2 · 830-9 · 830-10 재료 표 — 가격 열 재정의(요구 827 ~ 832) · 항목 목록 = EST 그대로(요구 834) 지킴.
열 한 벌(지역 · 상하차 = 단위 뒤 · 조달청 · 물가지 넷 · 일반가격) · 조달청 값 0 · 일반가격 빈칸 ·
이름 = EST 이름에서 지역 · 인도 조건 글만 뺌(지역 · 관급 · 사급 줄 따로 · 요구 836 · 839) · 상하차 = 인도 조건 글 판독 ·
사급 / 관급 열 없음(요구 837) · 재료 아닌 줄 0 ·
치즐 값 없음 · 줄 · 키 · 이름 · 규격 · 단위 · 물가지 값 = EST 원본 그 줄 · 빌더 = 정본 · 가장 낮은 값이 조달청 칸을 안 봄.
"""
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
import build_자재품목_분류 as cls # noqa: E402
import build_재료_자재품목 as builder # noqa: E402
import compare_estx as ce # noqa: E402
import master_keys as mk # noqa: E402
import master_material as mmat # noqa: E402
import material_merge as mm # noqa: E402
MASTER = ROOT / "resources/master_data"
COLS = (
"키",
"구분",
"상세구분",
"이름",
"규격",
"단위",
"시도",
"시군구",
"상하차",
"조달청",
"물가자료",
"유통물가",
"물가정보",
"거래가격",
"일반가격",
"출처",
"비고",
"면수",
)
@pytest.fixture(scope="module")
def rows():
return json.loads((MASTER / "재료_자재품목.json").read_text(encoding="utf-8"))["줄"]
@pytest.fixture(scope="module")
def raw():
if not ce.ESTX.is_file():
pytest.skip("ESTX 원본 없음")
return ce.Estx().table("StuffcDat")
def test_열_한_벌_차례(rows):
assert mm.COLUMNS == COLS and "사급/관급" not in COLS
assert {tuple(r) for r in rows} == {COLS}
def test_상하차_세_값(rows):
got = {r["상하차"] for r in rows}
assert got == {"-", "상차", "하차"}
def test_조달청_빔_일반가격_빔(rows):
assert all(r["조달청"] is None for r in rows)
assert all(
r["일반가격"] is None for r in rows
) # 처음엔 전부 빈칸 — 손 입력이 생기면 이 줄을 고침
def test_이름_EST_그대로_지역_관급_사급_줄_따로(rows):
remi = [r["이름"] for r in rows if r["이름"].startswith("레미콘")]
assert [n for n in remi if "관급" in n] and [n for n in remi if "사급" in n]
sa = {
mm.where(r) for r in rows if r["이름"] == "레미콘-사급"
} # 지역은 시도 · 시군구 칸(839 · 842)
assert {"서울", "경북 포항"} <= sa
assert not [r for r in rows if "통합:" in str(r["비고"] or "")]
def test_재료_아닌_줄_0_치즐_남고_값_없음(rows):
names = {r["이름"] for r in rows}
gone = ("골재시험", "콘크리트시험", "흙의물리시험", "아스팔트시험", "품질시험 현장출장비")
assert not names & set(gone)
assert not names & {"네오스톤블록", "네오스톤슬라브", "앤틱스톤블록"}
chisel = [r for r in rows if r["키"] in ("MT000009", "MT000011")] # 품셈(2016) 값 줄 둘
assert [r["이름"].replace(" ", "") for r in chisel] == ["치즐(0.4㎥)", "치즐(0.7㎥)"]
assert all(r[c] is None for r in chisel for c in (mm.GOVT, *mm.MARKET, mm.OWN))
assert not [
r
for r in rows
if "가격정보" in str(r["비고"] or "") or "품셈(2016)" in str(r["비고"] or "")
]
def test_상하차_인도_조건_글_칸으로_이름에서_뺌(rows):
assert not [r for r in rows if mm.split_load(r)[3]] # 이름 · 규격에 인도 조건 글 0
rebar = [r for r in rows if r["이름"].startswith("이형철근") and r["규격"].startswith("HD=")]
assert [r for r in rebar if r["상하차"] == "상차"]
assert {r["상하차"] for r in rows if r["이름"] == "모 래" and r["시도"] != "-"} >= {"하차"}
assert mm.split_load({"이름": "모 래, 서울, 도착도", "규격": ""}) == (
"모 래, 서울",
"",
"하차",
"도착도",
)
assert mm.split_load({"이름": "이형철근", "규격": "HD=10mm 하치장 상차도"})[1:3] == (
"HD=10mm",
"상차",
)
assert mm.split_load({"이름": "모래", "규격": "세척사"}) == ("모래", "세척사", "-", None)
def test_빌더_정본_줄_키_EST_그대로(rows, raw):
book = mk.load_book()["키"]
code = {r["키"]: book[r["키"]]["원문번호"] for r in rows}
src = {r["Code"]: r for r in raw if r.get("Code") and r["Code"] not in builder.DROP}
est = [r for r in rows if not str(r["출처"]).startswith(builder.CARRY)]
assert len(est) == 23908 and len(rows) == 24027 # 830-1 24,092 − 재료 아닌 줄 65
assert [code[r["키"]] for r in est] == [
c for c, s in sorted(src.items(), key=lambda x: x[1]["Index"]) if not builder.dropped(s)
]
for r in est:
s = src[code[r["키"]]]
name, spec, _, _ = mm.split_load({"이름": s.get("Name", ""), "규격": s.get("Size", "")})
name, spec, _ = mm.split_region(name, spec)
assert (r["이름"], r["규격"], r["단위"]) == (name, spec, s.get("Unite", ""))
assert [r[c] or 0 for c in mm.MARKET] == [s.get(f"Cost{i}") or 0 for i in range(2, 6)]
built = [mm.place(r) for r in builder.build(raw, builder.own_prices()) + builder.carried()]
assert [code[r["키"]] for r in rows] == [r["열쇠"] for r in built] # 다시 지어도 같은 표
same = [
c for c in COLS if c not in ("키", "구분", "상세구분", "비고")
] # 분류 두 칸 = 분류기 몫
for r, b in zip(rows, built):
assert [r[c] for c in same] == [b[c] for c in same]
assert r["비고"] == b["비고"] or (r["비고"] == cls.NOTE_EMPTY and not r["이름"])
def test_가장_낮은_값은_조달청_안_봄():
assert mm.GOVT not in mmat.SLOTS and mm.OWN in mmat.SLOTS
row = {"키": "MT999999", mm.GOVT: 1, "물가자료": 50, mm.OWN: 40}
class M:
def get(self, key):
return row
assert mmat._lowest(M(), "MT999999") == (40, "MT999999.일반가격")