"""일위대가 실무자료 STmate 꼴 읽기(PLAN 338-2b · `M02_Hopyo_Stmate`). 여섯 파일 올라감(봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표) · 봉화 줄 216(구성 209 + 근거글 7) · 단산 103 · 노임 30 · 호표 22 · 줄 코드 갈래 · 노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠(M~Z 열 코드 꼴 첫 것) · 문서 보안(장수 · 진안) 400 글 · 원본 엑셀 · master_data 그대로 · 영월 정답 자리(`y_ids` — 338 실험 C1 `labels_y` Y000~Y035 = D · X 고유 줄 차례 · 골든 시험이 씀). """ import hashlib from pathlib import Path import pytest from M02_MasterTemplete import M02_Hopyo_Basis as hb from M02_MasterTemplete import M02_Hopyo_Excel as hx from M02_MasterTemplete import M02_Hopyo_Stmate as st from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester import test_m02_hopyo_excel as base from resources.tester.test_m02_hopyo_excel import ROOT, _post, master_hash client = base.client # 공용 고정물(임시 층) DOCS = ROOT / "resources" / "knowledge" / "original" / "실무문서" FILES = { "봉화": "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", "영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", "대흥설계": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/" "2024년 간선임도사업(기번3).xlsx", "대흥착공": "2024년 간선임도(기번3-울진.대흥)(공통)/" "2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", "영덕": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/" "2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", "소광": "25년 산불진화임도(기번8)(울진소광)/" "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", } HOPYOS = {"봉화": 45, "영월": 23, "대흥설계": 34, "대흥착공": 34, "영덕": 9, "소광": 25} LOCKED = { "장수": "02.산불 장수/01.내역서/" "00.단가변경_26년_산불_무주(장수,장수,덕산,산54-1)내역+수량.xlsx", "진안": "03.산불진안/01.내역서/00.단가변경_26년_산불_무주(진안,백운,백암,산1-2)내역+수량.xlsx", } def _path(tag: str) -> Path: return DOCS / {**FILES, **LOCKED}[tag] @pytest.fixture(scope="module") def books() -> dict[str, dict]: return {tag: hx.read_book(_path(tag).name, _path(tag).read_bytes()) for tag in FILES} def _lines(book: dict) -> list[dict]: return [x for h in book["호표"] for x in h["줄"]] def test_여섯_파일_호표_수(books): assert {tag: len(b["호표"]) for tag, b in books.items()} == HOPYOS for book in books.values(): assert (book["꼴"], book["시트"]) == ("STmate", "일위대가표") assert book["검산"] == {"어긋남": [], "총괄표": 0} assert len(_lines(books["대흥설계"])) == len(_lines(books["대흥착공"])) == 171 # 같은 설계 def test_봉화_줄_갈래(books): bong = books["봉화"] lines = _lines(bong) kinds = {k: sum(1 for x in lines if x["갈래"] == k) for k in ("단가산출", "노임", "호표", "%")} assert kinds == {"단가산출": 103, "노임": 30, "호표": 22, "%": 7} assert len(lines) + sum(len(h.get("근거글") or []) for h in bong["호표"]) == 216 assert all(x["코드"] for x in lines) for book in books.values(): for x in _lines(book): if x["갈래"] == "호표": assert x["참조"]["번"] in {h["번"] for h in book["호표"]} if x["갈래"] == "단가산출": assert x["참조"] == {"종류": "단산", "번": int(x["코드"][1:]), "코드": x["코드"]} assert "근거" in x # 블록 모두 이어짐 by = {h["번"]: h for h in bong["호표"]} assert by[13]["근거글"] == ["건설표준품셈 7-2-2(찰붙임)"] # 코드 없는 줄 = 호표 근거글 assert hb.sources(" ".join(by[13]["근거글"])) == [["건설품셈", "7-2-2"]] def test_영월_줄과_블록(books): yw = books["영월"] one = yw["호표"][0] assert (one["번"], one["코드"], one["이름"], one["단위"]) == ( 1, "B00260", "녹생토(종자배합)", "M3", ) assert one["행"] == [5, 10] and one["금액"]["계"] == 155184 assert [(x["이름"], x["코드"], x["갈래"], x["수량"]) for x in one["줄"]] == [ ("산림종자", "M00024", "자재", 0.25), ("산림종자", "M00211", "자재", 0.25), ("인공토양(녹생토)", "M00832", "자재", 1.1), ("보통인부", "L00002", "노임", 0.43), ] two = {x["이름"]: x for x in yw["호표"][1]["줄"]} assert two["국가지점번호판 검증수수료"]["갈래"] == "별도계상" # 비고 「별산」 assert two["구조물터파기(인력)"]["근거"] == { "출처": [["건설품셈", "3-1-3"]], "직종": ["보통인부"], "행": [5, 54], } block = next(b for b in yw["단산"] if b["코드"] == "D00002") assert (block["번"], block["이름"], block["규격"], block["단위"]) == ( 2, "구조물터파기(인력)", "경질토사", "M3", ) assert block["자원"] == [ {"갈래": "L", "코드": "L00002", "이름": "보통인부", "규격": "", "품": 0.26, "절": ""} ] assert any("건설표준품셈3-1-3" in t for t in block["글"]) def test_기계_자원_하나로(books): block = next(b for b in books["봉화"]["단산"] if b["코드"] == "D00002") machines = [r for r in block["자원"] if r["갈래"] == "X"] assert len(machines) == 1 # 노무비 · 재료비 · 경비 셋 줄 assert machines[0]["이름"] == "굴삭기(무한궤도)" and machines[0]["규격"] == "0.2㎥" assert machines[0]["품"] == pytest.approx(0.0636537237529) def test_노임_이름_어긋남_0(books): names = hb.trade_names() for tag in FILES: sheets = hx._sheets(_path(tag).name, _path(tag).read_bytes()) table = st._lists(sheets) res = [r for b in books[tag]["단산"] for r in b["자원"] if r["갈래"] == "L" and r["이름"]] assert res assert sum(1 for r in res if r["코드"] in table) >= len(res) // 2 for r in res: if r["코드"] in table: # 목록표에 있는 직종 — X 열 이름과 같음 assert st._flat(r["이름"]) == st._flat(table[r["코드"]]["이름"]) if table.get(r["코드"], {}).get("단위") == "인": # L 코드 % 줄(기구손료) 뺌 assert st._flat(r["이름"]) in names, (tag, r) labor = [x for x in _lines(books[tag]) if x["갈래"] == "노임"] assert all(st._flat(x["이름"]) in names for x in labor) def test_이음_열쇠_코드_칸(): row = [None] * 26 row[12], row[14], row[15] = "별산M00985", "M00985_1", "T7_1" assert st._code(row) == "M00985" row = [None] * 26 row[25] = "D01352 →" # O 열 없는 판 assert st._code(row) == "D01352" row = [None] * 47 row[14], row[13] = "PRA01_1", "_x0007_`COD|PR_x0005_`EXI" assert st._code(row) == "PRA01" assert ( st._code(["계", None, None, None, None, None, None, None] + [None] * 6 + ["T7_1"]) == "T7" ) assert st._code([None] * 12 + ["D96"]) is None # 비고 글은 코드 아님 def test_문서_보안_400(client): for tag in LOCKED: data = _path(tag).read_bytes() with pytest.raises(hx.ExcelError, match="문서 보안"): hx.read_book(_path(tag).name, data) res = _post(client, data=data, name=_path(tag).name) assert res.status_code == 400 and res.json()["detail"] == hx.DRM def test_여섯_파일_올라감(client): before = master_hash() sources = {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} for at, tag in enumerate(FILES, 1): res = _post(client, data=_path(tag).read_bytes(), name=_path(tag).name) assert res.status_code == 200, res.text assert res.json()["호표수"] == HOPYOS[tag] doc = hs.read(layers.personal_dir(7, 42), f"HP-{at:06d}") assert doc["꼴"] == "STmate" assert sum(1 for b in doc["단산"] if not b["글"]) <= 3 # 봉화 무대처리 · 흙 쌓 기 = 값 0 assert any(b["자원"] for b in doc["단산"]) first = doc["호표"][0]["번"] view = client.get(f"{base.URL.format('personal')}/HP-{at:06d}/{first}") assert view.status_code == 200, view.text # 맞춤 보기가 STmate 줄을 받음 assert {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} == sources assert master_hash() == before def y_ids(book: dict) -> dict[str, dict]: """영월 정답 자리 — D · X 줄을 (이름, 규격, 단위, 코드) 고유로 처음 나온 차례 → `{"Y000": {호표, 차례, 열쇠}}`(338 실험 C1 `s09_stmate` 와 같은 번).""" out: dict[str, dict] = {} seen: set[tuple] = set() for hopyo in book["호표"]: for line in hopyo["줄"]: key = (line["이름"], line["규격"], line["단위"], line["코드"]) if line["코드"][0] in "DX" and key not in seen: seen.add(key) out[f"Y{len(out):03d}"] = {"호표": hopyo["번"], "차례": line["차례"], "열쇠": key} return out def test_영월_정답_자리(books): got = y_ids(books["영월"]) assert len(got) == 36 # 정답 34 + 우리에 없음 둘(Y004 · Y007) assert got["Y000"] == { "호표": 2, "차례": 1, "열쇠": ("무근콘크리트타설", "인력운반타설", "M3", "D02058"), } assert got["Y005"]["열쇠"] == ("크레인(타이어)", "10톤", "시간", "X02104") assert got["Y010"]["열쇠"][3] == "D00060" and got["Y035"]["열쇠"][3] == "D01810" by = {h["번"]: h for h in books["영월"]["호표"]} for one in got.values(): line = by[one["호표"]]["줄"][one["차례"]] assert (line["이름"], line["규격"], line["단위"], line["코드"]) == one["열쇠"] assert line["갈래"] == "기계" or "근거" in line # 단산 줄은 블록 이음