"""338 골든 도우미 — 실물 엑셀(`resources/knowledge/original/실무문서`)을 지금 읽기로 읽어 정답 표 (`fixtures/m02_hopyo_golden.json`)와 맞대고 지표(설계 7-3)를 셈. 읽기만. 블록 글 = HP 블록에 `글` 이 있으면 그것(나 창 2a 뒤) · 없으면 단가산출 시트 산출근거 열 글(엑셀꼴) · 거창 = 줄 칸을 이어 붙임. STmate 파일은 읽기(`M02_Hopyo_Excel.read_book`)가 되면 들어감 · 아니면 뺌. python resources/tester/helper_m02_hopyo_golden.py → 지표 찍음 · tmp/m02_338/golden.json """ from __future__ import annotations import json import sys import time import warnings from functools import lru_cache from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402 from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # noqa: E402 warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl") DOCS = ROOT / "resources/knowledge/original/실무문서" FIXTURE = Path(__file__).resolve().parent / "fixtures/m02_hopyo_golden.json" FILES = { "북부청임도": DOCS / "2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", "북부청사방": DOCS / "2026_실무일위대가 모음/2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls", "거창": DOCS / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", "봉화": DOCS / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", "동부청영월": DOCS / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", "울진대흥": DOCS / "2024년 간선임도(기번3-울진.대흥)(공통)" / "2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", "영덕계류": DOCS / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료" / "2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", "울진소광": DOCS / "25년 산불진화임도(기번8)(울진소광)" / "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", } LABELED = ("북부청임도", "북부청사방", "동부청영월") def labels() -> list[dict[str, Any]]: return json.loads(FIXTURE.read_text(encoding="utf-8")) def _row_text(row: list, start: int) -> str: cells = [] for v in row[start:140]: if v is None or v == "": continue cells.append(str(int(v) if isinstance(v, float) and v == int(v) else v).strip()) return " ".join(cells) @lru_cache(maxsize=8) def read(tag: str) -> dict[str, Any] | None: """파일 → HP 꼴(블록 `글` 붙임) · 못 읽으면 None(STmate 읽기 전).""" path = FILES[tag] data = path.read_bytes() try: book = hx.read_book(path.name, data) except hx.ExcelError: return None if any(b.get("글") or b.get("자원") for b in book.get("단산") or []): return book calc = hx._sheets(path.name, data).get(hb.SHEET) or [] _, col, _ = hb._columns(calc) join = tag == "거창" for b in book.get("단산") or []: a, z = b["행"] rows = calc[a:z] text = [_row_text(r, col) if join else (r[col] if col < len(r) else "") for r in rows] b["글"] = [str(t).rstrip() for t in text if isinstance(t, str) and t.strip()] return book def _keys(line: dict[str, Any]) -> list[tuple]: """줄 → 짝 열쇠 후보 — (이름 · 규격 · 단위 · 이음) · 이음 = 단산 번 · 줄 코드 · 없음.""" ref = line.get("참조") or {} head = (line.get("이름"), line.get("규격") or "", line.get("단위") or "") code = str(ref.get("코드") or line.get("코드") or "")[:6] out = [(*head, ("단산", ref["번"]))] if ref.get("종류") == "단산" else [] return out + ([(*head, ("코드", code))] if code else []) + [(*head, None)] def _lab_key(lab: dict[str, Any]) -> tuple: head = (lab["이름"], lab["규격"], lab["단위"]) if lab.get("단산") is not None: return (*head, ("단산", lab["단산"])) return (*head, ("코드", str(lab["코드"])[:6]) if lab.get("코드") else None) def _find(want: dict[tuple, dict], line: dict[str, Any]) -> dict | None: return next((want[k] for k in _keys(line) if k in want), None) def line_of(lid: str) -> tuple[dict | None, dict[str, Any]]: """정답 표 id → (블록, 줄) — 시험이 실물 줄 하나를 꺼냄.""" lab = next(x for x in labels() if x["id"] == lid) book = read(lab["파일"]) blocks = {int(b["번"]): b for b in book.get("단산") or []} for h in book["호표"]: for line in h["줄"]: if _find({_lab_key(lab): lab}, line): ref = line.get("참조") or {} return (blocks.get(int(ref["번"])) if ref.get("종류") == "단산" else None), line raise KeyError(lid) def evaluate(tags: tuple[str, ...] | None = None) -> dict[str, Any]: """{줄: {id: 결과}, 호표: {(파일, 번): (ids, refs)}, 읽음: [파일], 시간}.""" labs = labels() tags = tags or tuple(dict.fromkeys(lab["파일"] for lab in labs)) got = hm.targets() out: dict[str, Any] = {"줄": {}, "호표": {}, "읽음": [], "못읽음": [], "시간": {}} for tag in tags: book = read(tag) if book is None: out["못읽음"].append(tag) continue out["읽음"].append(tag) want = {_lab_key(lab): lab for lab in labs if lab["파일"] == tag} blocks = {int(b["번"]): b for b in book.get("단산") or []} t0 = time.time() for h in book["호표"]: ids, refs = [], [] for line in h["줄"]: ref = line.get("참조") or {} if ref.get("종류") == "일위" and ref.get("번"): refs.append(int(ref["번"])) lab = _find(want, line) if lab is None: continue ids.append(lab["id"]) if lab["id"] in out["줄"]: continue block = blocks.get(int(ref["번"])) if ref.get("종류") == "단산" else None out["줄"][lab["id"]] = {"lab": lab, "셈": verdict.block(block, line, got)} out["호표"][f"{tag}|{h['번']}"] = (list(dict.fromkeys(ids)), refs) by_code = {str(b.get("코드") or "")[:6]: b for b in blocks.values() if b.get("코드")} for lab in want.values(): # 호표 줄이 안 부르는 블록(실험 C6 = 블록마다) — 블록 머리로 줄 block = blocks.get(lab.get("단산") or -1) or by_code.get(str(lab.get("코드"))[:6]) if lab["id"] in out["줄"] or block is None: continue basis = {"출처": block.get("출처") or [], "직종": block.get("직종") or []} line = {**{k: lab[k] for k in ("이름", "규격", "단위")}, "갈래": "단가산출"} line.update(참조={"종류": "단산", "번": block["번"]}, 근거=basis) out["줄"][lab["id"]] = {"lab": lab, "셈": verdict.block(block, line, got)} out["시간"][tag] = round(time.time() - t0, 1) return out def chosen_of(entry: dict[str, Any]) -> set[str]: """줄이 고른 로직(T1) — 조합이면 체크한 것 · 아니면 1위.""" combo = entry.get("조합") or [] top = (entry.get("차례") or [None])[0] codes = {c["로직"] for c in combo if c["로직"] and c.get("체크")} return codes or ({top} if top else set()) def _gather(res: dict, key: str, seen: tuple = ()) -> list[str]: tag = key.split("|")[0] if key not in res["호표"] or key in seen: return [] ids, refs = res["호표"][key] out = list(ids) for no in refs: out += _gather(res, f"{tag}|{no}", (*seen, key)) return list(dict.fromkeys(out)) def metrics(res: dict[str, Any], group: set[str] | None = None) -> dict[str, Any]: """설계 7-3 지표 — 사람 정답 줄 · 새 파일 · 조합 맞음(T1 · 덮을 수 있는 호표).""" rows = [r for r in res["줄"].values() if group is None or r["lab"]["무리"] in group] have = [r for r in rows if r["lab"]["정답"]] ok1 = lambda r: (r["셈"]["차례"] or [None])[0] in set(r["lab"]["정답"]) # noqa: E731 state = lambda r: r["셈"]["판정"]["상태"] # noqa: E731 opt_ok = 0 for r in have: top = (r["셈"]["차례"] or [None])[0] found = (r["셈"]["옵션"] or {}).get(top) or {} if ok1(r) and found.get("수준", 9) <= 1 and state(r) == "맞음": opt_ok += 1 out = { "줄": len(have), "1순위": sum(ok1(r) for r in have), "다섯 안": sum(bool(set(r["셈"]["차례"][:5]) & set(r["lab"]["정답"])) for r in have), "맞음": sum(state(r) == "맞음" for r in rows), "맞음 틀림": sum(state(r) == "맞음" and not ok1(r) for r in rows), "다른 점": sum(state(r) == "다른 점" for r in rows), "다른 점 틀림": sum(state(r) == "다른 점" and not ok1(r) for r in rows), "옵션까지": opt_ok, } keys = [k for k in res["호표"] if k.split("|")[0] in LABELED] whole = cover = 0 by = res["줄"] for key in keys: ids = [ i for i in _gather(res, key) if i in by and (group is None or by[i]["lab"]["무리"] in group) ] if not ids: continue slots, chosen = [], set() for i in ids: lab = by[i]["lab"] if not lab["정답"]: slots.append(set()) elif lab.get("복합") and lab["id"] != "S062": slots += [{x} for x in lab["복합"]] else: slots.append(set(lab["정답"])) chosen |= chosen_of(by[i]["셈"]) if any(not s for s in slots): continue cover += 1 alts = set().union(*slots) whole += all(chosen & s for s in slots) and not (chosen - alts) out["조합 호표"], out["조합 맞음"] = cover, whole return out def show(res: dict[str, Any]) -> dict[str, Any]: table = { "사람": metrics(res, {"사람"}), "새 파일": metrics(res, {"자동", "씨앗", "자동+씨앗"}), **{ tag: metrics( {**res, "줄": {i: r for i, r in res["줄"].items() if r["lab"]["파일"] == tag}}, {"사람"}, ) for tag in LABELED if tag in res["읽음"] }, } return {"읽음": res["읽음"], "못읽음": res["못읽음"], "시간": res["시간"], "지표": table} if __name__ == "__main__": t = time.time() res = evaluate(tuple(sys.argv[1:]) or None) summary = show(res) print(json.dumps(summary, ensure_ascii=False, indent=1)) print("전체", round(time.time() - t, 1), "s") dump = { i: { "lab": r["lab"], "차례": r["셈"]["차례"], "판정": r["셈"]["판정"], "조합": r["셈"]["조합"], "B": r["셈"]["B"], } for i, r in res["줄"].items() } (ROOT / "tmp/m02_338").mkdir(parents=True, exist_ok=True) (ROOT / "tmp/m02_338/golden.json").write_text( json.dumps(dump, ensure_ascii=False, indent=1, default=str), "utf-8" )