feat(m01): 로직 재료 줄 키 → 기본 검색어 바꿔 넣기 (PLAN 8-3)

- 재료 줄 591 → {구분, 상세구분, 검색어} · 재료 입력 키 목록 76 걷음 · 넘기던 인자 뗌
- 결속선 등 원문대로 고른 품목(어닐링철선)이 검색 첫째 그대로
- 1,354 자동값 금액 변화 0 · 계산전부 멈춤 0 · check_master 로직 0 · 단위 16 그대로
- 못 맞춘 26줄은 옛 대표 · 조건 그대로: 값 빈 자리표 MT0306… 14 · 같은 단위 후보 0 빈 줄 12
- 도구: 빈 줄은 안 바꿈 · 판정은 서버 찾기(master_pick.search) · 시험 test_m01_search_words

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
2026-09-24 21:05:58 +09:00
co-authored by Claude Opus 5.5
parent 1eed93bb1d
commit 3f581ca41b
38 changed files with 715 additions and 1365 deletions
@@ -26,61 +26,25 @@ import check_master as cm # noqa: E402
import master_auto as mau # noqa: E402
import master_formula as mf # noqa: E402
import master_material as mm # noqa: E402
import master_pick as mp # noqa: E402
MASTER = HERE.parent
KEY = re.compile(r"MT\d{6}")
# 같은 성격 단위(8-1 단위 환산) — `plain_unit` 뒤 모양
KINDS = ({"g", "kg", "톤"}, {"mm", "cm", "m", "km"}, {"cm2", "m2"}, {"cm3", "ℓ", "m3"})
# 이름 끝 지역 낱말(「합판(내수), 서울」) — 자동값 차례의 지역 자리
REGIONS = set(
"서울 부산 대구 인천 광주 대전 울산 세종 경기 강원 충북 충남 전북 전남 경북 경남 제주 "
"청주 전주 포항 창원 강릉 춘천 충주 고양 수원 원주 진주 목포 여수 순천 안동 구미 경주 "
"천안 군산 익산 김해 마산 성남 안양 부천 의정부".split()
)
# ── 8-1 찾기 규칙 (마른 돌리기 판정) ─────────────────────────────────────
def same_kind(a: str, b: str) -> bool:
a, b = mm.plain_unit(a), mm.plain_unit(b)
return a == b or any(a in k and b in k for k in KINDS)
# ── 판정 — 서버 찾기(`master_pick.search`) 그대로 ─────────────────────────
def _has(word: str, row: dict) -> bool:
return mm.plain(word) in mm.plain(row.get("이름")) or mm.plain_spec(word) in mm.plain_spec(
row.get("규격")
)
"""`master_pick.search` 와 같은 맞대기 — 낱말이 이름 또는 규격에 듦."""
w = mm.plain_spec(word)
return w in mm.plain(row.get("이름")).replace("×", "x") or w in mm.plain_spec(row.get("규격"))
def found(master: mf.Master, elem: dict, unit: str | None, soft: bool = True) -> list[str]:
"""검색어로 잡히는 자재품목 키 — 자동값 차례(값 있음 → 같은 단위 → 지역 낱말 → 키).
soft = 지역 낱말은 거르지 않고 차례만(아니면 여느 낱말처럼 거름)."""
words = str(elem.get("검색어") or "").split()
region = [w for w in words if soft and w in REGIONS]
must = [w for w in words if w not in region]
pool = mm.by_class(master).get(elem["구분"], {}) if elem.get("구분") else master.index["MT"]
detail = elem.get("상세구분")
hits = [
key
for key, row in pool.items()
if (not detail or row.get("상세구분") == detail)
and all(_has(w, row) for w in must)
and (not unit or same_kind(row.get("단위"), unit))
]
def rank(key):
row = master.get(key)
return (
not mm._offers(master, key),
bool(unit) and mm.plain_unit(row.get("단위")) != mm.plain_unit(unit),
-sum(_has(w, row) for w in region),
key,
)
return sorted(hits, key=rank)
def found(master: mf.Master, elem: dict, unit: str | None, env: dict) -> list[str]:
return mp.search(master, elem, env, unit)
def first(master, elem, unit, soft=True) -> str | None:
got = found(master, elem, unit, soft)
def first(master, elem, unit, env) -> str | None:
got = found(master, elem, unit, env)
return got[0] if got else None
@@ -160,14 +124,14 @@ def _tokens(row: dict) -> list[str]:
def _place(row: dict) -> str | None:
tail = _name(row.get("이름")).split(",")[-1]
return tail if tail in REGIONS else None
return tail if tail in mp.REGIONS else None
def _ok(master, elem, unit, cases) -> bool:
"""판정 — 입력 벌마다 지역 낱말을 거르든(hard) 차례로만 보든(soft) 첫째가 그 품목."""
"""판정 — 입력 벌마다 서버 찾기 첫째가 지금 품목."""
for env, want in cases:
filled = {k: mf.fill(v, env) if isinstance(v, str) else v for k, v in elem.items()}
if any(first(master, filled, unit, soft) != want for soft in (True, False)):
if first(master, filled, unit, env) != want:
return False
return True
@@ -182,10 +146,9 @@ def words_for(master, base: dict, words: list[str], unit, cases) -> list[str] |
if len(target_rows) != 1 or None in target_rows:
return None # 입력 따라 품목이 갈리는 줄 — 끼운 낱말로만
want = master.get(next(iter(target_rows)))
ahead = []
for soft in (True, False):
got = found(master, {**elem}, unit, soft)
ahead += got[: got.index(want["키"])] if want["키"] in got else []
env = cases[0][0]
got = found(master, {k: mf.fill(v, env) for k, v in elem.items()}, unit, env)
ahead = got[: got.index(want["키"])] if want["키"] in got else got
pick = None
for w in _tokens(want):
if w in words or not _has(w, want):
@@ -209,11 +172,11 @@ def convert(master: mf.Master, row: dict, item: dict) -> tuple[dict | None, list
# 이미 좁혀 둔 이름·규격 낱말은 그대로 검색어로(`{관경}` 같은 끼운 칸도)
words = [w for k in ("이름", "규격") for w in str(elem.get(k) or "").split()]
targets = {want for _, want in cases}
if targets == {None}: # 지금 후보 0 인 줄 — 줄 이름 핵심으로 · 여전히 0 이어야 함
root = _root(item.get("이름"))
if root and not any(mm.plain(root) in mm.plain(w) for w in words):
words.insert(0, root)
elif len(targets) == 1:
if targets == {None}:
# 지금 같은 단위 후보 0 인 빈 줄 — 검색어는 다른 단위 품목을 잡아 값이 생기거나(금액 바뀜)
# 후보 0 검사에 걸림 · 조건 그대로 두고 목록으로 알림
return None, cases
if len(targets) == 1:
want = master.get(next(iter(targets)))
name, *spec = _core(want)
if not any(mm.plain(w) in mm.plain(name) for w in words):