feat(master_data): 미분류 이름 줄 145 → 1 · 특수장비 신설 · 관급 못 채움 346 → 320

- 화초·수생식물 규격 꼴은 식생·종자 · Air Hammer · 콘쉴드 · 바탕면조정재 · 본우드는 특수장비 · 상·하차비는 운송·용역
- 관급 지역 묶음 시군 일부만 조달청에 있는 아스콘·레미콘 26줄 채움(비고에 명시)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
2026-09-20 22:08:54 +09:00
co-authored by Claude Sonnet 5
parent e54db25302
commit b1fa3009a5
5 changed files with 206 additions and 178 deletions
@@ -481,12 +481,20 @@ R = [
("식생·종자", "수목", r"촉촉소일|나무여과상자|만병초"),
("석재", "경계석", r"빗물머금석"),
("잡재료", "소모품", r"마킹펜|크레용|세척제|WHITEPAINT|보조통|암등|이단철개|열수축시트"),
# 일감 3 — 뒤에 붙여 옛 결과는 그대로
("운송·용역", "상하차", r"^(상|하)차비$"),
("식생·종자", "초화·수생식물", r"^칡$"),
("특수장비", "특수장비", r"AirHammer|콘쉴드|바탕면조정재|본우드"),
]
RX = [(a, b, re.compile(c, re.I)) for a, b, c in R]
def 분류(이름: str, 출처: str):
# 화초·수생식물 줄 — 규격이 분·포트·cm·분얼·개화구·엽·년생 꼴 (이름 글자로는 못 가림)
식물규격 = re.compile(r"^\d+(\.\d+)?cm$|포트|분얼|개화구|\d엽$|년생|\d+cm분$")
def 분류(이름: str, 출처: str, 규격: str = ""):
n = re.sub(r"\s+", "", 이름 or "")
if 출처.startswith("자재품목 조달청"):
return 조달청_분류.get(n, ("미분류", None))
@@ -495,6 +503,8 @@ def 분류(이름: str, 출처: str):
for a, b, rx in RX:
if rx.search(n):
return (a, b)
if 규격 and 식물규격.search(규격.replace(" ", "")):
return ("식생·종자", "초화·수생식물")
return ("미분류", None)
@@ -519,7 +529,8 @@ def main():
if m and '"": "MT' in ln:
nm = json.loads('"' + NAME.search(ln).group(1) + '"')
sc = json.loads('"' + SRC_.search(ln).group(1) + '"')
a, b = 분류(nm, sc)
sp = re.search(r'"규격": "((?:[^"\\]|\\.)*)"', ln)
a, b = 분류(nm, sc, json.loads('"' + sp.group(1) + '"') if sp else "")
ln = LINE.sub(lambda _: f'"구분": {q(a)}, "상세구분": {q(b)}', ln, count=1)
if not nm:
ln = ln.replace('"비고": null', f'"비고": "{NOTE_EMPTY}"', 1)
@@ -221,6 +221,10 @@ def region_pick(cands: dict, want: frozenset):
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)}"
# 묶음 시군 가운데 일부만 조달청 줄이 있음 — 그 시군 줄만(같은 도 · 묶음 밖 시군이 낀 줄은 뺌)
part = [k for k in cands if towns & k and k <= want | PROVINCE and k - PROVINCE <= want]
if len(towns) > 1 and part:
return [r for k in part for r in cands[k]], f"지역 일부 {len(part)}"
return None, "지역 못 찾음"
@@ -308,7 +312,7 @@ class Matcher:
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(
@@ -538,6 +542,7 @@ def decide(x: dict, r: dict, day: str) -> tuple[int | None, str]:
return None, f"관급: 조달청 값 못 채움 — {r['까닭']} ({day})"
c = r["조달청"]
who = f" {c['제조사']} 최저" if "최저" in r["까닭"] else ""
who += " · 묶음 시군 일부만 조달청에 있음" if "지역 일부" in r["까닭"] else ""
return r["새값"], (
f"관급: 조달청 {METHOD.get(c['방식'], c['방식'])}{who} {day} · 계약 {c['계약시작']}~ · {c['지역']} · {c['인도']} · {c['부가세']}"
)