Files
Aislo/M02_MasterTemplete/M02_Hopyo_Judge.py
T
eomsangdonandClaude Opus 5.5 48848bb287 refactor(785-7): 옛 맞춤 층 Python 지움 · 서버 새 길로 정리 · 낡은 시험 정리
- 지움(git rm) 10 파일 3,374 줄: M02_Hopyo_Finger · Ours · Option · Fit · Combine · Verdict · Line · SkLink · SkPatch · Basis
- Match: 흐름 몫(candidates · name_cands · fix_typo · clause · b_order · _logic_specs · _line_mod · units_of · _state · view · guess · picks · chain) 지움 · 이름 도구 몫 · line_key 남김
- Words: conds · cond_score · close · cosine · jacc 지움 / Shares: sections 지움(trade · eq_key 남김)
- 옮김: Finger.kind_of → Fixed.kind_of · SkLink(sk_line · kept · left · LEFT · WHY) → Sangeun · SkPatch 기계 몫(MACH_IN · MACHINE · SOLO · _mach_logics) → Units.mach_logics
- Sangeun: 옛 /sangeun 몸(build · _sure · Maker.basis · judge · plan · make) 지움 · Kids: first · proposal · order 지움 · Build: build() · unlinked() 지움(모두 고정 Builder · build_all · _order · _origins 남김)
- Router: 옛 길 지움(GET hopyo/{키}/{번} · guess · hopyo-picks · hopyo-build · hopyo-unlinked · hopyo/{키}/sangeun) · GET hopyo/{키} = Judge.view(1차 줄 판정 줄상태 · 만든 · 블록 풀이 셈)
- Marks: 칠 = Judge.judged 만 · Forget: 맞춤 캐시 지움 줄 걷음 · Ledger: Ours.version 판 칸 걷음 · Item: Kids.first 걷음
- Row(덧): 단산 줄 = 장부 짝이 있어도 늘 「2차 필요」(장부 = 1차 후보 힌트만)

시험 정리(까닭 = 지운 길 · 모듈만 시험함):
- 지움 11 파일: test_m02_hopyo_finger · fit · golden · option · ours · basis · seam · flow · draft_calc · test_635 · test_628(지문 · 순위 · 옵션 거꾸로 · 맡은 절 · 옛 확인 흐름)
- 함수 지움: /sangeun · /hopyo-build · /hopyo-unlinked · hopyo-picks · 옛 줄 맞춤 보기를 부르던 것(572 · 718_1 · 729 · 739_1 · 777_1 · 778_5 · 780_1 · labor_national · 705_3 · m02_hopyo_match · 746_1)
- test_621 S2(지문 해시) 지움 · S1 남김 · test_m02_hopyo_build = 장부 길 시험만(PUT 으로 심음)
- 새 길로: 779_2(줄상태 = 1차 판정 · 칠 셈 전 None) · grid · stmate(hopyo-judge 줄 판) · 778_1 · 778_2(Units.mach_logics) · 642_6(Marks 원문) · 642_3 장부 짝 단산 줄 2차 지킴

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:15:20 +09:00

365 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 엑셀 불러오기 판정 · 검산(PLAN 785-5 · 요구 642 · 설계 642-A 2-6 ⑤ · 2-9) — 서버 단독 · 읽기만.
판정 = 1차 줄 판(`M02_Hopyo_Row.rows`) + 단산 줄 2차 블록 풀이(`Clause.basis` 후보 → `Solve.parts` →
`Solve.block`) · 목록(`listing`) · 호표 하나(`hopyo`) · 블록 하나(`block`).
블록 풀이 = 파생 저장본(HP 폴더 `_derived/hopyo_judge_{키}` · 입력 지문 = HP + M01 파일 내용 + 풀이 판 ·
지문 다를 때만 다시 셈 · GET 은 안 지움 · 올림 뒤 한 번 뒤에서 `ensure` · 셈 중 = `셈중` · 판정 None) ·
블록 판정 읽기 = `judged`(Marks 칠) · [끝내기] = `forget`.
줄 판 줄에 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록에 엑셀 금액 · 절에 몫(785-6 화면이 읽음).
검산(`check`) = 고른 것마다 — 기초 = 입력값(`inputs` · 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종
비움) · 입력필요 · 엑셀 단위 줄과 품 대조(`Solve.pair`) · 금액(엔진 한 번 · 입력 필요면 셈 안 함) /
그 밖 = 「확인」 · 수량 = 엑셀 × 몫 × 배(`Row.quantity` · 사용자 배 우선).
저장 없음 · 지어 넣은 값 없음(입력 필요 칸은 비워 둠 · 안 쓰는 칸 = 기초 기본값).
"""
from __future__ import annotations
import shutil
import threading
from collections import Counter
from decimal import Decimal
from pathlib import Path
from typing import Any
from common_util import common_util_derived_store as derived
from M02_MasterTemplete import M02_Hopyo_Bind as hb
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
from M02_MasterTemplete import M02_Hopyo_Clause as clause
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Row as hrow
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_Solve as solve
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
mf, ma = hb.mf, hb.ma
EXCEL = ("이름", "규격", "단위", "단가")
CHECKED = "확인"
SUGGEST = "제안 있음" # 1차 줄 판정(`M02_Hopyo_Row`)
KIND = "hopyo_judge_" # 파생 저장본 종류 = 이것 + HP 키(HP 폴더 `_derived/`)
VERSION = "785-5.1" # 풀이 꼴이 바뀌면 올림
_LOCK, _RUNNING = threading.Lock(), set()
def master() -> mf.Master:
return hb.m01.cm.master()
def _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
def _blocks(book: dict[str, Any]) -> dict[int, dict[str, Any]]:
return {int(b["번"]): b for b in book.get("단산") or [] if b.get("번") is not None}
def _unit(book: dict[str, Any], no: int) -> dict[str, Any]:
got = next((h for h in book.get("호표") or [] if int(h["번"]) == int(no)), None)
if got is None:
raise _bad(f"없는 호표 「제{no}호표」", 404)
return got
def _made(folder: Path, key: str, to: Path) -> tuple[dict[str, str], dict[str, str]]:
"""(만든 SC {본뜬 키: SC}, 만든 SK {본뜬 키: SK}) — 만들 층 · 이 파일 층 것만."""
source = refs.where(folder)
return hbuild.confirmed(to, source), sangeun.made(to, key, source)
# ── 기초 입력값 ───────────────────────────────────────────────────────
def inputs(m: mf.Master, code: str, given: dict[str, Any] | None) -> tuple[dict, list[str]]:
"""(저장 입력값, 입력필요) — 고른 값(그 기초 칸 · 빈 값 뺌) + 안 쓰는 칸 = 기초 기본값 ·
쓰는데 빈 칸 = 입력필요(비워 둠) · 지역 · 자재지역 · 조종원직종 = 비움(프로젝트가 정함)."""
row = m.logic(code)
names = [s["이름"] for s in hb.specs_of(row)]
have = {
k: v
for k, v in (given or {}).items()
if k in names and k not in hb.SKIP and v not in (None, "")
}
need = [n for n in names if n in hb._needed(m, code, row, have) and n not in hb.SKIP]
base = ma.defaults(m, row)
idle = [n for n in names if n not in have and n not in need and n not in hb.SKIP]
full = {n: hb.plain(base[n]) for n in idle if base.get(n) is not None}
return {**full, **{k: hb.plain(v) for k, v in have.items()}}, need
def money(m: mf.Master, code: str, given: dict[str, Any]) -> dict[str, Any]:
"""기초 한 줄 셈(일위대가 로직 줄과 같은 길 `Labor_Doc.run_line` · 끝수 · 비운 지역 칸 = 자동값 ·
참고 셈만) → {노무비, 재료비, 경비, 계} · 못 셈 = {계 None, 까닭}."""
got = labor.run_line(m, code, given)
if got.get("결과") is not None:
return {"결과": hb.plain(got["결과"])}
if got.get("기준") is None:
return {"계": None, "까닭": got.get("까닭")}
return {k: hb.plain(v) for k, v in got["기준"].items()}
# ── 판정 ─────────────────────────────────────────────────────────────
def _row(line: dict[str, Any], one: dict[str, Any]) -> dict[str, Any]:
return {**one, **{k: line.get(k) for k in EXCEL}}
def _solve(m: mf.Master, blk: dict, line: dict | None, same: list[dict]) -> dict[str, Any]:
"""블록 풀이 — 조항 색인 후보 → 절 → 검산 · 판정 + 근거 · 엑셀 금액 · 절 몫."""
basis = clause.basis(blk)
parts = solve.parts(blk, basis["후보"])
got = solve.block(m, blk, parts, {"줄": line, "같은줄": same})
for sec, part in zip(got["절"], parts, strict=True):
share = part.get("몫")
sec["몫"] = share.get("값") if isinstance(share, dict) else share
sec["번"] = part.get("번")
got["근거"] = {k: basis[k] for k in ("표지", "글", "출처", "후보", "뺌")}
got["금액"] = blk.get("금액")
return got
# ── 블록 풀이 파생 저장본(올림 뒤 한 번 · 뒤에서) ─────────────────────────
def _stamp(key: str, book: dict[str, Any]) -> str:
"""입력 지문 — HP(지문 · 판 · 올린때) + M01 첫 층 파일 내용 + 풀이 판."""
folder = Path(hb.m01.FOLDER)
files = {
p.name: derived.file_digest(p)
for p in sorted(folder.glob("*.json"))
if not p.name.startswith("_")
}
head = (VERSION, key, book.get("지문"), book.get("판"), book.get("올린때"))
return derived.derived_key(*head, files)
def _caller(book: dict[str, Any], no: int) -> tuple[dict | None, list[dict]]:
"""블록을 부르는 첫 줄 · 그 호표 다른 줄(B1 · B6) — 일위대가 밖 = (None, [])."""
line = sangeun.calls(book)[1].get(no)
if not line:
return None, []
unit = _unit(book, line["_호표"])
same = [x for x in unit.get("줄") or [] if x.get("차례") != line.get("차례")]
return {k: v for k, v in line.items() if k != "_호표"}, same
def solved(folder: Path, key: str) -> dict[str, Any] | None:
"""저장본 `{번 글: 풀이}` — 지문 다르거나 없으면 None(GET 은 안 지움)."""
book = hopyo_store.read(folder, key)
got = derived.read_derived(hopyo_store._dir(folder), KIND + key, _stamp(key, book))
return got["value"] if got else None
def solve_all(folder: Path, key: str) -> dict[str, Any]:
"""블록 모두 풀어 저장본으로(부르는 첫 줄 기준)."""
book = hopyo_store.read(folder, key)
m, out = master(), {}
for no, blk in sorted(_blocks(book).items()):
out[str(no)] = _solve(m, blk, *_caller(book, no))
root = hopyo_store._dir(folder)
derived.write_derived(root, KIND + key, _stamp(key, book), out, keep=1)
return out
def _work(folder: Path, key: str) -> None:
try:
solve_all(folder, key)
except Exception: # noqa: BLE001 — 뒤 셈 실패 = 다음 읽기에서 다시(파생 저장본)
pass
finally:
with _LOCK:
_RUNNING.discard(f"{folder}|{key}")
def _start(folder: Path, key: str) -> None:
tag = f"{folder}|{key}"
with _LOCK:
if tag in _RUNNING:
return
_RUNNING.add(tag)
threading.Thread(target=_work, args=(Path(folder), key), daemon=True).start()
def ensure(folder: Path, key: str) -> bool:
"""저장본 있음 = 참 · 없으면 뒤 셈 시작(화면 안 막음 · 같은 파일 한 번) = 거짓."""
if solved(folder, key) is not None:
return True
_start(folder, key)
return False
def judged(folder: Path, key: str) -> dict[int, dict[str, Any]] | None:
"""블록 판정 `{번: {판정, 고를수}}` — 셈 중 = None(뒤 셈 시작 · Marks 칠이 부름)."""
got = solved(folder, key)
if got is None:
_start(folder, key)
return None
return {int(no): {"판정": v["판정"], "고를수": v["고를수"]} for no, v in got.items()}
def forget(folder: Path, key: str) -> None:
"""[끝내기] — 그 HP 저장본 지움."""
shutil.rmtree(derived.derived_dir(hopyo_store._dir(folder), KIND + key), ignore_errors=True)
def listing(folder: Path, key: str, to: Path) -> dict[str, Any]:
"""호표 목록(1차 판정 수 · 만든 SC) · 블록 목록(부름 · 표지 · 판정 · 고를수 · 만든 SK) ·
`셈중` = 블록 풀이 뒤 셈 중(블록 판정 · 고를수 None)."""
book = hopyo_store.read(folder, key)
rows = hrow.rows(book, folder, ledger.doc_id(folder, key))
sc, sk = _made(folder, key, to)
units = []
for h in book.get("호표") or []:
no = int(h["번"])
count = Counter(r["판정"] for r in rows.get(h["번"]) or [])
head = {"번": no, **{k: h.get(k) for k in ("이름", "규격", "단위", "단가")}}
units.append(
{**head, "줄수": len(h.get("줄") or []), "판정": dict(count),
"만든": sc.get(hbuild.mark(key, no))}
) # fmt: skip
call, done = sangeun.callers(book), judged(folder, key)
blocks = []
for no, b in sorted(_blocks(book).items()):
got = (done or {}).get(no) or {"판정": None, "고를수": None}
blocks.append(
{"번": no, "이름": b.get("이름"), "단위": b.get("단위"), "금액": b.get("금액"),
"부름": call.get(no, []), "표지": clause.basis(b)["표지"], **got,
"만든": sk.get(sangeun.mark(key, no))}
) # fmt: skip
out = {"키": key, "파일": book.get("파일"), "셈중": done is None}
return {**out, "호표": units, "블록": blocks}
def view(folder: Path, key: str, to: Path) -> dict[str, Any]:
"""엑셀 보기 머리 · 목록(옛 `Match.view` 꼴) — 파일 줄 + 검산 · 호표마다 `맞춤` [제안 있음, 줄 수] ·
`줄상태 [[차례, 1차 판정, 제안 있음]]` · `만든` SC · `셈` = 블록 풀이 뒤 셈(`끝` 거짓 = 셈 중)."""
book = hopyo_store.read(folder, key)
rows = hrow.rows(book, folder, ledger.doc_id(folder, key))
sc, _ = _made(folder, key, to)
units = []
for h in book.get("호표") or []:
no = int(h["번"])
state = [
[i, r["판정"], r["판정"] == SUGGEST] for i, r in enumerate(rows.get(h["번"]) or [])
]
head = {k: h.get(k) for k in ("번", "이름", "규격", "단위", "금액", "행") if k in h}
units.append(
{**head, "맞춤": [sum(x[2] for x in state), len(state)], "줄상태": state,
"만든": sc.get(hbuild.mark(key, no))}
) # fmt: skip
ready = [sum(h["맞춤"][0] for h in units), sum(h["맞춤"][1] for h in units)]
calc = {"끝": judged(folder, key) is not None, "한": 0, "모두": None, "까닭": "블록 풀이"}
head = {**hopyo_store.brief(book), "검산": book.get("검산"), "셈": calc}
return {**head, "호표": units, "맞춤": ready}
def hopyo(folder: Path, key: str, no: int, to: Path) -> dict[str, Any]:
"""호표 하나 — 줄 판(+ 엑셀 칸) · 단산 줄 `풀이`(부르는 첫 줄이면 저장본)."""
book = hopyo_store.read(folder, key)
unit = _unit(book, no)
rows = hrow.rows({**book, "호표": [unit]}, folder, ledger.doc_id(folder, key))[unit["번"]]
lines, blocks, m = unit.get("줄") or [], _blocks(book), None
cache, first = solved(folder, key) or {}, sangeun.calls(book)[1]
out = []
for r, line in zip(rows, lines, strict=True):
r = _row(line, r)
sub = r.get("번")
if "단가산출" in r["갈래"] and sub is not None and int(sub) in blocks:
head = first.get(int(sub)) or {}
mine = head.get("_호표") == unit["번"] and head.get("차례") == line.get("차례")
if mine and str(sub) in cache:
r["풀이"] = cache[str(sub)]
else:
m = m or master()
others = [x for x in lines if x is not line]
r["풀이"] = _solve(m, blocks[int(sub)], line, others)
out.append(r)
sc, _ = _made(folder, key, to)
head = {k: unit.get(k) for k in ("번", "이름", "규격", "단위", "단가")}
return {**head, "만든": sc.get(hbuild.mark(key, no)), "줄": out}
def block(folder: Path, key: str, no: int, to: Path) -> dict[str, Any]:
"""단산 블록 하나 — 풀이(저장본 › 셈) + 이름 · 단위 · 부름 · 만든(부르는 첫 줄로 B1 · B6)."""
book = hopyo_store.read(folder, key)
blocks = _blocks(book)
if no not in blocks:
raise _bad(f"없는 블록 「단산{no}」", 404)
line, same = _caller(book, no)
got = (solved(folder, key) or {}).get(str(no)) or _solve(master(), blocks[no], line, same)
_, sk = _made(folder, key, to)
blk = blocks[no]
head = {"이름": blk.get("이름"), "단위": blk.get("단위") or (line or {}).get("단위")}
head |= {"부름": sangeun.callers(book).get(no, []), "만든": sk.get(sangeun.mark(key, no))}
return {**got, **head}
# ── 검산 ─────────────────────────────────────────────────────────────
def _want(m: mf.Master, pick: dict[str, Any]) -> Any:
"""고른 것 단위 — 기초 = 결과단위(「원/㎥」 → ㎥ · 「원/자재 단위」 → 자재) · 그 밖 = 고른 것 단위."""
if pick.get("갈래") == "기초" and pick.get("키"):
unit = str(m.logic(pick["키"]).get("결과단위") or "")
unit = unit.split("/", 1)[1] if unit.startswith("원/") else unit
return unit.removesuffix(" 단위").strip() # 「원/자재 단위」 = 줄 단위 그대로
return pick.get("단위")
def quantity(m: mf.Master, line: dict, pick: dict) -> dict[str, Any]:
"""줄 수량 `{엑셀, 몫, 배, 갈래, 우리}` — 사용자 배(단위 환산 값)가 있으면 그것."""
got = hrow.quantity(line, _want(m, pick), pick.get("몫"))
times = pick.get("배")
if times is not None and line.get("수량") is not None:
ours = Decimal(str(line["수량"])) * Decimal(str(pick.get("몫") or 1))
got |= {"배": times, "갈래": "사용자", "우리": hrow._plain(ours * Decimal(str(times)))}
return got
def one_pick(m: mf.Master, pick: dict[str, Any], items: list[dict], share: Any) -> dict:
"""고른 것 하나 검산."""
out = {"갈래": pick["갈래"], "키": pick.get("키")}
if pick["갈래"] != "기초":
return {**out, "판정": CHECKED}
code = str(pick.get("키") or "")
try:
m.logic(code)
except Exception as e: # noqa: BLE001 — 없는 키
raise _bad(f"없는 기초 「{code}」") from e
full, need = inputs(m, code, pick.get("입력값"))
rows = solve.pair(hb.quantities(m, code, full), solve._usable(items), share) if items else []
if need:
verdict = solve.NEED
elif any(r["판정"] == solve.DIFF for r in rows):
verdict = solve.DIFF
else:
verdict = solve.SAME if any(r["판정"] == solve.SAME for r in rows) else CHECKED
got = money(m, code, full) if not need else {"계": None, "까닭": f"입력 필요 {len(need)}"}
return {**out, "입력값": full, "입력필요": need, "줄": rows, "판정": verdict, "금액": got}
def check(
folder: Path, key: str, hno: int | None, at: int | None, bno: int | None, picks: list[dict]
) -> dict[str, Any]:
"""`{고른 [...], 수량}` — 호표 줄(`hno` · `at`) · 블록(`bno` · 줄이 단산이면 그 블록) · 고른 기초는
맡은 절(`Solve.parts`) 단위 줄 · 몫과 대조."""
book = hopyo_store.read(folder, key)
blocks, line = _blocks(book), None
if hno is not None:
lines = _unit(book, hno).get("줄") or []
if at is None or not 0 <= at < len(lines):
raise _bad(f"없는 줄 「제{hno}호표 {at}」")
line = lines[at]
ref = line.get("참조") or {}
if bno is None and ref.get("종류") == "단산" and ref.get("번") is not None:
bno = int(ref["번"])
if bno is not None and bno not in blocks:
raise _bad(f"없는 블록 「단산{bno}」", 404)
blk = blocks[bno] if bno is not None else {}
parts = solve.parts(blk, clause.basis(blk)["후보"]) if blk else []
m = master()
out = []
for i, pick in enumerate(picks):
part = next((x for x in parts if pick.get("키") in x["후보"]), None) or {}
items = part.get("항목") # 그 기초가 맡은 절 단위 줄(없으면 블록 통째)
items = (blk.get("항목") or []) if items is None else items
share = part.get("몫")
share = pick.get("몫") or (share.get("값") if isinstance(share, dict) else share) or 1
out.append({"차례": i, **one_pick(m, pick, items, share)})
qty = quantity(m, line, picks[0]) if line is not None and picks else None
return {"고른": out, "수량": qty}