feat(master_data): 관급 다산 줄 우선 채움 · 못 채움 800 → 495 · 못 채운 까닭 비고

- 아스콘 지역 포함(시군 붙은 글 끊기) · R3-H/A1-H 규격 번호 떼기 · 파형강관 ×t·1RS·PE 피복 · 교량받침·합성목재·혼합골재 제조사 괄호 짝
- 못 채운 495줄 비고에 까닭(조달청 종합쇼핑몰에 없음 · 짝 못 지음) · 경질폴리염화비닐관 받기
- 다산 4,552줄 기준 채움 4,057 · 맞음 3,442 · 다름 615 · 못 채움 495 · 로직 일괄 시험 멈춤 327 그대로
- 시험 13건 · 설계 문서 대조표 갱신

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
2026-09-20 18:40:55 +09:00
co-authored by Claude Sonnet 5
parent 3b37f9d24d
commit a905edd87f
8 changed files with 977 additions and 826 deletions
@@ -45,4 +45,6 @@
| ThptyUcntrct_시트.json.gz | getThptyUcntrctPrdctInfoList | 시트 | 39 |
| MASCntrct_비료.json.gz | getMASCntrctPrdctInfoList | 비료 | 101 |
| MASCntrct_압륜.json.gz | getMASCntrctPrdctInfoList | 압륜 | 142 |
| ThptyUcntrct_부직포.json.gz | getThptyUcntrctPrdctInfoList | 부직포 | 8 (totalCount 8) |
| ThptyUcntrct_부직포.json.gz | getThptyUcntrctPrdctInfoList | 부직포 | 8 |
| MASCntrct_경질폴리염화비닐관.json.gz | getMASCntrctPrdctInfoList | 경질폴리염화비닐관 | 949 (totalCount 949) |
| ThptyUcntrct_경질폴리염화비닐관.json.gz | getThptyUcntrctPrdctInfoList | 경질폴리염화비닐관 | 330 (totalCount 330) |
@@ -6,7 +6,7 @@
- 지난번 「시설공통자재 6,999줄」(`PriceInfoService`)에는 관급 자재가 없음 — 잘못 고른 API.
- **값 비교 기준 = 다산소프트 값 ±5% 안이면 「맞음」**(받는 시점이 달라 생기는 차이).
- **제조사별로 값이 갈리면 같은 규격의 가장 낮은 값**을 관급 열에 넣고 비고에 그 제조사(「관급: 조달청 다수공급자계약 <제조사> 최저 <날짜> · …」) — 줄·열은 안 늘림. 보류 394 → 0.
- 정본 `재료_자재품목.json` 관급 열: 옛 4,552줄**채움 3,752 · null 800**(맞음 3,232 · 5% 넘게 다름 520 · 못 채움 800) + **조달청에서 새로 들인 줄 380**(모두 채움) = 4,932줄 중 **채움 4,132 · null 800**. 다산 값은 남기지 않음.
- 정본 `재료_자재품목.json` 관급 열: 옛 4,552줄(다산소프트 관급 값이 있던 줄) 중 **채움 4,057(89%) · null 495**(맞음 3,442 · 5% 넘게 다름 615 · 못 채움 495) + **조달청에서 새로 들인 줄 380**(모두 채움) = 4,932줄 중 **채움 4,437 · null 495**. 다산 값은 남기지 않음. 못 채운 495줄은 비고에 까닭(「조달청 종합쇼핑몰에 없음」 · 「짝 못 지음」).
- 다시 돌리기 — `./venv/Scripts/python.exe resources/master_data/scripts/update_관급단가.py update`
## 조달청 API (공공데이터포털 15129471 `at/ShoppingMallPrdctInfoService`)
@@ -39,27 +39,43 @@
- 지역 — 지명 낱말로 쪼개 약칭을 풀고(경기 → 경기도) 「시·군·구」·「전지역(구 나열)」·조합 이름을 뺀 뒤, 같은 묶음 → 겹침 0.5 이상 하나뿐일 때만 짝(동점은 못 채움). 이름에 조합이 붙은 줄(제주…레미콘(협))은 그 조합 줄만.
- 후보가 여럿이면 — 값이 하나면 그 값 · **제조사별로 값이 갈리면 가장 낮은 값 + 비고에 그 제조사**(확정 · `pick()` 한 곳).
## 다산소프트 값과 맞대기 (±5% 안 = 맞음 — 사본에서 관급 열을 지우고 다시 채움 · 2026-09-20)
## 다산소프트 값과 맞대기 (±5% 안 = 맞음 — 다산 관급 값이 있던 4,552줄 기준 · 2026-09-20 · 3차 반영)
| 갈래 | 전체 | 짝 서고 채움 | 맞음(±5%) | 5% 넘게 다름 | 보류 | 못 채움 |
|---|---|---|---|---|---|---|
| 레미콘 | 2,095 | 2,078 | **2,033** | 45 | 0 | 17 |
| 아스콘 | 1,430 | 1,128 | 855 | 273 | 0 | 302 |
| 아스콘 | 1,430 | 1,303 | 1,010 | 293 | 0 | 127 |
| 철근 | 81 | 81 | **81** | 0 | 0 | 0 |
| H형강 | 57 | 27 | 26 | 1 | 0 | 30 |
| 파형강관 | 214 | 98 | 32 | 66 | 0 | 116 |
| 파형강관 | 214 | 210 | 69 | 141 | 0 | 4 |
| 흄관 | 27 | 27 | 10 | 17 | 0 | 0 |
| 조경석 | 48 | 48 | **48** | 0 | 0 | 0 |
| 파형강관이음관 | 108 | 102 | 87 | 15 | 0 | 6 |
| 주철관 · 부속 | 116 | 116 | 34 | 82 | 0 | 0 |
| 측구수로관 | 23 | 17 | 8 | 9 | 0 | 6 |
| 시멘트 · 골재 · 볼라드 · 맨홀 · 용접철망 · 돌망태 · 매트 | 56 | 30 | 18 | 12 | 0 | 26 |
| 그 밖(벤치플륨 · 블록 · 폴리에틸렌 · 교량받침 · 합성목재 · 무게수로관 등) | 297 | 0 | 0 | 0 | 0 | 296 |
| **합계** | **4,552** | **3,752** | **3,232** | **520** | **0** | **800** |
| 시멘트 · 골재 · 볼라드 · 맨홀 · 용접철망 · 돌망태 · 매트 | 56 | 36 | 24 | 12 | 0 | 20 |
| 그 밖(벤치플륨 · 소형고압블록 · 폴리에틸렌 · 교량받침 · 합성목재 · 무게수로관 등) | 297 | 12 | 12 | 0 | 0 | 285 |
| **합계** | **4,552** | **4,057** | **3,442** | **615** | **0** | **495** |
- 「5% 넘게 다름」 = 제조사별 최저값이 다산이 고른 제조사·조건 값보다 낮은 경우가 대부분(주철관 0.60~1.00 · 이음관 0.45~1.00 · 흄관·파형강관) + 아스콘 기준일 차이(조달청 계약 2026-08-01~ · 다산 「조달 26.7」).
- 3차 전(2차 끝) → 후: 채움 3,752 → 4,057 · 맞음 3,232 → 3,442 · 다름 520 → 615 · 못 채움 800 → 495.
- 「5% 넘게 다름」 = 제조사별 최저값이 다산이 고른 제조사·조건 값보다 낮은 경우가 대부분(주철관 0.60~1.00 · 이음관 0.45~1.00 · 흄관·파형강관) + 아스콘 기준일 차이(조달청 계약 2026-08-01~ · 다산 「조달 26.7」) + 파형강관 PE 피복줄(조달청 최저 = 다산 값의 0.7~0.8).
- 레미콘 다름 45 — 광주·전남 25-30-180(다산 117,800 · 조달청 118,710) · 제주 세 조합(다산 119,130 · 조달청 125,940~126,000) — 지역·규격 같음 → 기준일 차이 또는 다산 원문 값.
- 못 채움 800 — 아스콘 302(지역 240 · 규격 62) · 그 밖 297(짝 규칙 없음 · 조달청에 같은 품명 없음) · 파형강관 116(「-거양」 규격이 조달청 목록에 없음) · H형강 30 · 돌망태·매트 20 · 레미콘 17 · 이음관 6 · 측구수로관 6 · 골재 6.
## 다산 관급 줄 우선 채움 (3차 · 못 채움 800 → 495)
- 아스콘 「지역 포함」 — 다산 묶음의 시군이 조달청 한 줄의 공급 지역 안에 모두 들어 있으면 그 줄들의 최저가(+127). 구분 없이 붙은 시군(「진주사천남해…」)은 두 글자씩 끊어 다시 찾음.
- 순환·일반 아스콘 규격 「R3-H-BB2, BB-2, …」 · 「A1-H-…」 — 앞의 관리 번호를 떼고 짝(+17).
- 파형강관 — 다산 규격 「Φ300×t1.6mm, 1RS, PE내면피복」 의 「×t」 · 「1RS」(괄호 없음) · 피복 글자를 읽음. 먼저 「직관(nRS)」 꼴(PE 피복 줄은 피복 글자가 맞아야), 없으면 종류·피복 글자가 맞는 줄(+112). 「-거양」은 조달청 제조사가 아니라 무시.
- 교량받침(부흥시스템) 9 · 합성목재(본우드) 3 · 도로용혼합골재(충남) 6 — 이름 괄호의 제조사를 조달청 제조사와 맞대고, 「-관급」 꼬리·「×t25」·「1,000」·「solid」 표기 차이를 맞춤.
- 경질폴리염화비닐관(MAS · 3자단가) 받아 봄 — 다산 HI-VP(삼중벽)는 조달청에 단층 HIVG 뿐이라 짝 안 지음.
- 못 채움 495의 까닭은 비고 「관급: 조달청 값 못 채움 — …」 에 적음:
| 까닭 | 줄 수 | 갈래 |
|---|---|---|
| 조달청 종합쇼핑몰에 없음 — 품명 없음 | 247 | 벤치플륨 111 · 소형고압블록 62 · 삼중벽 HI-VP 23 · 무게수로관(뚜껑 포함) 23 · KP 접합부속 17 · VR관 11 |
| 조달청 종합쇼핑몰에 없음 — 그 규격 없음 | 81 | H형강 30(치수) · 아스콘 10 · 돌망태·개비온 14 · 보행매트 6 · 이음관 6 · 합성목재 6 · 생태블록 3 · 파형강관 4 · 섬유 1 · 레미콘 1 |
| 조달청 종합쇼핑몰에 그 지역 묶음의 줄 없음 | 73 | 아스콘 57(강원 · 세종충남 · 전북) · 레미콘 여수 읍면 16 |
| 짝 못 지음 | 94 | 아스콘 「~구 제외」 60 · PE직관·PE이중벽 28(조달청은 압력·강성 등급별) · 측구수로관 6 |
## 짝짓기 규칙 넓힘 (2차)
@@ -83,7 +99,7 @@
## 로직 일괄 시험 계산 (전후)
- 방법 — 로직 줄마다 입력 조합(최대 40)으로 `master_formula.run` 을 돌려 하나라도 통과하면 통과 · 아니면 멈춤.
- 관급 열 교체 전(다산소프트 값) **통과 1,024 · 멈춤 327** → 후(조달청 값 · 최저 채움 · 새 줄 380 포함) **통과 1,024 · 멈춤 327** — 변화 없음(멈춤 까닭 = 값 없음 285 · 맞는 줄 없음 25 · 지역 값 없음 16 · 칸 없음 1 — 관급이 멈춤 까닭이 아님).
- 관급 열 교체 전(다산소프트 값) **통과 1,024 · 멈춤 327** → 후(조달청 값 · 최저 채움 · 새 줄 380 · 3차 채움 포함) **통과 1,027 · 멈춤 327**(통과 +3 = dev 합류로 로직 줄이 3 늘어난 몫 · 멈춤 수 그대로)(멈춤 까닭 = 값 없음 285 · 맞는 줄 없음 25 · 지역 값 없음 16 · 칸 없음 1 — 관급이 멈춤 까닭이 아님).
- check_master — 틀 0 · 본문 1(기존) · 로직 0.
## 갱신 절차
@@ -96,7 +112,7 @@
## 남은 일
- 못 채움 800 — 아스콘 지역 240 · 규격 62 · 그 밖 297 · 파형강관 116 · H형강 30 · 그 밖 소수(위 표).
- 받아 둔 분류(블록 · 교량받침 · 폴리에틸렌 · 합성목재 · 그레이팅 · 낙석 · 시트 · 비료 · 매트)는 다산 규격과 조달청 규격 표기가 달라 짝 규칙을 더 만들어야 채움 — 또는 새 줄로 들임.
- 못 채움 495 — 위 까닭 표. 사람 확인이 필요한 것 = 벤치플륨(수로관 유사품과 같은 물건인지) · 소형고압블록(보차도용콘크리트블록 t60·t80 으로 볼지).
- 새로 더 받는 분류는 다산 줄 채움이 끝난 뒤 — 받아 둔 분류(블록 · 교량받침 · 폴리에틸렌 · 합성목재 · 그레이팅 · 낙석 · 시트 · 비료 · 매트 · 경질폴리염화비닐관)는 다산 규격과 표기가 달라 짝 규칙을 더 만들거나 새 줄로 들임.
- 품셈재료 → 자재품목 연결(343 줄 미연결)은 화면에서 사람이 이음.
- 짝짓기 시험 = `resources/tester/test_masterdata_procure.py`(견본 응답 11건).
- 짝짓기 시험 = `resources/tester/test_masterdata_procure.py`(견본 응답 13건).
@@ -76,6 +76,8 @@ JOBS = [ # 분류명은 부분 글자 조회 — 「철근」은 암거블록
(MAS, "비료"),
(MAS, "압륜"),
(THPTY, "부직포"),
(MAS, "경질폴리염화비닐관"),
(THPTY, "경질폴리염화비닐관"),
]
# 보관하는 칸 — 나머지(첨부·원산지·이미지·긴 설명)는 버림(원본이 수백 MB)
KEEP = (
@@ -213,6 +215,10 @@ def region_pick(cands: dict, want: frozenset):
hit = [k for k in cands if k <= want and k - PROVINCE]
if towns <= set().union(*hit) if hit else False:
return [r for k in hit for r in cands[k]], f"지역 묶음 {len(hit)}"
# 다산 묶음의 시군이 조달청 한 줄의 공급 지역 안에 모두 들어 있음 — 그 줄들의 최저가(값이 갈리면 pick 이 최저)
sup = [k for k in cands if towns and towns <= k]
if sup:
return [r for k in sup for r in cands[k]], f"지역 포함 {len(sup)}"
return None, "지역 못 찾음"
@@ -243,7 +249,7 @@ def _dia(t):
def _thick(t):
m = re.search(r"[×xX*](\d+(?:\.\d+)?)", t or "")
m = re.search(r"[×xX*]t?(\d+(?:\.\d+)?)", t or "")
return float(m.group(1)) if m else None
@@ -254,6 +260,10 @@ def _size(t):
return tuple(float(x) for x in m.groups()) if m else None
CORR_WORDS = ("PE양면피복", "PE내면피복", "아연도금", "유공관", "접속티", "커플링밴드", "직관")
CORR_EXTRA = ("내부평활", "플랜지", "세라믹", "복합", "PE+PP", "PE/PP", "내식")
class Matcher:
"""자재품목 줄 → 조달청 줄. 짝이 서면 (줄, 방식), 못 서면 (None, 까닭)."""
@@ -287,8 +297,16 @@ class Matcher:
def _region(table: dict, name: str):
want = places(name.split(")-", 1)[1] if ")-" in name else name)
cands, why = region_pick(table, want)
if cands is None: # 구분 없이 붙은 시군(「진주사천남해…」)은 두 글자씩 끊어 다시
cut = frozenset(
q
for w in want
for q in ([w[i : i + 2] for i in range(0, len(w), 2)] if len(w) >= 6 else [w])
)
if cut != want:
cands, why = region_pick(table, cut)
if cands is None or (
why.startswith("지역 묶음") and "제외" in name
why.startswith(("지역 묶음", "지역 포함")) and "제외" in name
): # 「~ 제외」 묶음은 짝짓지 않음
return None, why if cands is None else "지역 못 찾음"
corp = re.findall(
@@ -299,6 +317,12 @@ class Matcher:
return cands, why
def find(self, row: dict):
r, why = self._find(row)
if r is None:
why = misc.absent(row["이름"], why)
return r, why
def _find(self, row: dict):
n, spec = row["이름"], row["규격"]
if n.startswith("레미콘(관급)-"):
k = strength(spec)
@@ -312,7 +336,15 @@ class Matcher:
if re.match(r"(순환)?아스콘\(관급\)-", n):
k = (
n.startswith("순환"),
re.sub(r"\s+", "", re.split(r"조합공통품목(?:\([^)]*\))?,", spec)[-1]),
re.sub(
r"\s+",
"",
re.sub(
r"^\s*[A-Z]\d-H-[^,]*,",
"",
re.split(r"조합공통품목(?:\([^)]*\))?,", spec)[-1],
),
), # 「R3-H-BB2, BB-2, …」 = 앞의 순환 관리 번호를 떼고 「BB-2, …」
)
if k not in self.asc:
return None, "규격 없음(원문 규격이 조달청에 없음)"
@@ -358,18 +390,31 @@ class Matcher:
if re.match(r"파형강관\(관급\)", n):
dia, th = _dia(spec), _thick(spec)
rs = re.search(r"(\d)RS", spec)
c = [
if not (dia and th and rs):
return None, "후보 없음"
coat = [w for w in ("PE양면피복", "PE내면피복") if w in spec]
c = [ # 먼저 「(nRS)」 꼴 — 이미 채운 값이 흔들리지 않게 · PE 피복 줄은 피복 글자가 맞아야
r
for r in self.corr
if dia
and th
and rs
and _dia(r["prdctSpecNm"]) == dia
if _dia(r["prdctSpecNm"]) == dia
and re.search(rf"t{th:g}mm", r["prdctSpecNm"])
and f"({rs.group(1)}RS)" in r["prdctSpecNm"]
and all(re.search(rf"(?<!복합)(?<!\+)(?<!/){w}", r["prdctSpecNm"]) for w in coat)
]
r, how = pick(c)
return r, how
if c:
return pick(c)
kinds = [w for w in CORR_WORDS if w in spec] # 없을 때만 — 종류·피복 글자가 맞는 줄
for r in self.corr:
t = r["prdctSpecNm"]
if (
_dia(t) == dia
and re.search(rf"t{re.escape(f'{th:g}')}(\.0)?mm", t)
and re.search(rf"(?<!\d){rs.group(1)}RS", t)
and all(re.search(rf"(?<!복합)(?<!\+)(?<!/){w}", t) for w in kinds)
and all(w in spec or w not in t for w in CORR_EXTRA) # 다산에 없는 덧붙임은 뺌
):
c.append(r)
return pick(c)
if re.match(r"\s*관", n):
dia = _dia(spec)
c = [
@@ -38,6 +38,43 @@ FAMILIES = (
["MASCntrct_폴리에틸렌", "ThptyUcntrct_폴리에틸렌"],
),
)
# 조달청에 그 품목·규격이 없거나 다산 규격으로는 물건을 가릴 수 없는 것 — 「관급」 못 채움의 까닭 글
ABSENT = (
(
r"^벤치플륨",
"조달청 종합쇼핑몰에 없음 — 벤치플륨 품명 없음(수로관 유사품이 같은 물건인지 확인 못 함)",
),
(r"^무게수로관", "조달청 종합쇼핑몰에 없음 — 무게수로관 품명 없음"),
(r"^HI-VP", "조달청 종합쇼핑몰에 없음 — 삼중벽 품명 없음(단층 HIVG 만 있음)"),
(r"^KP 접합부속", "조달청 종합쇼핑몰에 없음 — 접합부속 세트 품명 없음"),
(
r"^소형고압블록",
"조달청 종합쇼핑몰에 없음 — 소형고압블록 품명 없음(보차도용콘크리트블록은 형·색 표기가 달라 안 지음)",
),
(
r"^(PE직관\(수도용\)|PE이중벽)",
"짝 못 지음 — 조달청은 압력·강성 등급별 값인데 다산 규격에 등급 없음",
),
(r"^VR관", "조달청 종합쇼핑몰에 없음 — VR관 품명 없음"),
(
r"^(순환)?아스콘.*제외",
"짝 못 지음 — 다산 묶음이 「~구 제외」 꼴이라 조달청 공급 지역과 안 맞음",
),
(r"^측구수로관", "짝 못 지음 — 조달청은 길이·형별 표기(가로×세로×길이)"),
)
GONE = ("후보 없음", "강도 없음(원문 규격이 조달청에 없음)", "규격 없음(원문 규격이 조달청에 없음)")
def absent(name: str, why: str) -> str:
"""못 채운 까닭 → 조달청에 없음 · 짝 못 지음 을 가려 적음."""
for pat, text in ABSENT:
if re.search(pat, name):
return text
if why == "지역 못 찾음":
return "조달청 종합쇼핑몰에 그 지역 묶음의 줄 없음 — 지역 못 찾음"
return f"조달청 종합쇼핑몰에 없음 — {why}" if why in GONE else why
SKIP_WORDS = {"관급", "포장품", "", "각종"}
@@ -49,6 +86,8 @@ def norm(t: str) -> str:
t = (t or "").lower().replace("", "mm").replace("", "m3").replace("", "kg")
t = re.sub(r"[φ∅øᴓф]", "d", t)
t = re.sub(r"(?<=\d)mm", "", t)
t = re.sub(r"(?<=\d),(?=\d{3})", "", t) # 「1,000」
t = re.sub(r"(?<=[×x*\s,])t(?=\d)", "", t).replace("solid", "솔리드") # 「×t30」 = 「×30」
t = re.sub(r"(?<=[\d.])[×*x](?=[\d.d])", "x", t)
return re.sub(r"\s+", "", t).replace("", "")
@@ -67,7 +106,7 @@ def tail_of(name: str) -> str | None:
"""이름 끝의 「-지역」 또는 「-제조사」 · 「(충남지역」 · 「시멘트, 제주」."""
m = re.search(r"[\)\s]-([가-힣,·]+)$", name) or re.search(r"\(([가-힣]+)지역", name)
if m:
return m.group(1)
return None if m.group(1) == "관급" else m.group(1)
return "제주" if ", 제주" in name else None
@@ -83,6 +122,9 @@ def find(row: dict, raw: dict, places, pick, PROVINCE=frozenset()):
tail = tail_of(n)
reg = tail if tail and places(tail) & PROVINCE else None # 지역 이름이 아니면 제조사
maker = norm(tail) if tail and not reg else None
if fam[0] == "골재": # 「토금산업(홍성), 75mm」 — 공장 이름 + 굵기 · 지역은 공장이 정함
spec, reg = re.sub(r"\([^)]*\)", "", spec), None
paren = re.search(r"\(([가-힣]+)", n) if fam[0] in ("교량받침", "합성목재") else None
need = pieces(spec)
if fam[0] == "주철관": # 이름의 종별(2종 · 3종)도 맞아야
need += [p for p in pieces(n) if re.search(r"\d", p)]
@@ -93,6 +135,10 @@ def find(row: dict, raw: dict, places, pick, PROVINCE=frozenset()):
s = norm(r["prdctSpecNm"])
if not all(has(s, p) if re.search(r"\d", p) else p in s for p in need):
continue
if paren and norm(paren.group(1)) not in norm(
r["prdctSpecNm"] + r["cntrctCorpNm"] + str(r["prdctMakrNm"])
):
continue
if maker and maker not in norm(
r["prdctSpecNm"] + r["cntrctCorpNm"] + str(r["prdctMakrNm"])
):
File diff suppressed because it is too large Load Diff
+43 -1
View File
@@ -106,7 +106,7 @@ def test_제조사별_값_갈리면_최저_제조사_비고():
def test_짝_없는_갈래는_안_채움():
_, (val, note) = fill(item("h", "벤치플륨(관급)", "300"))
assert val is None and "짝짓기 없음" in note
assert val is None and "벤치플륨 품명 없음" in note
def test_호출_오류에_키_안_찍힘(monkeypatch):
@@ -160,3 +160,45 @@ def test_조달청에만_있는_품목은_같은_규격_최저가로_새_줄():
"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")