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:
Binary file not shown.
Binary file not shown.
@@ -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"])
|
||||
):
|
||||
|
||||
+800
-800
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user