Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
218 lines
10 KiB
Python
218 lines
10 KiB
Python
"""일위대가 실무자료 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 # 단산 줄은 블록 이음
|