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