Files
Aislo/resources/tester/helper_m02_hopyo_golden.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

272 lines
12 KiB
Python

"""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"
)