"""관급 단가 짝짓기 — 작은 견본 응답으로 레미콘 지역·규격 · 아스콘 · 값 갈림 보류 · 지역 불일치 미채움 (`resources/master_data/scripts/update_관급단가.py`).""" import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "master_data/scripts")) import update_관급단가 as up # noqa: E402 def raw_row( spec, region, price, corp="테스트조합", maker="조합공통품목", cond="", bgn="2026-06-01" ): return { "prdctSpecNm": spec, "prdctSplyRgnNm": region, "cntrctPrceAmt": str(price), "cntrctCorpNm": corp, "prdctMakrNm": maker, "prdctDlvryCndtnNm": cond, "cntrctBgnDate": bgn, "rgstDt": bgn, "cntrctMthdNm": "다수 공급자 계약", "vatAplDivNm": "포함", "prdctUnit": "㎥", } def item(key, name, spec): return {"키": key, "이름": name, "규격": spec, "관급": 1, "비고": "조달 26.7"} RAW = { "MASCntrct_레미콘": [ raw_row("조합공통품목, 25-18-080", "경기도 수원시", 100000), raw_row("조합공통품목, 25-18-080", "강원도 원주시", 90000), raw_row("조합공통품목, 25-21-080", "경기도 수원시", 105000), raw_row("삼표, 25-18-080", "경기도 수원시", 1, maker="삼표"), # 제조사 줄 — 조합공통 아님 ], "MASCntrct_아스팔트콘크리트": [ raw_row("아스팔트콘크리트 조합공통품목, WC-2, 가열, 3등급", "경기도 수원시", 70000), raw_row("순환 아스팔트콘크리트 조합공통품목, WC-2, 가열, 3등급", "경기도 수원시", 60000), ], "MASCntrct_시멘트": [ raw_row("시멘트, 갑, 포틀랜드시멘트(1종), 포장40kg", "전지역(제주제외)", 5820, maker="갑"), raw_row("시멘트, 갑, 포틀랜드시멘트(1종), 포장40kg", "제주", 6470, maker="갑"), ], "MASCntrct_조경석": [ raw_row( "조경석, 을산업, B-1, 300×400×500mm, R5이상~15미만, 보령성주석", "충남", 29980, maker="을산업", ), raw_row( "조경석, 병산업, C-1, 300×400×500mm, R5이상~15미만, 함양석", "충남", 20000, maker="병산업", ), ], "Ucntrct_주철관": [ raw_row("수도용덕타일주철관, Φ80mm×6m, 2종, KP메커니컬/시멘트라이닝", "전지역", 148240), raw_row("하수도용덕타일주철관, Φ80mm×6m, 3종, KP메커니컬/시멘트라이닝", "전지역", 118940), ], "MASCntrct_봉강": [ raw_row("SD400, D10", "전지역(제주제외)", 827640, maker="가철강", cond="생산공장 상차도"), raw_row("SD400, D10", "전지역(제주제외)", 830000, maker="나철강", cond="생산공장 상차도"), ], } def fill(x): (r,) = up.refill([x], RAW) return r, up.decide(x, r, "2026-09-20") def test_레미콘_지역_규격_짝(): r, (val, note) = fill(item("a", "레미콘(관급)-경기 수원", "조합공통품목, 25-18-80")) # 80 = 080 assert val == 100000 and "조달청" in note r, (val, _) = fill(item("b", "레미콘(관급)-경기 수원", "조합공통품목, 25-21-080")) assert val == 105000 def test_레미콘_지역_불일치는_안_채움(): r, (val, note) = fill(item("c", "레미콘(관급)-울진", "조합공통품목, 25-18-080")) assert val is None and "지역 못 찾음" in note def test_레미콘_규격_없으면_안_채움(): r, (val, note) = fill(item("d", "레미콘(관급)-경기 수원", "조합공통품목, 30-24-150")) assert val is None and "강도 없음" in note def test_아스콘_순환_구분(): _, (val, _) = fill(item("e", "아스콘(관급)-경기 수원", "조합공통품목, WC-2, 가열, 3등급")) assert val == 70000 _, (val, _) = fill(item("f", "순환아스콘(관급)-경기 수원", "조합공통품목, WC-2, 가열, 3등급")) assert val == 60000 def test_제조사별_값_갈리면_최저_제조사_비고(): _, (val, note) = fill(item("g", "이형철근(SD400)-관급", "HD=10mm 생산공장 상차도")) assert val == 827640 and "가철강 최저" in note def test_짝_없는_갈래는_안_채움(): _, (val, note) = fill(item("h", "VR관(관급)", "D=300mm")) assert val is None and "VR관 품명 없음" in note def test_호출_오류에_키_안_찍힘(monkeypatch): def boom(url, **kw): raise OSError(f"연결 실패 {url}") monkeypatch.setattr(up.cc, "fetch", boom) try: up.call("op", "SECRETKEY123", a=1) except RuntimeError as e: assert "SECRETKEY123" not in str(e) and "***" in str(e) else: raise AssertionError("예외가 나야 함") def test_시멘트_제주는_제주_줄만(): _, (val, _) = fill(item("i", "시멘트 (관급)", "40kg入(포장품)")) assert val == 5820 _, (val, _) = fill(item("j", "시멘트, 제주 (관급)", "40kg入(포장품)")) assert val == 6470 def test_조경석_산지가_규격에_있으면_그_줄만(): spec = "300×400×500,R5이상~15미만,보령성주석(을산업)" _, (val, _) = fill(item("k", "조경석(관급)", spec)) assert val == 29980 # 더 싼 병산업 줄이 아니라 산지·업체가 맞는 줄 def test_주철관_종별이_맞는_줄만(): _, (val, _) = fill(item("l", "KP 주철직관(2종)시멘트라이닝/관급", "D80mm×6m")) assert val == 148240 # 3종(118,940)은 안 씀 def test_조달청에만_있는_품목은_같은_규격_최저가로_새_줄(): import update_관급단가_기타 as misc def r(price, maker): x = raw_row( f"금속돌망태, {maker}, M-1, 1000×1000×1000mm, 사각상자형", "전지역", price, maker=maker ) x["prdctClsfcNoNm"] = "금속돌망태" return x raw = {"MASCntrct_돌망태": [r(46800, "가"), r(44000, "나")]} g = misc.groups(raw) assert list(g) == ["조달청 금속돌망태 1000×1000×1000mm, 사각상자형"] name, spec, rows = g["조달청 금속돌망태 1000×1000×1000mm, 사각상자형"] val, note = up.decide( None, {"새값": 44000, "까닭": up.pick(rows)[1], "조달청": up.info(up.pick(rows)[0])}, "2026-09-20", ) assert val == 44000 and "나 최저" in note def test_교량받침_이름_괄호_제조사와_관급_꼬리(): up_raw = { "MASCntrct_교량받침": [ raw_row( "교량받침, 부흥시스템, BHS-C-700-110-F, 250×300×60mm, 700kN, 고정단, 일체형탄성받침", "전지역", 599000, maker="부흥시스템", ), raw_row( "교량받침, 타사, X-700-F, 250×300×60mm, 700kN, 고정단", "전지역", 500000, maker="타사", ), ] } x = item("m", "교량받침(부흥시스템/예산)-관급", "250×300×60mm,700kN,고정단") (r,) = up.refill([x], up_raw) assert up.decide(x, r, "2026-09-20")[0] == 599000 # 더 싼 타사가 아니라 이름의 제조사 def test_파형강관_t표기와_PE피복_글자(): def cor(spec, price): return raw_row(spec, "전지역", price, maker="가") raw = { "MASCntrct_파형강관": [ cor("파형강관, 가, A, Φ300×t1.6mm, 직관(1RS), PE내면피복", 33820), cor("파형강관, 가, B, Φ300×t1.6mm, 1RS, PE양면피복", 36310), cor("파형강관, 가, C, Φ300×t1.6mm, 1RS, 아연도금", 31870), ] } _, (val, _) = fill_raw(item("n", "파형강관(관급)-거양", "Φ300×t1.6mm, 1RS, PE양면피복"), raw) assert val == 36310 def fill_raw(x, raw): (r,) = up.refill([x], raw) return r, up.decide(x, r, "2026-09-20") def test_벤치플룸_표기_달라도_모델_치수_종별로_짝(): def bp(spec, price, region="전지역(제주,도서지역 제외)"): return raw_row(f"철근콘크리트벤치플룸관, 가, {spec}", region, price, maker="가") raw = { "MASCntrct_플룸": [ bp("500C, 500×500×2000mm, 3종", 97100), bp("500C, 500×500×2000mm, 3종", 86900, "전북(도서지역 제외)"), bp("500C, 500×500×2000mm, 2종", 50000), bp("(부품)440×80×500mm, 수로관뚜껑", 1), bp("300, 300×200×2000mm, 2종", 40000), ] } _, (val, _) = fill_raw(item("o", "벤치플륨관(3종)(관급)-전북", "500C(500*500)"), raw) assert val == 86900 # 3종만 · 전북 줄 중 최저 · 부품 줄 아님 _, (val, _) = fill_raw(item("p", "벤치플륨관(2종)", "300×200, 140kg"), raw) assert val == 40000 # kg 는 조달청 표기에 없음 def test_소형고압블록_형_두께_색(): def sb(spec, price, region="전지역(제주,도서 산간지역 제외)"): return raw_row(f"보차도용콘크리트블록, 가, X, {spec}", region, price) raw = { "MASCntrct_블록": [ sb("220×110×60mm, 소형고압블록, 불투수", 7000), # 색 안 적힘 = 회색 sb("220×110×60mm, 소형고압블록, 불투수, 컬러", 11000), sb("220×110×60mm, 잡초방지소형고압블록, 불투수", 1), sb("200×100×60mm, 소형고압블록, 불투수", 6500), ] } name = "소형고압블록(관급)-대구경북" # 붙여 쓴 두 도 assert fill_raw(item("q", name, "U형 T=6cm, 회색"), raw)[1][0] == 7000 assert fill_raw(item("r", name, "U형 T=6cm, 칼라"), raw)[1][0] == 11000 assert fill_raw(item("s", name, "I형 T=6cm, 회색"), raw)[1][0] == 6500 assert fill_raw(item("t", name, "U형 T=6cm, 적색"), raw)[1][0] is None def test_무게수로관_모델_치수(): raw = { "MASCntrct_배수로": [ raw_row( "철근콘크리트용배수로관, 가, M-400C, 400×400×2000mm, 이형수로관", "전지역", 90800 ), raw_row("철근콘크리트용배수로관, 나, CM-400C, 400×400×2000mm, 이형수로관", "전지역", 1), ] } _, (val, _) = fill_raw(item("u", "무게수로관-관급", "M-400C(400*400*120),680kg"), raw) assert val == 90800 def test_보강_같은_치수만_채움_없으면_비움(): import update_관급단가_합침 as boost def c( name, spec, price, unit="개", note="관급: 조달청 다수공급자계약 2026-09-20 · 계약 2020-01-01~ · 전지역 · 도착도 · 포함", ): return {"이름": name, "규격": spec, "단위": unit, "관급": price, "비고": note} def row(name, spec, unit="개"): return {"키": "k", "이름": name, "규격": spec, "단위": unit, "출처": "자재품목 X"} P, PV = up.places, up.PROVINCE g = lambda r, *cs: boost._same(r, cs[0], P, PV) # noqa: E731 grate = row("스틸그레이팅", "700×1000×75(I-75×7×4)") assert g(grate, c("스틸그레이팅", "700×1000×75mm, 7×4mm, 뚜껑, 격자형", 1)) assert not g(grate, c("스틸그레이팅", "700×1000×75mm, 7×4mm, 뚜껑, 투수형", 1)) # 특수형 assert not g(grate, c("스틸그레이팅", "700×1000×75mm, 5×3mm, 뚜껑", 1)) # 바 치수 다름 assert not g(grate, c("스틸그레이팅", "(부품)700×1000×75mm, 7×4mm, 뚜껑", 1)) assert not g(grate, c("스틸그레이팅", "710×1000×75mm, 7×4mm, 뚜껑", 1)) # 가까운 치수로 안 이음 assert g(row("갈매기표지판(단면)", "300×450mm", "EA"), c("갈매기표지판", "300×450×2mm", 1)) assert not g( row("갈매기표지판(단면)", "300×450mm", "EA"), c("갈매기표지판", "300×450×2mm, 양면", 1) ) assert not g( row("갈매기표지판(단면)", "300×450mm", "EA"), c("갈매기표지판", "300×450×23mm, 태양광LED", 1), ) assert not g( row("주철맨홀뚜껑(원형)", "Φ648 KS", "EA"), c("주철맨홀뚜껑", "원형, Φ648×110mm", 1) ) assert g(row("보행매트(관급)", "1,000×t30mm", "M"), c("보행매트", "1000×t30mm", 1, "m")) assert not g( row("보행매트(관급)", "1,000×t30mm", "M"), c("보행매트", "1000×t30mm", 1, "개") ) # 단위 다름 def test_보강_새_줄은_목록_품명만_같은_규격_한_줄(): import update_관급단가_합침 as boost assert "스틸그레이팅" not in boost.NEW and "주철맨홀뚜껑" not in boost.NEW # 다산에 있는 품명 assert boost.NEW["철제가드레일"] == "안전시설" keys = {} rows = boost.new_rows( {"키": {}, "다음": {}}, lambda b, t, n, f: keys.setdefault(n, f"MT{len(keys) + 1:06d}"), {("낙석방지책", "존재")}, ) assert all(r["이름"] in boost.NEW and r["출처"].startswith("자재품목 조달청 ") for r in rows) assert len({r["키"] for r in rows}) == len(rows)