"""M02 일위대가 맞춤 — 블록 하나 셈 · 판정(PLAN 338-1 · 설계 2-1 · 2-6 · 3-2). 블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) · 같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve` · 근거 안 닿은 덧 = 후보 끝) → 위 다섯 옵션 거꾸로(`Option`) → 위 다섯 가운데 딱 맞음(옵션까지)이 있는데 1위가 아니면 그것이 1위(조합 없는 줄) → 판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표). 글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음. """ from __future__ import annotations from typing import Any from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis from M02_MasterTemplete import M02_Hopyo_Combine as combine from M02_MasterTemplete import M02_Hopyo_Finger as finger from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match from M02_MasterTemplete import M02_Hopyo_Option as option def _num(x: float) -> str: return f"{x:.4g}" def excel_qty(efp: dict[str, Any]) -> dict[str, Any]: """엑셀 품(화면 꼴) — {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}.""" return { "인력": {k: round(x["품"], 6) for k, x in efp["인력"].items() if x.get("품")}, "장비": [{"갈래": m["갈래"], "시간": m["시간"]} for m in efp["장비"] if m.get("시간")], "자재": [{"이름": m["이름"], "수량": m["품"]} for m in efp["자재"] if m.get("품")], } def _public(efp: dict[str, Any]) -> dict[str, Any]: keep = ("인력", "장비", "자재", "출처", "조건", "옛판", "율", "계수") return {k: efp.get(k) for k in keep} def _label(code: str, values: dict[str, Any]) -> str: """옵션 요약 — 고르기 글은 그대로 · 수 칸은 「칸 값」 · 기계 코드는 고르기이름.""" specs = {s.get("이름"): s for s in hopyo_match._logic_specs(code)} out = [] for name, v in values.items(): names = (specs.get(name) or {}).get("고르기이름") or {} out.append(str(names.get(v) or v) if isinstance(v, str) else f"{name} {v}") return " · ".join(out) def _part_fp(part: dict[str, Any], x: float, line: dict[str, Any]) -> dict[str, Any]: """조합 몫 → 옵션 찾기용 지문(몫 x 를 풀어 100% 품).""" fp = finger.empty(line) k = x or 1.0 for t, q in (part.get("노임") or {}).items(): fp["인력"][t] = {"품": q / k, "밑품": q / k, "율": [], "어떻게": ["몫"]} return fp def _diffs(efp: dict, found: dict) -> list[str]: """숫자 사실 꼬리표 — 직종 품 다름 · 섞음 · 율 · 엑셀만 · 우리만 장비.""" tags = [] ours_l = (found.get("우리품") or {}).get("인력") or {} for t, x in efp["인력"].items(): a, b = x.get("품"), ours_l.get(t) if a and b and not option.near(a, b, fit.TOL[2]): tags.append(f"{t} 엑셀 {_num(a)} · 우리 {_num(b)}") elif a and not b: tags.append(f"{t} 엑셀만 {_num(a)}") tags += [f"{t} 우리만 {_num(q)}" for t, q in ours_l.items() if t not in efp["인력"]] for x in efp["인력"].values(): for v, note in x.get("율") or []: tag = f"섞음 — 엑셀이 × {_num(v)}" if note == "섞음" else f"율 {note} × {_num(v)}" if v and tag not in tags: tags.append(tag) tags += [d for d in ((found.get("견줌") or {}).get("덧") or []) if d not in tags] return tags def _state(efp: dict, code: str, found: dict | None, theirs: list) -> str: lvl = 9 if (found is None or fit.single(efp)) else found["수준"] if lvl <= 1: return "맞음" our = hopyo_basis.ours().get(code) clause_ok = bool(our and theirs and hopyo_match.clause(theirs, our)[0] >= 0.9) if lvl == 2 or (found and found.get("직종같음") and clause_ok): return "다른 점" return "후보" def _no_numbers(entry: dict, top: list[str], why: str) -> dict[str, Any]: entry["순위"] = [{"로직": k, "점수": None, "B": i} for i, k in enumerate(top[: fit.TOP_OPT])] entry["차례"] = top[: fit.TOP_OPT] entry["판정"] = {"상태": "후보", "글": why, "꼬리표": [why]} return entry def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dict[str, Any]: """블록(또는 `{_지문}` 직접 줄 호표) + 줄 → 맞춤셈 한 칸(설계 3-2).""" if block_ and "_지문" in block_: efp, text = block_["_지문"], [] else: efp, text = finger.excel(block_, line), (block_ or {}).get("글") or [] border = hopyo_match.b_order(line, got, text) entry: dict[str, Any] = {"지문": _public(efp), "품": excel_qty(efp), "B": border["상태"]} entry.update(옵션={}, 조합=None) if not fit.has_numbers(efp): return _no_numbers(entry, border["top"], fit.NO_NUMBER) if fit.prepared() is None: return _no_numbers(entry, border["top"], "우리 로직 준비 중") e = fit.ex_prep(efp) pool = fit.rank(e) btop = border["top"] scores = {k: s for s, k, _, _ in pool} fused = fit.fuse(e, pool, btop) if border["상태"] == "같음": # 같음 1위 고정 — 지문 딱 맞음이 다른 로직이면 지문 1위 first = pool[0][1] if fit.sure(e, pool) and pool[0][1] != btop[0] else btop[0] top = [first] + [k for k in fused if k != first][:4] else: top = fused entry["지문차례"] = top[: fit.TOP_OPT] # 조합 · 딱 맞음 먼저 전(시험 지표) parts = efp.get("몫") or [] if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저 top = btop[: fit.TOP_OPT] top, combo, spare = combine.solve(top, parts, line, got, border["합침"]) opts = {k: option.find(k, efp, text) for k in top[: fit.TOP_OPT]} for c in combo: if c["로직"] not in opts and c["체크"]: part = next((p for p in parts if p.get("제목") == c["어디"]), None) pfp = _part_fp(part, c["몫"], line) if part else efp opts[c["로직"]] = option.find(c["로직"], pfp, text) if pfp["인력"] else None theirs = efp["출처"] exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"] if ( not combo and exact and exact[0] != top[0] ): # 딱 맞음 먼저(K5) — 1위가 못 맞추면 위 다섯 딱 맞음 top = [exact[0]] + [k for k in top if k != exact[0]] chans = {k: ch for _, k, ch, _ in pool} entry["순위"] = [ { "로직": k, "점수": round(scores[k], 3) if k in scores else None, "채널": {a: round(b, 3) for a, b in (chans.get(k) or {}).items() if b is not None}, "B": btop.index(k) if k in btop else None, } for k in top[: fit.TOP_OPT] ] entry["차례"] = top[: fit.TOP_OPT] entry["옵션"] = opts entry["조합"] = combo or None entry["판정"] = judge(efp, top, opts, theirs, got) entry["후보"] = candidates(efp, top, opts, theirs, got, scores, border.get("근거") or {}) names = {x["로직"]: x for x in got["로직"]} for k in (k for k in spare if k not in top[: fit.TOP_OPT] and k in names): # 근거 안 닿은 덧 one = {key: names[k][key] for key in ("로직", "이름", "단위")} entry["후보"].append({**one, "점수": None, "판정": "후보", "글": "덧 후보 — 근거 안 닿음"}) return entry def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dict[str, Any]: """1위 판정 {상태, 글, 꼬리표}.""" if not top: return {"상태": "후보", "글": "후보 없음", "꼬리표": []} names = {x["로직"]: x["이름"] for x in got["로직"]} t1, found = top[0], opts.get(top[0]) or {} state = _state(efp, t1, opts.get(t1), theirs) exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"] tags = [] if state == "맞음": label = _label(t1, found.get("입력") or {}) text = " · ".join(x for x in ("맞음 — " + t1, names.get(t1, ""), label) if x) if len(exact) > 1: tags.append("맞음 둘") if found.get("갈리는칸"): tags.append(f"{' · '.join(found['갈리는칸'])} 못 가름") return {"상태": state, "글": text, "꼬리표": tags} if state == "다른 점": tags = _diffs(efp, found) return {"상태": state, "글": " · ".join(["다른 점 — " + t1, *tags[:2]]), "꼬리표": tags} return {"상태": "후보", "글": f"후보 {len(top[: fit.TOP_OPT])}", "꼬리표": []} def candidates( efp: dict, top: list[str], opts: dict, theirs: list, got: dict, scores: dict, why: dict ) -> list[dict[str, Any]]: """후보 다섯 — 로직마다 옵션 판정 꼬리표 · 「맞음 둘」이면 둘째 맞음이 맨 앞.""" by = {x["로직"]: x for x in got["로직"]} table = hopyo_basis.ours() out = [] for k in top[: fit.TOP_OPT]: found = opts.get(k) or {} state = _state(efp, k, opts.get(k), theirs) our = table.get(k) sc, clauses, tag = hopyo_match.clause(theirs, our) if (our and theirs) else (0, [], "") one = by.get(k) or {"로직": k, "이름": "", "단위": ""} unit = hopyo_match.fits(efp["단위"], one["단위"]) basis = why.get(k) or {"조항": clauses, "직종": [], "단위": unit, "점수": sc, "까닭": tag} text = state if state != "다른 점" else " · ".join(["다른 점", *_diffs(efp, found)[:1]]) out.append( { "로직": k, "이름": one["이름"], "단위": one["단위"], "점수": round(scores[k], 3) if k in scores else None, "판정": state, "글": text, "근거": basis, } ) first = [c for c in out[1:] if c["판정"] == "맞음"][:1] if out and out[0]["판정"] == "맞음" and first: out = [out[0], *first, *[c for c in out[1:] if c is not first[0]]] return out