feat(master_data): 관급 못 채움 320 → 108 — 느슨한 두 번째 짝짓기 · 다산 옛 값 ±5% 안일 때만 채움
- update_관급단가_느슨.py 신설 — 엄격한 짝이 안 선 줄을 갈래 안에서 규격 글자를 느슨하게 맞춰 다시 봄 (아스콘 · H형강 · HI-VP · PE · VR관 · 돌망태 · 무게수로관 뚜껑 · 벤치플룸 · 유공관 · 이음관) - 아스콘 관리번호 A3-W 떼기 · 못 채운 까닭 비고를 지금 말로 다듬음 - 자재품목 관급 열 212줄 채움(다른 열·줄 그대로) · 시험 · 설계 문서 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
@@ -24,6 +24,7 @@ sys.path.insert(0, str(PIPE))
|
||||
import collect_cost_sources as cc # noqa: E402 (키 읽기 · fetch)
|
||||
import master_keys as mk # noqa: E402 (새 줄 키)
|
||||
import update_관급단가_기타 as misc # noqa: E402 (「그 밖」 갈래 짝짓기)
|
||||
import update_관급단가_느슨 as slow # noqa: E402 (엄격한 짝이 안 선 줄 · 느슨한 두 번째 짝)
|
||||
import update_관급단가_합침 as boost # noqa: E402 (보강 후보 합치기)
|
||||
|
||||
BASE = "https://apis.data.go.kr/1230000/at/ShoppingMallPrdctInfoService/"
|
||||
@@ -346,11 +347,11 @@ class Matcher:
|
||||
r"\s+",
|
||||
"",
|
||||
re.sub(
|
||||
r"^\s*[A-Z]\d-H-[^,]*,",
|
||||
r"^\s*[A-Z]\d-[HW]-[^,]*,",
|
||||
"",
|
||||
re.split(r"조합공통품목(?:\([^)]*\))?,", spec)[-1],
|
||||
),
|
||||
), # 「R3-H-BB2, BB-2, …」 = 앞의 순환 관리 번호를 떼고 「BB-2, …」
|
||||
), # 「R3-H-BB2」 · 「A3-W-BB2」 = 앞의 관리 번호를 떼고 「BB-2, …」
|
||||
)
|
||||
if k not in self.asc:
|
||||
return None, "규격 없음(원문 규격이 조달청에 없음)"
|
||||
@@ -480,6 +481,8 @@ def refill(rows: list[dict], raw: dict) -> list[dict]:
|
||||
): # 지난번에 못 채운 줄도 다시 시도
|
||||
continue
|
||||
r, why = m.find(x)
|
||||
if r is None: # 못 채운 줄은 규격 글자를 느슨하게 다시 봄 — 다산 옛 값 ±5% 안일 때만 채움
|
||||
r, why = slow.find(x, m, why, pick, places, PROVINCE)
|
||||
out.append(
|
||||
{
|
||||
"키": x["키"],
|
||||
@@ -541,10 +544,12 @@ def decide(x: dict, r: dict, day: str) -> tuple[int | None, str]:
|
||||
if r["새값"] is None:
|
||||
return None, f"관급: 조달청 값 못 채움 — {r['까닭']} ({day})"
|
||||
c = r["조달청"]
|
||||
who = f" {c['제조사']} 최저" if "최저" in r["까닭"] else ""
|
||||
loose = r["까닭"].startswith("느슨") # 느슨한 짝 — 제조사와 까닭을 함께 적음
|
||||
who = f" {c['제조사']}" if loose else (f" {c['제조사']} 최저" if "최저" in r["까닭"] else "")
|
||||
who += " · 묶음 시군 일부만 조달청에 있음" if "지역 일부" in r["까닭"] else ""
|
||||
tail = f" · {r['까닭']}" if loose else ""
|
||||
return r["새값"], (
|
||||
f"관급: 조달청 {METHOD.get(c['방식'], c['방식'])}{who} {day} · 계약 {c['계약시작']}~ · {c['지역']} · {c['인도']} · {c['부가세']}"
|
||||
f"관급: 조달청 {METHOD.get(c['방식'], c['방식'])}{who} {day} · 계약 {c['계약시작']}~ · {c['지역']} · {c['인도']} · {c['부가세']}{tail}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -42,23 +42,35 @@ FAMILIES = (
|
||||
# 조달청에 그 품목·규격이 없거나 다산 규격으로는 물건을 가릴 수 없는 것 — 「관급」 못 채움의 까닭 글
|
||||
ABSENT = (
|
||||
(
|
||||
r"^(벤치플륨관뚜껑|무게수로관뚜껑)",
|
||||
"짝 못 지음 — 조달청 뚜껑은 (부품) 치수 표기가 달라 다산 규격과 안 맞음",
|
||||
r"^벤치플륨관뚜껑",
|
||||
"짝 못 지음 — 조달청 뚜껑은 (부품) 표기라 다산 규격과 안 맞음",
|
||||
),
|
||||
(r"^HI-VP", "조달청 종합쇼핑몰에 없음 — 삼중벽 품명 없음(단층 HIVG 만 있음)"),
|
||||
(r"^KP 접합부속", "조달청 종합쇼핑몰에 없음 — 접합부속 세트 품명 없음"),
|
||||
(
|
||||
r"^소형고압블록",
|
||||
"조달청 소형고압블록은 제조사 몇 줄뿐이라 다산의 색 · 지역 묶음 줄이 없음",
|
||||
),
|
||||
(
|
||||
r"^생태(옹벽|어소)블록",
|
||||
"조달청에 그 품명이 없음 — 호안 · 옹벽블록은 모양 · 단위(㎡)가 달라 안 맞음",
|
||||
),
|
||||
(r"^합성목재\(본우드\)", "조달청 본우드 줄과 규격(치수 · 형)이 안 맞음"),
|
||||
(r"^콘크리트보강섬유\(슈퍼셀\)", "조달청에 셀룰로오스 보강섬유 품명이 없음"),
|
||||
(r"^KP 접합부속", "조달청에 접합부속 세트 품명이 없음(고무링 · 압륜 부품만 따로 있음)"),
|
||||
(
|
||||
r"^(PE직관\(수도용\)|PE이중벽)",
|
||||
"짝 못 지음 — 조달청은 압력·강성 등급별 값인데 다산 규격에 등급 없음",
|
||||
),
|
||||
(r"^VR관", "조달청 종합쇼핑몰에 없음 — VR관 품명 없음"),
|
||||
(
|
||||
r"^(순환)?아스콘.*제외",
|
||||
"짝 못 지음 — 다산 묶음이 「~구 제외」 꼴이라 조달청 공급 지역과 안 맞음",
|
||||
),
|
||||
(r"^측구수로관", "짝 못 지음 — 조달청은 길이·형별 표기(가로×세로×길이)"),
|
||||
)
|
||||
GONE = ("후보 없음", "강도 없음(원문 규격이 조달청에 없음)", "규격 없음(원문 규격이 조달청에 없음)")
|
||||
GONE = {
|
||||
"후보 없음": "조달청에 이 품목 줄이 없음(후보 없음)",
|
||||
"강도 없음(원문 규격이 조달청에 없음)": "조달청에 이 강도 줄이 없음(강도 없음)",
|
||||
"규격 없음(원문 규격이 조달청에 없음)": "조달청에 같은 품명은 있으나 이 규격 줄이 없음",
|
||||
}
|
||||
|
||||
|
||||
def absent(name: str, why: str) -> str:
|
||||
@@ -67,8 +79,8 @@ def absent(name: str, why: str) -> str:
|
||||
if re.search(pat, name):
|
||||
return text
|
||||
if why == "지역 못 찾음":
|
||||
return "조달청 종합쇼핑몰에 그 지역 묶음의 줄 없음 — 지역 못 찾음"
|
||||
return f"조달청 종합쇼핑몰에 없음 — {why}" if why in GONE else why
|
||||
return "조달청에 그 지역 묶음의 줄이 없음(지역 못 찾음)"
|
||||
return GONE.get(why, why)
|
||||
|
||||
|
||||
SKIP_WORDS = {"관급", "포장품", "", "각종"}
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
"""관급 단가 — 느슨한 두 번째 짝짓기(`update_관급단가.py` 가 엄격한 짝이 안 서면 부름).
|
||||
|
||||
엄격한 짝짓기가 못 세운 줄만 다시 봄 — 같은 갈래 안에서 규격 글자를 느슨하게 맞춰 후보를 찾고,
|
||||
**값이 잡히면 다산소프트 옛 값과 ±5% 안일 때만** 채움(밖이면 안 채움 · 까닭에 조달청 값).
|
||||
· 후보가 같은 규격 여럿이면 최저가(제조사 비고) — 최저가가 ±5% 밖이고 후보가 등급·형이 달라
|
||||
글자로 못 가릴 때만 다산 값에 가장 가까운 줄(비고에 「느슨」 으로 밝힘)
|
||||
· 다산소프트 옛 값 = 원문(estx) Cost1 · 키 → 원문번호는 `_키대장.json` — 자재품목 정본엔 이미 없음
|
||||
갈래: H형강 · 아스콘 · 돌망태·개비온 · 관(HI-VP · PE 이중벽 · PE 수도용 · VR관 · 유공관)
|
||||
· 무게수로관 뚜껑 · 벤치플룸 · 파형강관 이음관.
|
||||
"""
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
PCT = 0.05 # 다산 옛 값과 이 안이면 「맞음」
|
||||
SPREAD = 0.01 # 후보 묶음마다 값이 이 안이면 같은 값으로 봄
|
||||
|
||||
|
||||
def within(new, old) -> bool:
|
||||
return bool(old) and abs(new / old - 1) <= PCT
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _dasan() -> dict[str, int]:
|
||||
"""{자재품목 키: 다산소프트 관급 값} — 원문 estx 에서 읽음(못 채운 줄을 다시 볼 때만 부름)."""
|
||||
import compare_estx as ce # noqa: PLC0415 — 무거운 불러오기는 필요할 때만
|
||||
import master_keys as mk # noqa: PLC0415
|
||||
|
||||
book = mk.load_book()["키"]
|
||||
cost = {r["Code"]: r.get("Cost1") for r in ce.Estx().table("StuffcDat") if r.get("Code")}
|
||||
return {
|
||||
k: int(cost[v["원문번호"]])
|
||||
for k, v in book.items()
|
||||
if v["파일"] == "재료_자재품목.json" and cost.get(v["원문번호"])
|
||||
}
|
||||
|
||||
|
||||
def old_of(key: str) -> int | None:
|
||||
return _dasan().get(key)
|
||||
|
||||
|
||||
def _price(r: dict) -> int:
|
||||
return int(r["cntrctPrceAmt"])
|
||||
|
||||
|
||||
def choose(cands: list[dict], old, pick, why: str = ""):
|
||||
"""(줄, 까닭) — 최저가가 ±5% 안이면 그 줄 · 밖이면 가장 가까운 줄(±5% 안) · 아니면 못 채움."""
|
||||
if not cands:
|
||||
return None, "후보 없음"
|
||||
prices = sorted({_price(r) for r in cands})
|
||||
low, high = prices[0], prices[-1]
|
||||
if within(low, old):
|
||||
r, how = pick(cands)
|
||||
return r, f"느슨 · {why}{how}"
|
||||
near = min(cands, key=lambda r: (abs(_price(r) - old), _price(r)))
|
||||
if within(_price(near), old):
|
||||
return near, (
|
||||
f"느슨 · {why}값이 {low:,}~{high:,} 로 갈려 최저가는 다산과 5% 넘게 다름"
|
||||
" — 다산 값에 가장 가까운 줄"
|
||||
)
|
||||
return None, f"규격은 맞으나 조달청 값({low:,}~{high:,})이 다산 값과 5% 넘게 다름"
|
||||
|
||||
|
||||
# ── H형강 — 치수가 조달청에 없어도 강종별 톤당 단가가 모든 치수에서 같음 ────────────────
|
||||
def hbeam(row: dict, m, old, pick):
|
||||
g = re.match(r"(SS\d+|SM\d+|SHN\d+)", row["규격"])
|
||||
rows = [
|
||||
r
|
||||
for r in m.hbeam
|
||||
if g and g.group(1) in r["prdctSpecNm"] and r["prdctSplyRgnNm"].startswith("전지역")
|
||||
]
|
||||
if not rows:
|
||||
return None, "조달청에 이 강종의 H형강 줄이 없음"
|
||||
prices = {_price(r) for r in rows}
|
||||
makers = {r["cntrctCorpNm"] for r in rows}
|
||||
if len(prices) > len(makers): # 제조사마다 값이 하나뿐이어야 치수와 무관한 값
|
||||
return None, "조달청은 치수마다 톤당 단가가 달라 치수 없이는 못 정함"
|
||||
got = [r for r in rows if _price(r) == min(prices)]
|
||||
return choose(
|
||||
got, old, pick, "치수는 조달청에 없으나 이 강종은 모든 치수의 톤당 단가가 같음 · "
|
||||
)
|
||||
|
||||
|
||||
def _out(text: str) -> set[str]:
|
||||
"""「(강북,노원,도봉구 제외)」 — 묶음에서 뺀 시·군·구 낱말(끝 글자 구·시·군은 뗌)."""
|
||||
return {
|
||||
w[:-1] if len(w) > 2 and w[-1] in "구시군" else w
|
||||
for grp in re.findall(r"\(([^()]*?)제외\)", text or "")
|
||||
for w in re.findall(r"[가-힣]+", grp)
|
||||
}
|
||||
|
||||
|
||||
# ── 아스콘 — 묶음 이름의 「~구 제외」 · 줄임 글을 도 이름으로 맞춤 ────────────────────
|
||||
def asphalt(row: dict, m, old, pick, places, province):
|
||||
name = row["이름"]
|
||||
key = (
|
||||
name.startswith("순환"),
|
||||
re.sub(
|
||||
r"\s+",
|
||||
"",
|
||||
re.sub(
|
||||
r"^\s*[A-Z]\d-[HW]-[^,]*,",
|
||||
"",
|
||||
re.split(r"조합공통품목(?:\([^)]*\))?,", row["규격"])[-1],
|
||||
),
|
||||
),
|
||||
)
|
||||
table = m.asc.get(key)
|
||||
if not table:
|
||||
return None, "이 배합(가열 · 중온 · 등급)의 아스콘 줄이 조달청에 없음"
|
||||
prov = places(name.split(")-", 1)[1] if ")-" in name else name) & province
|
||||
got = [r for reg, rows in table.items() if prov and prov <= reg for r in rows]
|
||||
if not got:
|
||||
return None, f"조달청에 그 지역의 {key[1].split(',')[1]} 아스콘 줄이 없음(일부 지역만 있음)"
|
||||
out = _out(name)
|
||||
if out: # 「~구 제외」 묶음 — 조달청 묶음의 제외 목록이 다산이 뺀 곳을 포함하는 줄
|
||||
near = [r for r in got if out <= _out(r["prdctSplyRgnNm"])]
|
||||
if near:
|
||||
return choose(near, old, pick, "다산이 뺀 구가 조달청 묶음의 제외 목록에 듦 · ")
|
||||
prices = sorted({_price(r) for r in got})
|
||||
if prices[-1] / prices[0] - 1 > SPREAD:
|
||||
return (
|
||||
None,
|
||||
f"그 도의 조달청 묶음마다 값이 갈리고({prices[0]:,}~{prices[-1]:,})"
|
||||
" 제외 목록도 안 맞음",
|
||||
)
|
||||
return choose(got, old, pick, "묶음의 「제외」 · 줄임 글을 도 이름으로 맞춤 · ")
|
||||
|
||||
|
||||
# ── 돌망태 · 개비온 — 치수 표기(m · cm) 달라도 mm 로 맞춤 ─────────────────────────────
|
||||
def _dims(spec: str):
|
||||
"""규격의 치수 셋(또는 둘) → mm 후보 글 목록."""
|
||||
m = re.match(r"\s*([\d.]+)L×([\d.]+)W×([\d.]+)H", spec)
|
||||
if m: # 1.5L×1.0W×1.0H (m)
|
||||
return [[str(round(float(x) * 1000)) for x in m.groups()]]
|
||||
m = re.search(r"\((\d+)×(\d+)\)", spec) # #8(45×45) (cm)
|
||||
if m:
|
||||
return [[str(int(x) * 10) for x in m.groups()]]
|
||||
return []
|
||||
|
||||
|
||||
def gabion(row: dict, raw: dict, old, pick):
|
||||
n, spec = row["이름"], row["규격"]
|
||||
dims = _dims(spec)
|
||||
if not dims:
|
||||
return None, "규격 치수를 못 읽음"
|
||||
d = dims[0]
|
||||
form = re.match(r"(사각|육각|원형|타원형|이불형)", n)
|
||||
part = re.search(r"\((뚜껑|몸통)\)", n)
|
||||
wire = re.search(r"\(([\d.]+)mm", n)
|
||||
rows = [r for f in ("MASCntrct_돌망태", "ThptyUcntrct_돌망태") for r in raw.get(f, [])]
|
||||
pat = "×".join(d)
|
||||
got = []
|
||||
for r in rows:
|
||||
s = re.sub(r"\s+", "", r["prdctSpecNm"])
|
||||
if not re.search(rf"(?<!\d){pat}(?!\d)", s) or re.search(r"매트리스|식생", s):
|
||||
continue
|
||||
if form and not (form.group(1) in s or (form.group(1) + "형") in s):
|
||||
continue
|
||||
if part and part.group(1) not in s:
|
||||
continue
|
||||
if wire and f"Φ{wire.group(1)}" not in s:
|
||||
continue
|
||||
if not part and "-" in s.split(",")[-1] and re.search(r"뚜껑|몸통", s):
|
||||
continue
|
||||
got.append(r)
|
||||
return choose(got, old, pick, "치수 표기(m · cm)를 mm 로 맞춤 · ")
|
||||
|
||||
|
||||
# ── 관 — 압력 · 강성 등급이 규격에 없어 같은 지름 후보를 함께 봄 ─────────────────────
|
||||
def _per(r: dict, meters: int) -> dict:
|
||||
"""본(길이 m) 값을 다산 단위(m)로 — 줄 사본에 값을 바꿔 넣음."""
|
||||
return dict(r, cntrctPrceAmt=str(round(_price(r) / meters)), prdctUnit="m")
|
||||
|
||||
|
||||
def pipe(row: dict, raw: dict, old, pick):
|
||||
n, spec = row["이름"], row["규격"]
|
||||
pvc = raw.get("MASCntrct_경질폴리염화비닐관", []) + raw.get(
|
||||
"ThptyUcntrct_경질폴리염화비닐관", []
|
||||
)
|
||||
pe = raw.get("MASCntrct_폴리에틸렌", []) + raw.get("ThptyUcntrct_폴리에틸렌", [])
|
||||
if n.startswith("HI-VP"):
|
||||
m = re.match(r"D(\d+)mm×(\d)m", spec)
|
||||
kind = "편수칼라" if "편수칼라" in n else "직관"
|
||||
if not m:
|
||||
return None, "규격 지름을 못 읽음"
|
||||
got = [
|
||||
r
|
||||
for r in pvc
|
||||
if "HIVP" in r["prdctSpecNm"]
|
||||
and "삼중" in r["prdctSpecNm"]
|
||||
and kind in r["prdctSpecNm"]
|
||||
and f"Φ{m.group(1)}mm×{m.group(2)}m" in r["prdctSpecNm"]
|
||||
]
|
||||
return choose(got, old, pick, "압력등급 · 제조사가 달라 같은 지름 후보를 함께 봄 · ")
|
||||
if n.startswith("PE이중벽"):
|
||||
m = re.match(r"Φ(\d+)m/m", spec)
|
||||
if not m:
|
||||
return None, "규격 지름을 못 읽음"
|
||||
got = []
|
||||
for r in pe:
|
||||
s = r["prdctSpecNm"]
|
||||
k = re.search(rf"Φ{m.group(1)}mm×(\d+)m", s)
|
||||
if s.startswith("일반용폴리에틸렌관") and "이중벽관" in s and k:
|
||||
got.append(_per(r, int(k.group(1))))
|
||||
return choose(
|
||||
got, old, pick, "본 값을 m 당으로 나눔 · 강성등급이 달라 같은 지름 후보를 함께 봄 · "
|
||||
)
|
||||
if n.startswith("PE직관(수도용)"):
|
||||
kp = re.search(r"신관(\d+)×(\d)m", spec) # 「75×6m (신관90×6m) KP식」 — 6m 한 본 값
|
||||
if kp:
|
||||
got = [
|
||||
r
|
||||
for r in pe
|
||||
if r["prdctSpecNm"].startswith("수도용폴리에틸렌관")
|
||||
and f"Φ{kp.group(1)}mm×{kp.group(2)}m" in r["prdctSpecNm"]
|
||||
]
|
||||
return choose(got, old, pick, "신관 지름 · 본 길이로 맞춤 · ")
|
||||
m = re.search(r"신관(\d+)mm\)$", spec) # 「D16mm (신관20mm)」 — 조달청은 신관 지름
|
||||
if not m:
|
||||
return None, "규격에 신관 지름이 없음"
|
||||
got = [
|
||||
r
|
||||
for r in pe
|
||||
if r["prdctSpecNm"].startswith("수도용폴리에틸렌관")
|
||||
and re.search(rf"Φ{m.group(1)}mm", r["prdctSpecNm"])
|
||||
and r["prdctUnit"].lower() == "m"
|
||||
]
|
||||
return choose(
|
||||
got, old, pick, "신관 지름으로 맞춤 · 등급이 달라 같은 지름 후보를 함께 봄 · "
|
||||
)
|
||||
if n.startswith("VR관"): # VR = 진동및전압철근콘크리트관 — 조달청 「콘크리트관」 분류 안
|
||||
m = re.match(r"D=(\d+)mm", spec)
|
||||
if not m:
|
||||
return None, "규격 지름을 못 읽음"
|
||||
hume = raw.get("MASCntrct_콘크리트관", []) + raw.get("Ucntrct_콘크리트관", [])
|
||||
got = [
|
||||
r
|
||||
for r in hume
|
||||
if "진동및전압" in r["prdctSpecNm"]
|
||||
and "보통관" in r["prdctSpecNm"]
|
||||
and f"Φ{m.group(1)}×" in r["prdctSpecNm"]
|
||||
]
|
||||
return choose(got, old, pick, "VR관 = 진동및전압철근콘크리트관 · ")
|
||||
if n.startswith("파형강관(관급)"): # 유공관 — 다산 규격에 RS 가 없어 엄격한 짝이 안 섬
|
||||
dia = re.search(r"Φ(\d+)", spec)
|
||||
th = re.search(r"t(\d+(?:\.\d+)?)mm", spec)
|
||||
if not (dia and th and "유공관" in spec):
|
||||
return None, ""
|
||||
kinds = [w for w in ("아연도금", "세라믹", "PE") if w in spec]
|
||||
got = [
|
||||
r
|
||||
for r in raw.get("MASCntrct_파형강관", []) + raw.get("ThptyUcntrct_파형강관", [])
|
||||
if r["prdctSpecNm"].startswith("파형강관,")
|
||||
and f"Φ{dia.group(1)}×" in r["prdctSpecNm"]
|
||||
and re.search(rf"t{re.escape(th.group(1))}(\.0)?mm", r["prdctSpecNm"])
|
||||
and "유공관" in r["prdctSpecNm"]
|
||||
and all(w in r["prdctSpecNm"] for w in kinds)
|
||||
]
|
||||
return choose(got, old, pick, "다산 규격에 없는 RS 표기를 뺌 · ")
|
||||
return None, ""
|
||||
|
||||
|
||||
# ── 무게수로관 뚜껑 · 벤치플룸 · 파형강관 이음관 — 조달청 표기(모델 · 치수 순서 · 피복)를 맞춤 ──
|
||||
def lid(row: dict, raw: dict, old, pick):
|
||||
m = re.match(r"(M-\w+)\((\d+)\*(\d+)\*(\d+)\)", row["규격"])
|
||||
if not m:
|
||||
return None, "규격 모델 · 치수를 못 읽음"
|
||||
want = sorted(
|
||||
int(x) for x in m.groups()[1:]
|
||||
) # 다산 가로*길이*높이 — 조달청은 가로×높이×길이 순
|
||||
got = []
|
||||
for f in ("MASCntrct_배수로", "ThptyUcntrct_배수로"):
|
||||
for r in raw.get(f, []):
|
||||
s = r["prdctSpecNm"]
|
||||
if "수로관뚜껑" not in s or not re.search(rf"(?<![\w-]){re.escape(m.group(1))},", s):
|
||||
continue
|
||||
d = re.search(r"(\d+)×(\d+)×(\d+)mm", s)
|
||||
have = sorted(int(x) for x in d.groups()) if d else []
|
||||
if have and all(
|
||||
abs(a / b - 1) <= 0.03 for a, b in zip(have, want)
|
||||
): # 모델 같고 치수 3% 안
|
||||
got.append(r)
|
||||
return choose(
|
||||
got, old, pick, "조달청은 (부품) 표기 · 치수 순서가 달라 모델과 치수(3% 안)로 맞춤 · "
|
||||
)
|
||||
|
||||
|
||||
def flume(row: dict, raw: dict, old, pick, places, province):
|
||||
m = re.search(r"\((\d+)\*(\d+)\)", row["규격"])
|
||||
jong = re.search(r"\((\d)종\)", row["이름"])
|
||||
if not (m and jong and ")-" in row["이름"]):
|
||||
return None, "규격 치수 · 종별 · 지역을 못 읽음"
|
||||
tail = row["이름"].split(")-", 1)[1]
|
||||
want = places(tail)
|
||||
cut = frozenset(y for x in want for i in range(0, len(x), 2) for y in places(x[i : i + 2]))
|
||||
want = cut if want and not want & province and cut <= province else want
|
||||
got = []
|
||||
for r in raw.get("MASCntrct_플룸", []):
|
||||
s = r["prdctSpecNm"]
|
||||
if (
|
||||
"(부품)" in s
|
||||
or f"{m.group(1)}×{m.group(2)}×2000mm" not in s
|
||||
or f"{jong.group(1)}종" not in s
|
||||
):
|
||||
continue
|
||||
reg = r["prdctSplyRgnNm"]
|
||||
if reg.startswith("전지역") or (want and want <= places(reg)):
|
||||
got.append(r)
|
||||
return choose(got, old, pick, "다산 모델 글자(400A · 500C …)가 달라 치수와 종별로 맞춤 · ")
|
||||
|
||||
|
||||
def joint(row: dict, raw: dict, old, pick):
|
||||
spec = row["규격"]
|
||||
kind = re.match(r"(커플링밴드|접속티)", spec)
|
||||
dia = re.search(r"Φ(\d+)mm", spec)
|
||||
coat = re.search(r"(양면|내면|단면)피복", spec)
|
||||
if not (kind and dia and coat):
|
||||
return None, "규격 종류 · 지름 · 피복을 못 읽음"
|
||||
got = [
|
||||
r
|
||||
for f in ("MASCntrct_파형강관이음관", "ThptyUcntrct_파형강관이음관", "MASCntrct_파형강관")
|
||||
for r in raw.get(f, [])
|
||||
if r["prdctSpecNm"].startswith("파형강관이음관")
|
||||
and kind.group(1) in r["prdctSpecNm"]
|
||||
and f"Φ{dia.group(1)}mm" in r["prdctSpecNm"]
|
||||
and coat.group(0) in r["prdctSpecNm"]
|
||||
and not re.search(r"세라믹|아연도금", r["prdctSpecNm"])
|
||||
]
|
||||
return choose(got, old, pick, "조달청은 「PE」 를 빼고 적기도 함 · (부품) 표기 포함 · ")
|
||||
|
||||
|
||||
FAMILIES = (
|
||||
("H형강", r"^H 형강\(관급\)"),
|
||||
("아스콘", r"^(순환)?아스콘\(관급\)-"),
|
||||
("돌망태", r"(돌망태|개비온)"),
|
||||
("관", r"^(HI-VP|PE이중벽|PE직관\(수도용\)|VR관|파형강관\(관급\))"),
|
||||
("뚜껑", r"^무게수로관뚜껑"),
|
||||
("벤치플룸", r"^벤치플륨관\(\d종\)\(관급\)-"),
|
||||
("이음관", r"^파형강관이음관\(관급\)"),
|
||||
)
|
||||
|
||||
|
||||
def find(row: dict, m, why: str, pick, places, province, old=None):
|
||||
"""(조달청 줄, 까닭) — 엄격한 짝이 안 선 줄(`why`)을 다시 봄 · 갈래 밖이면 (None, why)."""
|
||||
fam = next((k for k, pat in FAMILIES if re.search(pat, row["이름"])), None)
|
||||
if fam is None:
|
||||
return None, why
|
||||
old = old if old is not None else old_of(row["키"])
|
||||
if not old:
|
||||
return None, why
|
||||
if fam == "H형강":
|
||||
r, how = hbeam(row, m, old, pick)
|
||||
elif fam == "아스콘":
|
||||
r, how = asphalt(row, m, old, pick, places, province)
|
||||
elif fam == "돌망태":
|
||||
r, how = gabion(row, m.raw, old, pick)
|
||||
elif fam == "뚜껑":
|
||||
r, how = lid(row, m.raw, old, pick)
|
||||
elif fam == "벤치플룸":
|
||||
r, how = flume(row, m.raw, old, pick, places, province)
|
||||
elif fam == "이음관":
|
||||
r, how = joint(row, m.raw, old, pick)
|
||||
else:
|
||||
r, how = pipe(row, m.raw, old, pick)
|
||||
return (r, how) if r is not None or how else (None, why)
|
||||
Reference in New Issue
Block a user