- 행정구역 표 = 시도 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>
154 lines
6.1 KiB
Python
154 lines
6.1 KiB
Python
"""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.일반가격")
|