- 새 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
272 lines
12 KiB
Python
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"
|
|
)
|