Files
Aislo/resources/tester/test_812_3_native_layout.py
T
eomsangdonandClaude Opus 5.5 3d4b2ec1d1 feat(812-9): 가져온 기계 잇기 다시 — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 범위 규격 · 별칭 먼저 · 조합 기계 둘 · 조회어 기종만
- 새 `M02_Machine_Find` — 기종 = 기계표 이름 · 괄호 붙임 · 상세구분(낱말표 24 안 씀) · 소리 표기 한 글(로우더 = 로더 · 로울러 = 롤러 · 브레이카 = 브레이커 · 굴삭기 = 굴착기 · 램 머 = 래머 …) · 기계표 괄호 낱말로 좁힘(표에 없는 낱말 발파 · 리핑 = 안 봄) · 규격 = 수 + 단위 전부(범위 · 복수 · 인치 = ㎝) · 하나 = 이음 · 여럿 = 「기계 후보 n — 고르기」 · 0 = 「기계 못 찾음」 표지(가까운 규격으로 대신 잇지 않음)
- 별칭 먼저 — 마스터 층 + 지금 층 별칭 장부 기계 짝(이름 + 규격 · 이름만 · 규격 속 할증 글 뺌) · `M02_Doc_Match_Note._ALIAS` 여섯 쌍 걷음(소리 표기 = 정규화 · 같은 기종 = 기계표 · 별칭 장부)
- 조합 기계 = 기계마다 항목 하나(요구 738-3 · 제목 = 그 기계 · 줄 쌓기 차림 `M02_Doc_NativeLayout`) · 줄 둘(「+」 · 괄호 속 다른 기종 · 「…조합」 · 덤프트럭(자동덮개) = 덤프트럭 + 자동덮개시설) — 같은 수량 식 · 엑셀 값은 첫 줄 · 다음 줄 0 + 표지 「조합 기계 — 경비는 한 덩이」 · 우리 단가로 = 각자 시간당 경비
- 원파일 EST · STmate · 엑셀 읽기기 · 일위대가 호표(`Hopyo_Elem` · `Hopyo_Fixed._eqs` · `Hopyo_Row.m01_picks`)가 같은 찾기 · STmate 묶음 기계 줄도 시간당 경비 이음
- 문서 머리 `가져온.기계`(기호마다 판정 · 후보 · 찾기어) · 못 이은 기계 줄도 기계 줄(이름 단추 · `owned_of` 파이썬 · TS 짝) · 이름 단추 조회어 = 기종 이름만(규격 뺌) · 고르면 엑셀 값 기억 그대로(금액 안 바뀜) · 문서 [저장] = 고른 EQ → 이 층 별칭 장부 줄 → M01 별칭 장부 후보(승인 흐름 그대로) · 장부 짝 글 = 「기계 EQ…」
- 마스터 층 별칭 장부(resources/master_template/실무자료/맞춤.json) = 사용자 승인 다섯(요구 735-5 · 736) — 아스팔트페이버(피니셔) = 아스팔트 피니셔 · 디젤 엔진 = 엔진(디젤) · 워터제트 = 워터젯트 · 버킷준설기 = 버킷식준설기 · 바이브레이트 봉상후렉시블 = 콘크리트 진동기 엔진식 ø45(2.6㎾)
- 시험 — 새 test_812_9(예 전부 · 정규화 · 괄호 · 범위 · 조합 둘 · 별칭 먼저 · 표지 · 고르면 별칭 줄 · 금액 전후) · test_812_3 전석쌓기 · 측구터파기 연암 항목을 기계마다 항목으로 고침(조합 기계 둘로 가름 · 요구 738-3 이 바꾼 까닭)
- 전수 목록 docs/raw/요구_1104/_목록_812-9_기계이음.xlsx

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 21:02:29 +09:00

228 lines
9.4 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.
"""812-3 지킴 — 가져온 단가산출(원파일) 줄 쌓기 차림 = 경쟁사 산출근거 꼴(요구 713).
차림 = `M02_Doc_NativeLayout`.
(a) 견본(거창 덤프 꼴) — 1 측구터파기(토사) 「기 계 … 굴삭기」 항목 + 공식 묶음
q · K · f · E · Cm · Q + 보조인부 항목 · 41 합판거푸집(4회) 근거 글 ·
재료비(재료 · 소계 · 율 · 글) · 인건비(인력 · 소계 · 율 · 글) ·
15 전석 메쌓기 별산 · 단산 참조 Q 직접 · 기계 · 인부 ·
2 가 · 나 소항목 · 봉화 STC = 계 단마다 항목
(b) 전수 — 원파일 EST · STC 산근 블록 모두: 차림 전후 셈(비목 · 계 · 줄 금액) 같음 ·
TS 왕복(문서 → 줄 → 문서) 같음 · TS `stackOf` = 파이썬 거울 `stack_of` ·
수 표 = tmp/s8123/sweep.json(일회 기록)
(c) M01 문서 1,518 — 차림 안 바뀜(쌓기 없음 · 왕복 같음 · 거울 같음)
원파일 없으면 그 시험만 skip(다른 PC).
"""
from __future__ import annotations
import json
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
for p in (ROOT, HERE):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
import test_810_2_native_doc as t2 # noqa: E402
from test_810_1_estx import FILES as EST # noqa: E402
from test_810_1_stc import FILES as STC # noqa: E402
from M02_MasterTemplete import M02_Doc_Copy as dc # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
from M02_MasterTemplete import M02_Doc_NativeLayout as lay # noqa: E402
COSTS = ("노무비", "재료비", "경비", "계")
def _ts(cases: list[dict]) -> list[dict]:
proc = subprocess.run(
["node", str(HERE / "helper_812_3_layout.cjs")],
input=json.dumps(dc.ml._norm(cases), ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
)
assert proc.returncode == 0, proc.stderr[-3000:]
return json.loads(proc.stdout)
def _doc(kind: str, tag: str, no: int, laid: bool = True) -> tuple[dict, dict]:
sk = t2.sk_of(t2.fn.emit(t2.native(kind, tag), no))
keep = lay.apply
if not laid:
lay.apply = lambda doc, meta: [x.pop(lay.KEEP, None) for x in doc.get("금액") or []]
try:
return sk, dim.derive(sk, None, t2.whole())
finally:
lay.apply = keep
def _items(doc: dict) -> list[dict]:
return lay.stack_of(doc)
def _show(doc: dict) -> list[tuple]:
"""(제목, 가나다, [줄 글]) — 금액 = 이름 · 공식 = 「=기호…」 · 글 = 「#글」."""
money = doc["금액"]
out = []
for it in _items(doc):
lines = []
for x in it["줄"]:
if x["꼴"] == "금액":
lines.append(money[x["i"]]["이름"])
elif x["꼴"] == "공식":
lines.append("=" + " ".join(x["기호"]))
else:
lines.append("#" + x["글"])
out.append((it["제목"], bool(it.get("가나다")), lines))
return out
# ── (a) 견본 ─────────────────────────────────────────────────────────────
def test_sample_excavation_machine_and_formula() -> None:
_, doc = _doc("EST", "거창", 1)
got = _show(doc)
assert got == [
("기 계(100%) : 굴삭기(무한궤도) (0.7㎥)", False, ["=q K f E Cm Q", "굴삭기(무한궤도)"]),
("보조인부(1일당 1인)", False, ["보통인부"]),
], got
assert [x["묶음"] for x in doc["금액"]] == [got[0][0], got[1][0]]
def test_sample_formwork_text_rate_subtotal() -> None:
_, doc = _doc("EST", "거창", 41)
got = _show(doc)
assert got[0] == (
"",
False,
["#< 토목품셈 6-3-1 합판거푸집(2022개정) >", "#※ 사용횟수 4회 : 보통"],
)
assert got[1] == (
"재료비",
False,
[
"합판(내수),대전",
"외송각재,대전",
"주자재소계",
"#소모자재(박리재등) (주자재비의 9%)",
"율",
],
)
assert got[2][0] == "인건비 (24년 품셈 개정)" and got[2][2][:3] == [
"형틀목공",
"보통인부",
"인부소계",
]
assert got[2][2][3].startswith("#공구손료") and len(got) == 3
sub = next(x for x in doc["금액"] if x["이름"] == "주자재소계")
assert sub["합밖"] and "비목" not in sub # 소계 = 합 밖 · 비목 칸 빈칸 그대로(810-2)
def test_sample_stone_wall_reference_and_direct_q() -> None:
_, doc = _doc("EST", "거창", 15)
got = _show(doc)
titles = [g[0] for g in got]
assert titles[0] == "전석 구입운반 : 별산" and got[0][2] == []
assert (
titles[1].startswith("뒷채움돌 운반 및 채집")
and got[1][2][-1] == "뒷채움돌기초잡석채집운반"
)
assert got[1][2][0].split()[0] == "=Q" # 단산 참조 Q 직접 = 그 항목 공식 묶음
# 조합 기계 「굴삭기 0.6㎥+부착용집게」 = 기계마다 항목 하나 · 제목 = 그 기계(812-9 · 요구 738-3)
assert titles[3] == "굴착기(무한궤도) 0.6㎥" and got[3][2] == ["=Q_3", "굴삭기(무한궤도)"]
assert got[4] == ("부착용 집게 0.6∼0.8", False, ["부착용 집게"])
assert got[5] == ("전석쌓기 인부", False, ["석공", "보통인부"])
assert "합" not in "".join(titles) # 합계 절 = 화면 자동 합계(두 번 안 보임)
def test_sample_stc_steps_are_items() -> None:
_, doc = _doc("STC", "봉화", 14)
got = [
(t, g, [x for x in ls if not x.startswith("=P_")]) for t, g, ls in _show(doc)
] # 손 단가 = 기본 자리
assert got == [
("", False, ["#▣ 토공의 비탈규준틀[개소]2-6-1"]),
("재 료 비", False, ["각재(거푸집용,외송)", "판재", "일반못(철못)"]),
("인 건 비", False, ["건축목공", "보통인부"]),
], got
def test_sample_ganada_sub_items() -> None:
_, doc = _doc("EST", "거창", 2)
got = _show(doc)
assert got[0] == ("기 계(100%)", False, [])
# 「가. 굴삭기(무한궤도)(0.7+브레이카)」 = 조합 기계 둘 = 가나다 항목 둘(812-9 · 요구 738-3)
assert got[1][:2] == ("굴착기(무한궤도) 0.7㎥", True)
assert got[2] == ("대형 브레이커 0.7㎥", True, ["대형 브레이커"])
assert got[3] == ("치즐(0.7㎥용)", True, ["치즐(0.7㎥)"]) # 「다. 소 계」 절 = 뺌
# ── (b) 전수 ─────────────────────────────────────────────────────────────
def _money(sk: dict, doc: dict) -> tuple:
got = dim.run(doc, sk, t2.whole())
lines = sorted(str(x.get("금액")) for x in got.get("줄") or [])
return tuple(str(got.get(c)) for c in COSTS), lines
@lru_cache(maxsize=1)
def _sweep() -> dict:
stat = {"블록": 0, "금액같음": 0, "다름": [], "cases": []}
for kind, files in (("EST", EST), ("STC", STC)):
for tag, path in files.items():
if not path.exists():
continue
for no in t2.fn.numbers(t2.native(kind, tag)):
sk, doc = _doc(kind, tag, no)
_, plain = _doc(kind, tag, no, laid=False)
stat["블록"] += 1
if _money(sk, doc) == _money(sk, plain):
stat["금액같음"] += 1
else:
stat["다름"].append((kind, tag, no))
stat["cases"].append(
{"키": f"{kind}|{tag}|{no}", "문서": doc, "파이썬": _items(doc)}
)
return stat
def test_sweep_money_same_and_ts_round_trip() -> None:
stat = _sweep()
if not stat["블록"]:
pytest.skip("원파일 없음")
assert not stat["다름"] and stat["금액같음"] == stat["블록"], stat["다름"][:5]
got = _ts(stat["cases"])
bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")}
assert bad == {"같음": [], "거울": []}, {k: v[:5] for k, v in bad.items()}
by = {}
for g in got:
kind = g["키"].split("|")[0]
row = by.setdefault(
kind, dict.fromkeys(("블록", "항목", "제목", "공식", "글", "쌓기", "무제목만"), 0)
)
row["블록"] += 1
for f in ("항목", "제목", "공식", "글"):
row[f] += g[f]
row["쌓기"] += g["쌓기"]
row["무제목만"] += g["항목"] > 0 and g["제목"] == 0
out = ROOT / "tmp" / "s8123"
out.mkdir(parents=True, exist_ok=True)
(out / "sweep.json").write_text(json.dumps(by, ensure_ascii=False, indent=1), "utf-8")
assert by.get("EST", {}).get("제목", 0) > by.get("EST", {}).get("블록", 0)
# ── (c) M01 ──────────────────────────────────────────────────────────────
def test_m01_docs_layout_unchanged() -> None:
docs = dc.folder()
assert len(docs) >= 1518
cases = [{"키": k, "문서": d, "파이썬": _items(d)} for k, d in docs.items()]
got = _ts(cases)
bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")}
assert bad == {"같음": [], "거울": []}, {k: v[:5] for k, v in bad.items()}
assert not any(g["쌓기"] for g in got)