Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
157 lines
6.9 KiB
Python
157 lines
6.9 KiB
Python
"""M01 로직 원문 — 출처 글 → 품셈 본문 절(44-4). 원본은 읽기만."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from M01_MasterData import M01_MasterData_Router as router_module
|
||
from M01_MasterData import M01_MasterData_Store as store
|
||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||
|
||
|
||
def test_build_civil_section_with_table() -> None:
|
||
got = make.source("건설품셈 토목 1-1-2")
|
||
assert len(got["found"]) == 1
|
||
one = got["found"][0]
|
||
assert one["matched"] == "1-1-2"
|
||
assert one["file"].startswith("원가계산/건설공사_표준품셈/본문/02_토목부문/제01장_")
|
||
assert one["blocks"][0]["h"] == 3 and one["blocks"][0]["text"].startswith("1-1-2 유도선")
|
||
table = next(b["table"] for b in one["blocks"] if "table" in b)
|
||
assert table[0][0] == "구분" and ["특별인부", "인", "2", "1,350", "1,560"] in table
|
||
# 다음 절(1-1-3 …) 앞에서 끊김 · 주석 줄 뺌
|
||
assert not any("1-1-3" in b.get("text", "") for b in one["blocks"])
|
||
assert not any(b.get("p", "").startswith("<!--") for b in one["blocks"])
|
||
|
||
|
||
def test_forest_and_many_numbers() -> None:
|
||
forest = make.source("산림품셈 3-4-2 1")["found"]
|
||
assert [x["matched"] for x in forest] == ["3-4-2"]
|
||
many = make.source("건설품셈 공통 8-1-5 · 8-1-6")["found"]
|
||
assert [x["number"] for x in many] == ["8-1-5", "8-1-6"]
|
||
|
||
|
||
def test_no_link() -> None:
|
||
assert make.source("현장 자체 단가")["found"] == []
|
||
assert make.source("건설품셈 토목 99-9-9")["found"] == []
|
||
|
||
|
||
def test_most_master_logics_link() -> None:
|
||
"""정본 로직 출처 대부분이 원문 절에 닿음."""
|
||
hit = total = 0
|
||
for path in store.FOLDER.glob("로직_*품셈_*.json"):
|
||
for row in json.loads(path.read_text(encoding="utf-8"))["줄"]:
|
||
total += 1
|
||
hit += bool(make.source(row.get("출처", ""))["found"])
|
||
assert total > 1000 and hit / total > 0.9, (hit, total)
|
||
|
||
|
||
def test_route() -> None:
|
||
app = FastAPI()
|
||
app.include_router(router_module.router)
|
||
got = TestClient(app).get("/api/m01/logic/source", params={"src": "산림품셈 3-1"}).json()
|
||
assert got["found"][0]["matched"] == "3-1"
|
||
|
||
|
||
def test_tables_split_and_br() -> None:
|
||
"""빈 줄 · 주석이 붙은 표 둘을 가름(49-2) · 칸 안 `<br>` = 줄바꿈."""
|
||
one = make.source("건설품셈 공통 6-1-1")["found"][0]
|
||
tables = [b["table"] for b in one["blocks"] if "table" in b]
|
||
assert [len(t[0]) for t in tables] == [7, 2]
|
||
assert tables[0][2][0] == "인력운반\n타설"
|
||
|
||
|
||
def test_markdown_stripped() -> None:
|
||
"""역슬래시 · 인용 `>` 머리 · 글 안 주석을 벗김(50-2) — 모든 정본 출처 원문에 0."""
|
||
bad = 0
|
||
for path in store.FOLDER.glob("로직_*품셈_*.json"):
|
||
for row in json.loads(path.read_text(encoding="utf-8"))["줄"]:
|
||
for one in make.source(row.get("출처", ""))["found"]:
|
||
for b in one["blocks"]:
|
||
t = b.get("p") or b.get("text") or ""
|
||
bad += "\\" in t or "<!--" in t or t.startswith(">")
|
||
assert bad == 0
|
||
one = make.source("산림품셈 3-4-2 1")["found"][0]
|
||
assert any(b.get("p") == "Q = 3,600 × q × K × f × E / Cm" for b in one["blocks"])
|
||
|
||
|
||
def test_item_marked() -> None:
|
||
"""출처 항 · 주 번호 → 그 조각만 hit(절은 통째) · 못 찾으면 item 없음."""
|
||
|
||
def hits(src: str) -> list[str]:
|
||
one = make.source(src)["found"][0]
|
||
return [(b.get("p") or b.get("text") or "table")[:2] for b in one["blocks"] if b.get("hit")]
|
||
|
||
one = make.source("산림품셈 3-4-2 1")["found"][0]
|
||
assert one["item"] == "1" and one["blocks"][0].get("hit") is None
|
||
assert not any(b.get("hit") for b in one["blocks"] if b.get("p", "").startswith("2."))
|
||
assert hits("산림품셈 3-4-2 1")[0] == "1."
|
||
assert hits("산림품셈 9-7-1 주⑥") == ["⑥ "]
|
||
assert make.source("산림품셈 13-10-2 2")["found"][0]["item"] == "2"
|
||
assert "item" not in make.source("산림품셈 3-4-2")["found"][0]
|
||
|
||
|
||
def test_item_all_marked() -> None:
|
||
"""항 · 주 번호가 달린 정본 출처는 전부 표시가 붙음(전수)."""
|
||
total = missing = 0
|
||
for path in store.FOLDER.glob("로직_*품셈_*.json"):
|
||
for row in json.loads(path.read_text(encoding="utf-8"))["줄"]:
|
||
src = row.get("출처", "")
|
||
if not any(i for _n, i in make._NUMBER.findall(src)):
|
||
continue
|
||
total += 1
|
||
missing += not any(f.get("item") for f in make.source(src)["found"])
|
||
assert total >= 30 and missing == 0, (total, missing)
|
||
|
||
|
||
def _m02_client(role: str = "USER") -> TestClient:
|
||
from common_util.common_util_auth import verify_session
|
||
from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos
|
||
|
||
app = FastAPI()
|
||
app.include_router(combos.router)
|
||
app.dependency_overrides[verify_session] = lambda: {"user_id": 42, "role": role}
|
||
return TestClient(app)
|
||
|
||
|
||
def test_m02_원문_대리_길과_pdf() -> None:
|
||
"""85-4 — 일반 사용자도 원문(M01 길과 같은 몸) · pdf 통째(책 이름만 · 그 밖 400 · 한글 파일명)."""
|
||
from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos
|
||
|
||
client = _m02_client()
|
||
got = client.get("/api/m02/logic-lib/source", params={"src": "산림품셈 3-1"})
|
||
assert got.status_code == 200 and got.json() == make.source("산림품셈 3-1")
|
||
for book in ("건설", "산림"):
|
||
res = client.get("/api/m02/logic-lib/source-pdf", params={"book": book})
|
||
assert res.status_code == 200 and res.headers["content-type"] == "application/pdf"
|
||
assert (
|
||
len(res.content) == combos.SOURCE_PDF[book].stat().st_size
|
||
and res.content[:4] == b"%PDF"
|
||
)
|
||
assert "filename*=utf-8''" in res.headers["content-disposition"]
|
||
again = client.get(
|
||
"/api/m02/logic-lib/source-pdf",
|
||
params={"book": book},
|
||
headers={"if-none-match": res.headers["etag"]},
|
||
)
|
||
assert again.status_code == 304
|
||
for bad in ("../../main.py", "건설품셈", ""):
|
||
assert client.get("/api/m02/logic-lib/source-pdf", params={"book": bad}).status_code == 400
|
||
|
||
|
||
def test_원문_패널_길_주입() -> None:
|
||
"""85-4 — M01 은 옛 길 그대로(기본값) · M02 일위대가 상세는 M02 길 + pdf."""
|
||
from pathlib import Path
|
||
|
||
root = Path(__file__).resolve().parents[2]
|
||
src = (root / "M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts").read_text(encoding="utf-8")
|
||
assert 'opts.path ?? "/m01/logic/source"' in src
|
||
side = (root / "M01_MasterData/M01_MasterData_UI_LogicLab_Side.ts").read_text(encoding="utf-8")
|
||
assert "createSourceView()" in side
|
||
labor = (root / "M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
assert 'path: "/m02/logic-lib/source"' in labor and 'pdf: "/m02/logic-lib/source-pdf"' in labor
|