"""M02 일위대가 실무자료 줄 맞춤 응답(PLAN 338-1 · 설계 3-3 · 3-4) — 줄 상태 · 체크 제안 · 대상 목록 · 후보 · 품 비교 · 호표 통째 · 셈 · 만든 · 미리 셈(`guess`). 상태 차례 = ① % ② 호표 참조(만들 층에 확인된 사전조합이면 체크) ③ 노임(그릇 없음) ④ 장부 「우리에 없음 · 안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(규격 다름 · 근거 다름 · 숫자 판정 맞음이 다른 로직이면 체크 안 함) ⑥ 맞춤셈 판정(맞음 = 체크 · 다른 점 · 후보) ⑦ 셈 전 · 숫자 없음 → 이름 · 근거 후보(「셈 중」 · 「숫자 판정 없음」). 자재 · 별도계상 = 이름 · 규격 같음 하나면 맞음(체크) · 그 밖 후보. 입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값. 체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독. 장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창). """ from __future__ import annotations from pathlib import Path from typing import Any from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build from M02_MasterTemplete import M02_Hopyo_Finger as finger from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_Match as hm from M02_MasterTemplete import M02_Hopyo_Option as option from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Hopyo_Verdict as verdict from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store SKIP = ("우리에 없음", "안 씀") class Ctx: """보기 한 번 — 파일 · 장부 · 맞춤셈 · 확인된 사전조합을 한 번 읽어 줄마다 넘김.""" def __init__(self, folder: Path, key: str, to: Path | None = None) -> None: self.folder, self.key = Path(folder), key self.to = Path(to or folder) self.doc = hopyo_store.read(folder, key) self.got = hm.targets() self.units = {int(h["번"]): h for h in self.doc.get("호표") or []} self.books = ledger.books(self.to) self.calc = fit.ensure(folder, key) self.cache = (fit.result(folder, key) or {}).get("블록") or {} self.done = hopyo_build.confirmed(self.to, refs.where(self.folder)) self.names = {x["로직"]: x for x in self.got["로직"]} self.mats = {x["자재"]: x for x in self.got["자재"]} def _target(code: str, line: dict, ctx: Ctx, found: dict | None, use: bool, **extra) -> dict: """대상 하나(로직) — 입력값 · 못읽음 · 출처 · 환산 · 몫 · 배.""" one = ctx.names.get(code) or {"이름": "", "단위": ""} vals, unread = hm.spec_inputs(line, hm._logic_specs(code)) src = {k: "규격" for k in vals} for k, v in ((found or {}).get("입력") or {}).items() if use else (): if str(vals.get(k)) != str(v): vals[k], src[k] = v, "블록" conv = hm.factor(line.get("단위"), one["단위"], line.get("규격")) share = extra.pop("몫", None) times = conv["배"] * share if conv["배"] is not None and share else conv["배"] head = {"로직": code, "이름": one["이름"], "단위": one["단위"], "입력값": vals} head.update(못읽음=[u for u in unread if u not in vals], 출처=src, 환산=conv) return {**head, "몫": share, "배": times, **extra} def _pair(pair: dict, line: dict, ctx: Ctx) -> dict: """장부 짝 → 대상(입력값 출처 = 별칭).""" vals = dict(pair.get("입력값") or {}) if pair.get("로직"): one = ctx.names.get(pair["로직"]) or {"이름": "", "단위": ""} head = {"로직": pair["로직"]} elif pair.get("자재"): one, head = ctx.mats.get(pair["자재"]) or {"이름": "", "단위": ""}, {"자재": pair["자재"]} else: h = ctx.units.get(int(pair.get("호표") or 0)) or {} one, head = ( {"이름": h.get("이름", ""), "단위": h.get("단위", "")}, {"호표": pair.get("호표")}, ) conv = {"배": pair.get("배"), "갈래": "별칭"} return {**head, "이름": one["이름"], "단위": one["단위"], "입력값": vals, "못읽음": []} | { "출처": {k: "별칭" for k in vals}, "환산": conv, "몫": pair.get("몫"), "배": pair.get("배"), } def _qty(entry: dict, found: dict | None) -> dict: """품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용).""" ex = entry.get("품") or {"인력": {}, "장비": [], "자재": []} us = (found or {}).get("우리품") or option.ours_qty(None) same = {} for t, a in ex["인력"].items(): b = us["인력"].get(t) same[t] = bool(b) and option.near(a, b, fit.TOL[0], option.ru(a)) for m in ex["장비"]: mine = [x["시간"] for x in us["장비"] if set(x["갈래"]) & set(m["갈래"])] hit = any(option.near(m["시간"], q, fit.TOL[0], option.ru(m["시간"])) for q in mine) same["·".join(m["갈래"])] = hit or same.get("·".join(m["갈래"]), False) return {"엑셀": ex, "우리": us, "같음": same} def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict: """⑥ 맞춤셈 판정 → 상태 · 대상 · 후보 · 품 · 글 · 꼬리표.""" judged = entry["판정"] state = judged["상태"] top = (entry.get("차례") or [None])[0] found = (entry.get("옵션") or {}).get(top) use = state in ("맞음", "다른 점") combo = [c for c in entry.get("조합") or [] if c["체크"]] combo = combo if len(combo) > 1 else [] # 체크할 로직이 하나뿐이면 조합 아님(기계 몫은 꼬리표) if combo: targets = [ _target( c["로직"], line, ctx, entry["옵션"].get(c["로직"]), use, 몫=c["몫"], 어디=c["어디"] ) for c in combo ] else: targets = [_target(top, line, ctx, found, use)] if top else [] tags = list(judged.get("꼬리표") or []) tags += [f"조합 — {' + '.join(c['로직'] for c in combo)}"] if combo else [] for c in entry.get("조합") or []: if not c["체크"]: tags.append(f"기계 몫 {c['로직']} — 시간 = 엑셀 값(우리 셈 아님)") if any("블록" in t["출처"].values() for t in targets): tags.append("옵션 — 블록에서") tags += [f"근거 {b} {n}" for b, n in (entry.get("지문") or {}).get("출처") or []] ok = all(t["배"] is not None and t["환산"]["갈래"] != "추정" for t in targets) out = {"상태": state, "체크": state == "맞음" and bool(targets) and ok, "대상": targets} out.update(후보=entry.get("후보") or [], 글=judged["글"], 꼬리표=tags, 품=_qty(entry, found)) if found and found.get("갈리는칸"): out["갈리는칸"] = found["갈리는칸"] return out def _names_only(line: dict, ctx: Ctx, why: str, full: bool) -> dict: """⑦ 숫자 판정 없음 · 셈 전 — 이름 · 근거 후보(차례만 · 대상 = 1위).""" if not full: return {"상태": "후보", "체크": False, "대상": [], "후보": [], "글": why, "꼬리표": [why]} order = hm.b_order(line, ctx.got) top = order["top"] cands = [{**c, "판정": "후보", "글": why} for c in order["후보"]] targets = [_target(top[0], line, ctx, None, False)] if top else [] state = "후보" if top else "없음" return { "상태": state, "체크": False, "대상": targets, "후보": cands, "글": why, "꼬리표": [why], } def _material(line: dict, ctx: Ctx, full: bool) -> dict: found = hm._exact(line, "자재", ctx.got) if len(found) == 1: one = found[0] conv = hm.factor(line.get("단위"), one["단위"], line.get("규격")) target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv} target.update(몫=None, 배=conv["배"]) ok = conv["배"] is not None and conv["갈래"] != "추정" return {"상태": "맞음", "체크": ok, "대상": [target], "후보": [], "글": "이름 · 규격 같음"} cands = [{**c, "판정": "후보"} for c in hm.candidates(line, "자재", ctx.got, 5)] if full else [] return { "상태": "후보" if cands or not full else "없음", "체크": False, "대상": [], "후보": cands, } def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]: """줄 → 맞춤(설계 3-3) — `full` 아니면 상태 · 체크만(호표 목록 수).""" out: dict[str, Any] = {"열쇠": hm.line_key(line), "체크": False, "대상": [], "후보": []} kind = finger.kind_of(line) ref = line.get("참조") or {} if kind == "%": return {**out, "상태": "%", "글": "% 줄 — 우리 로직 덧줄이 셈"} if kind == "호표" or ref.get("종류") == "일위": no = int(ref.get("번") or 0) h = ctx.units.get(no) or {} conv = hm.factor(line.get("단위"), h.get("단위"), line.get("규격")) made = ctx.done.get(hopyo_build.mark(ctx.key, no)) target = {"호표": no, "이름": h.get("이름", ""), "단위": h.get("단위"), "입력값": {}} target.update(못읽음=[], 출처={}, 환산=conv, 몫=None, 배=conv["배"]) ok = bool(made) and conv["배"] is not None and conv["갈래"] != "추정" text = f"확인된 사전조합 {made}" if made else f"제{no}호표 먼저 확인" return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text} if kind == "노임": return {**out, "상태": "노임", "글": "노임 · 그릇 없음"} alias = ledger.recall(line, ctx.to, ctx.books) entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None if alias and alias.get("갈래") in SKIP: return {**out, "상태": "뺌", "뺌": alias["갈래"], "별칭": _alias_info(alias, False)} if alias and alias.get("갈래") == "짝" and alias.get("짝"): return {**out, **_aliased(line, ctx, alias, entry)} if kind in hm.MATERIAL_KINDS: return {**out, **_material(line, ctx, full)} if entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"): return {**out, **(_numeric(line, ctx, entry) if full else _brief(entry))} why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER return {**out, **_names_only(line, ctx, why, full)} def _brief(entry: dict) -> dict: state = entry["판정"]["상태"] return {"상태": state, "체크": state == "맞음", "글": entry["판정"]["글"]} def _alias_info(alias: dict, basis_off: bool) -> dict: keep = ("층", "주인", "때", "누가", "처음파일", "규격다름") return {**{k: alias.get(k) for k in keep}, "근거다름": basis_off} def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict: """⑤ 장부 짝 — 체크 = 규격 같음 · 근거 같음 · 숫자 판정 맞음이 다른 로직 아님.""" targets = [_pair(p, line, ctx) for p in alias["짝"]] codes = {t["로직"] for t in targets if t.get("로직")} theirs = ((line.get("근거") or {}).get("출처")) or [] table = hopyo_basis.ours() off = bool(theirs and codes) and all( hm.clause(theirs, table[c])[0] < 0.9 for c in codes if c in table ) other = bool(entry and entry["판정"]["상태"] == "맞음" and entry["차례"][0] not in codes) check = not alias.get("규격다름") and not off and not other tags = ["별칭 — " + " · ".join(str(x) for x in (alias.get("층"), alias.get("때")) if x)] tags += ["규격 다름"] * bool(alias.get("규격다름")) + ["근거 다름"] * off tags += [f"숫자 판정 맞음 = {entry['차례'][0]}"] if other else [] out = {"상태": "별칭", "체크": check, "대상": targets, "글": "별칭 장부 짝", "꼬리표": tags} out["별칭"] = _alias_info(alias, off) if entry and entry.get("품"): out["품"] = _qty(entry, (entry.get("옵션") or {}).get(next(iter(codes), None))) return out def whole(hopyo: dict, ctx: Ctx) -> dict | None: """직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」.""" head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")} alias = ledger.recall(head, ctx.to, ctx.books, whole=True) if alias and alias.get("갈래") == "짝" and alias.get("짝"): return { "대상": [_pair(p, head, ctx) for p in alias["짝"]], "글": "별칭 장부 짝", "체크": True, } entry = ctx.cache.get(f"호표:{hopyo['번']}") if not entry or not entry.get("차례"): return None top = entry["차례"][0] state = entry["판정"]["상태"] target = _target(top, head, ctx, entry["옵션"].get(top), state in ("맞음", "다른 점")) return {"대상": [target], "글": f"통째 = {entry['판정']['글']}", "체크": state == "맞음"} def hopyo_view(hopyo: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]: """호표 하나 — 줄마다 맞춤 · `맞춤` = [체크 제안 수, 줄 수] · 통째 · 셈 · 만든.""" lines = [{**one, "맞춤": match_line(one, ctx, full)} for one in hopyo.get("줄") or []] check = sum(1 for one in lines if one["맞춤"]["체크"]) out = {**hopyo, "줄": lines, "맞춤": [check, len(lines)]} if full: made = ctx.done.get(hopyo_build.mark(ctx.key, int(hopyo["번"]))) out.update(통째=whole(hopyo, ctx), 셈=ctx.calc) out["만든"] = {"키": made, "확인": True} if made else None return out def guess( folder: Path, key: str, no: int, at: int, target: Any, to: Path | None = None ) -> dict[str, Any]: """미리 셈(저장 안 함) — 고른 대상 · 옵션으로 엔진 한 번 → {대상, 품: {엑셀, 우리, 같음}, 글}.""" ctx = Ctx(folder, key, to) hopyo = ctx.units.get(int(no)) lines = (hopyo or {}).get("줄") or [] if not 0 <= int(at) < len(lines): raise store.StoreError(404, f"없는 줄 「제{no}호표 {at}」") line = lines[int(at)] picked = target if isinstance(target, list) else [target] if not picked or not all(isinstance(t, dict) for t in picked): raise store.StoreError(400, "대상 꼴이 틀림") entry = ctx.cache.get(fit.unit_key(line)) if entry is None: block = next( ( b for b in ctx.doc.get("단산") or [] if b.get("번") == (line.get("참조") or {}).get("번") ), None, ) entry = {"품": verdict.excel_qty(finger.excel(block, line))} targets, total = [], {"L": {}, "X": [], "M": {}, "S": {}} for t in picked: if t.get("로직"): if t["로직"] not in ctx.names: raise store.StoreError(400, "돈 로직 · 우리 자재가 아님") given = t.get("입력값") one = _target(t["로직"], line, ctx, None, False, 몫=t.get("몫")) if isinstance(given, dict): one["입력값"] = {**one["입력값"], **given} one["출처"].update({k: "고름" for k in given}) v = ours.run(t["로직"], one["입력값"]) or {"L": {}, "X": [], "M": {}, "S": {}} k = float(t.get("몫") or 1) for name, q in v["L"].items(): total["L"][name] = total["L"].get(name, 0.0) + q * k total["X"] += [[a, b, q * k] for a, b, q in v["X"]] for name, q in v["M"].items(): total["M"][name] = total["M"].get(name, 0.0) + q * k targets.append(one) elif t.get("자재") in ctx.mats: targets.append(_pair({"자재": t["자재"], "배": t.get("배")}, line, ctx)) else: raise store.StoreError(400, "돈 로직 · 우리 자재가 아님") qty = _qty(entry, {"우리품": option.ours_qty(total)}) return {"대상": targets, "품": qty, "글": "우리 품 — 엔진 한 번(저장 안 함)"}