Files
Aislo/M02_MasterTemplete/M02_Hopyo_Line.py
T
eomsangdonandClaude Opus 5.5 e6cdcb2952 feat(일위대가실무): 338-1 맞춤 핵심 서버 2 단계 — 줄 새 상태 · 체크 제안 · 대상 목록 · 품 비교 · 호표 통째 · 셈 · 만든 · guess 목록 + 골든 시험
- 새 Line — 상태 차례(% · 호표 · 노임 · 장부 뺌 · 별칭 · 맞춤셈 판정 · 이름 근거 후보) · 체크 제안(맞음 · 별칭 규칙 셋 · 확인된 하위 호표) · 대상(입력값 출처 별칭 > 블록 > 규격 · 환산 · 몫 · 배) · 품 비교 · 직접 줄 호표 통째 · guess(대상 목록 · 낱개도 받음 · 엔진 한 번)
- Match — view(to = 만들 층 · 셈) · picks · guess 를 Line 으로 · 옛 층 기억 · confirm · 들어감 없앰 · b_order 후보에 근거 칸
- Verdict — 위 다섯 딱 맞음 먼저(조합 없는 줄) · 후보 근거 = 근거 맞춤 꼴 · 지문차례(조합 전)
- Fit — 맞춤셈 캐시 이름에 셈 판(CALC_VERSION)
- 골든 시험(느림 · 1 분 54 초) — 사람 126 1순위 118 · 다섯 안 122 · 맞음 57 틀림 0 · 다른 점 틀림 1 · 옵션까지 57 · 조합 맞음 62/83 · 새 파일 51 맞음 틀림 2 · 씨앗 끔 · 북부청 조합 전 통과
- 근거 시험(나 창) 세 곳 — 옛 match_line → b_order(새 API)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-16 00:00:56 +09:00

324 lines
16 KiB
Python

"""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, "글": "우리 품 — 엔진 한 번(저장 안 함)"}