Merge remote-tracking branch 'origin/dev' into sub_laptop_1

This commit is contained in:
2026-09-20 17:41:55 +09:00
4 changed files with 230 additions and 88 deletions
@@ -53,7 +53,10 @@ KEEP = (
def call(op: str, key: str, **kw) -> dict:
q = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in kw.items())
url = f"{BASE}{op}?serviceKey={key}&type=json&{q}"
return json.loads(cc.fetch(url, timeout=90).decode("utf-8", "replace"))["response"]
try:
return json.loads(cc.fetch(url, timeout=90).decode("utf-8", "replace"))["response"]
except Exception as e: # 오류 글에 받은 주소(키 포함)가 실려도 키는 가림
raise RuntimeError(f"{op} 호출 실패: {str(e).replace(key, '***')}") from None
def fetch_all() -> Path:
@@ -152,6 +155,9 @@ def strength(text: str):
return tuple(int(x) for x in m.groups()) if m else None
PROVINCE = frozenset(w for k, v in ALIAS.items() for w in places(k) | places(v))
def region_pick(cands: dict, want: frozenset):
"""cands = {지명묶음: 줄} — 같은 묶음 → 겹침 가장 큰 하나(동점이면 None)."""
if want in cands:
@@ -159,6 +165,12 @@ def region_pick(cands: dict, want: frozenset):
best = sorted(((len(want & k) / len(want | k), k) for k in cands if want & k), reverse=True)
if best and best[0][0] >= 0.5 and (len(best) == 1 or best[0][0] > best[1][0]):
return cands[best[0][1]], f"지역 비슷함 {best[0][0]:.2f}"
# 다산 묶음이 조달청 시군 줄 여럿을 합친 것 — 묶음 안 시군마다 조달청 줄이 있을 때만(값이 갈리면 pick 이 보류)
towns = want - PROVINCE
if len(towns) > 1:
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)}"
return None, "지역 못 찾음"
@@ -232,8 +244,10 @@ 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:
return None, why
if cands is None or (
why.startswith("지역 묶음") and "제외" in name
): # 「~ 제외」 묶음은 짝짓지 않음
return None, why if cands is None else "지역 못 찾음"
corp = re.findall(
r"([가-힣]+)레미콘\(협\)", name
) # 이름에 조합이 붙은 줄(「제주…레미콘(협)」)은 그 조합 줄만
@@ -253,7 +267,10 @@ class Matcher:
r, how = pick(c)
return r, f"{why} · {how}"
if re.match(r"(순환)?아스콘\(관급\)-", n):
k = (n.startswith("순환"), re.sub(r"\s+", "", spec.split("조합공통품목,")[-1]))
k = (
n.startswith("순환"),
re.sub(r"\s+", "", re.split(r"조합공통품목(?:\([^)]*\))?,", spec)[-1]),
)
if k not in self.asc:
return None, "규격 없음(원문 규격이 조달청에 없음)"
c, why = self._region(self.asc[k], n)
@@ -339,7 +356,9 @@ def refill(rows: list[dict], raw: dict) -> list[dict]:
"""조달 값(관급 열)이 있는 줄마다 조달청 값으로 다시 채운 결과."""
m, out = Matcher(raw), []
for x in rows:
if x["관급"] is None:
if x["관급"] is None and not (x["비고"] or "").startswith(
"관급:"
): # 지난번에 못 채운 줄도 다시 시도
continue
r, why = m.find(x)
out.append(