feat(797-1a): 엑셀 불러오기 빠진 정보 표지 다섯 + 줄 단위 합격 시험 · 표본 밖 고정

- 금액 줄 못 읽음: 결과 뒤 꼬리 글은 떼고 금액 줄로 읽음(2016 틀린 줄 2 → 0) · 못 읽는 글끝 · 식못읽음 = 표지 + 보인 값 보존
- 단가 후보 여럿 · 사람 확인 / 추정(문맥 · 기계 세 줄) 표지 — 임의 선택 대신 표지
- 당초 / 적용 칸 표지(Book.applied_how) · 조정 비율 · 낙찰률 미지원 표지
- FromHopyo.keep 이 읽기 불가 사유(빈 파일 · DRM 암호 · 형식 오류)를 돌려줌
- 사람 확인 표지는 「참고 · 」 머리 — 못 옮김 표지와 가름 · test_793_2 는 못 옮김 표지만 셈(낡은 기대 수정)
- test_797_1a 신설: 작은 견본 · 표지 없는 틀린 줄 0 · 표본 밖 수목 142 · 대흥공통 46 · 2016 374 고정 · 버림1 일치율 감시
- 금액 결과 그대로(엔진 · 수식 · Copy 손대지 않음)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 12:07:36 +09:00
co-authored by Claude Sonnet 5.5
parent 02bd87bb4e
commit 0792a17e2c
6 changed files with 378 additions and 18 deletions
+23 -6
View File
@@ -162,17 +162,33 @@ def _n(v: Any) -> float | None:
return None
def applied(r: list[Any]) -> dict[str, float]:
"""총괄표 줄 → {T, L, M, S} — 적용 칸(5~8) 합이 맞으면 적용 · 아니면 당초(25~28)."""
def applied_how(r: list[Any]) -> tuple[dict[str, float], str | None]:
"""총괄표 줄 → ({T, L, M, S}, 쓴 칸 표지) — 적용 칸(5~8) 합이 맞으면 적용 · 아니면 당초(25~28).
표지 = 당초를 쓴 경우(까닭) · 적용 ≠ 당초인 경우(차이) · 그 밖 None(PLAN 797-1a)."""
a = {k: _n(r[j]) for k, j in (("T", 5), ("L", 6), ("M", 7), ("S", 8))}
first = {k: _n(r[j]) for k, j in (("T", 25), ("L", 26), ("M", 27), ("S", 28))}
names = {"T": "계", "L": "노무비", "M": "재료비", "S": "경비"}
gap = " · ".join(
f"{names[k]} 적용 {a[k] or 0:g} · 당초 {first[k]:g}"
for k in "TLMS"
if a["T"] and first[k] is not None and abs((a[k] or 0) - first[k]) >= 0.5
)
if (
a["T"]
and all(a[k] is not None for k in "LMS")
and abs(a["L"] + a["M"] + a["S"] - a["T"]) < 0.5
and (a["L"] or a["M"] or a["S"])
):
return a
return {k: (_n(r[j]) or 0.0) for k, j in (("T", 25), ("L", 26), ("M", 27), ("S", 28))}
return a, (f"적용 칸 사용 — 당초와 다름 ({gap})" if gap else None)
got = {k: (v or 0.0) for k, v in first.items()}
if not got["T"]:
return got, None # 쓸 값이 없음 — 표지할 것 없음
why = "적용 칸 합이 안 맞음" if a["T"] else "적용 칸 비어 있음"
return got, f"당초 칸 사용 — {why}" + (f" ({gap})" if gap else "")
def applied(r: list[Any]) -> dict[str, float]:
return applied_how(r)[0]
def est_prices(book: dict[str, list]) -> tuple[list[dict[str, Any]], dict[int, dict[str, Any]]]:
@@ -209,8 +225,9 @@ def est_prices(book: dict[str, list]) -> tuple[list[dict[str, Any]], dict[int, d
for r in book.get("단가산출총괄표", [])[3:]:
if len(r) > 28 and str(r[1]).strip().isdigit():
n = int(str(r[1]).strip())
san[n] = {k: float(v or 0) for k, v in applied(r).items()}
san[n] |= {"이름": flat(r[2]), "규격": flat(r[3])}
got, note = applied_how(r)
san[n] = {k: float(v or 0) for k, v in got.items()}
san[n] |= {"이름": flat(r[2]), "규격": flat(r[3]), "칸": note}
for k in "TLMS":
if san[n][k] or k != "T":
one = {"종류": "산근", "이름": f"단산{n}", "규격": ""}