Files
Aislo/resources/tester/test_534_3_e05_report.py
T
eomsangdonandClaude Opus 5.5 e7afaa6664 feat(534-3): E05 안정성평가 보고서 — 빌더 · storage · 길 · 화면
- 빌더: E02 지반 · E03 2D 요약 · 측점별 저장본 · 대표 단면 · E04 3D 요약 → 프로그램 몫 시트(입력조건 · 측점표 · 대표단면 · 3D요약) · 사용자 몫(표지 · 개요)은 처음 한 번 · N.G 서식 10 · O.K 서식 11 직접 · 해석 지문
- 그림: 서버 SVG(지층 · 암 경계 · 우기 지하수위 · 임계면 · 노폭 · 비탈 높이 · 평면 위험 띠) → PNG(공용 svg_png) · fig/<측점 키>.png
- storage: 작업본 report.json · meta(지문 · 시트 서명) · 초기값 initial_snapshot · 갱신 덮임 목록 · 옛 판 _지난/
- 길: GET(없으면 처음 만듦) · PUT 판 409 + 서버 재계산 · refresh · reset · xlsx · pdf(POST 화면 책 · GET 작업본) · 그림
- 화면: 내장 엑셀(시트 탭 · 그림 겹판) · 캐시 · [저장] · [초기화] · [보고서 갱신] · 해석 바뀜 띠 · [엑셀] · [PDF] · [PDF 미리보기]
- 시험 test_534_3_e05_report 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:34:40 +09:00

297 lines
12 KiB
Python

"""534-3(빌더 · storage) — E05 안정성평가 보고서 `E05_StabReport_Build` · `_Store`.
가짜 E 프로젝트(2D 요약 · 측점별 저장본 · 대표 단면 · 지반 조건 · 3D 저장본 하나 · 복사본 읽기는 바꿔 끼움)로
측점 수 = 줄 수 · N.G 수 · 그림 파일 수 · 갱신 뒤 사용자 몫 불변 · 덮임 목록 · 초기화 · xlsx 되읽기 · PDF 쪽 수.
"""
import io
import json
import shutil
from pathlib import Path
import pytest
from openpyxl import load_workbook
from common_util.common_util_print_pdf import PrintError
from common_util.common_util_spreadsheet import workbook_pdf
from common_util.common_util_workbook_xlsx import workbook_to_xlsx
from E02_StabGround import E02_StabGround_Model as ground_model
from E05_StabReport import E05_StabReport_Build as build
from E05_StabReport import E05_StabReport_Figure as figure
from E05_StabReport import E05_StabReport_Store as store
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
N = 30 # 측점 수
NG_AT = {3, 10, 17, 24} # N.G 측점
REPS = [("grid:3", "lowest_fs"), ("grid:12", "cut_15m"), ("grid:20", "fill_10m")]
def _key(i: int) -> str:
return f"grid:{i}"
def _summary(i: int, bump: float = 0.0) -> dict:
ng = i in NG_AT
dry = 1.31 if ng else 1.7 + i / 100 + bump
return {
"station_key": _key(i),
"chainage_m": 20.0 * i,
"side": "cut" if i % 2 else "fill",
"fs_min": dry - 0.2,
"fs_by_condition": {"dry": dry, "wet": dry - 0.2},
"method": "bishop",
"verdict": "ng" if ng else "ok",
}
def _slip(F: float) -> dict:
pts = [[-6.0 + k, 101.0 - 0.3 * (6 - abs(k - 6))] for k in range(13)]
return {"F": F, "kind": "circle", "points": pts, "center": [0.0, 112.0], "radius": 12.5}
def _station_doc(i: int) -> dict:
s = _summary(i)
cases = []
for cond in ("dry", "wet"):
F = s["fs_by_condition"][cond]
for side, role in (("left", "cut"), ("right", "fill")):
cases.append(
{
"condition": cond,
"side": side,
"role": role,
"direction": -1 if side == "left" else 1,
"limits": [-10.0, 10.0],
"height_m": 6.0,
"min_F": {"bishop": F},
"result": {"methods": {"bishop": {"min": _slip(F)}}},
}
)
return {"chainage_m": s["chainage_m"], "label": f"No.{i}", "cases": cases}
def _cross(chainage: float) -> dict:
samples = [{"offset_m": float(x), "z": 100.0 + 0.4 * x, "valid": True} for x in range(-20, 21)]
line = [
{"offset_m": 8.0, "elevation_m": 103.2},
{"offset_m": 2.0, "elevation_m": 100.0},
{"offset_m": -2.0, "elevation_m": 100.0},
{"offset_m": -6.0, "elevation_m": 97.6},
]
return {
"chainage_m": chainage,
"label": f"No.{int(chainage // 20)}",
"samples": samples,
"design": {
"design_line": line,
"road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}},
"ground_type": "ripping_rock",
},
}
@pytest.fixture
def project(tmp_path, monkeypatch) -> Path:
root = tmp_path / "e_project"
d2 = root / "E03_StabSection" / "_derived"
(d2 / "results").mkdir(parents=True)
(d2 / "result2d.json").write_text(
json.dumps(
{
"input_fingerprint": "fp-1",
"computed_at": "2026-10-25T10:00:00",
"stations": [_summary(i) for i in range(N)],
}
),
encoding="utf-8",
)
for key, _ in REPS:
i = int(key.split(":")[1])
(d2 / "results" / f"grid_{i}.json").write_text(
json.dumps(_station_doc(i)), encoding="utf-8"
)
(root / "E03_StabSection" / "representative.json").write_text(
json.dumps(
{
"rule": {"fill_h_m": 10, "cut_h_m": 15, "lowest_n": 5},
"stations": [{"station_key": k, "reason": r, "enabled": True} for k, r in REPS]
+ [{"station_key": _key(5), "reason": "user", "enabled": False}],
}
),
encoding="utf-8",
)
ground = ground_model.pick(ground_model.load_defaults())
ground["stations"] = [
{"chainage_m": 20.0 * i, "ground_type": "ripping_rock", "soil_thickness_m": 1.5}
for i in range(N)
]
ground["zones"] = [
{"from_m": 0, "to_m": 100, "soil": "colluvium", "rock": None, "soil_thickness_m": None}
]
path = ground_model.ground_path(root)
path.parent.mkdir(parents=True)
path.write_text(json.dumps(ground), encoding="utf-8")
r3 = root / "E04_Stab3D" / "results"
r3.mkdir(parents=True)
(r3 / "0.00_200.00.json").write_text(
json.dumps(
{
"fingerprint": "3d-1",
"settings": {},
"summary": {"start_m": 0.0, "end_m": 200.0, "f_min": 1.42, "volume": 830.0},
}
),
encoding="utf-8",
)
stations = [
{"chainage_m": 20.0 * i, "label": f"No.{i}", "center_x": 10.0 * i, "center_y": 3.0 * i}
for i in range(N)
]
db = {
"project": {"name": "검증 임도", "region": "울진군", "road_type": "산불진화임도"},
"route_points": [
{"sequence_num": i, "model_x": 10.0 * i, "model_y": 3.0 * i, "chainage_m": 20.0 * i}
for i in range(N)
],
}
monkeypatch.setattr(build.source, "db_snapshot", lambda _root: db)
monkeypatch.setattr(build.source, "stations", lambda _root: stations)
monkeypatch.setattr(build.source, "cross_section", lambda _root, ch: _cross(ch))
return root
def _sheets(book: dict) -> dict:
return {s["이름"]: s for s in book["시트"]}
def _create(root: Path) -> dict:
try:
return store.create(root)
except PrintError as error:
pytest.skip(f"브라우저 없음 — {error}")
def test_처음_만듦_측점수_NG_그림(project):
meta = _create(project)
book, saved = store.load(project)
by = _sheets(book)
rows = [a1 for a1, c in by["측점표"]["칸"].items() if a1.startswith("A") and "값" in c]
rows = [a1 for a1 in rows if int(a1[1:]) >= 6]
assert len(rows) == N # 측점 수 = 줄 수
j = [c for a1, c in by["측점표"]["칸"].items() if a1.startswith("J") and int(a1[1:]) >= 6]
assert sum(c.get("값") == "N.G" and c["서식"] == build.ST_NG for c in j) == len(NG_AT)
assert sum(c.get("값") == "O.K" and c["서식"] == build.ST_OK for c in j) == N - len(NG_AT)
figs = sorted((project / "E05_StabReport" / "fig").glob("*.png"))
assert len(figs) == len(REPS) + 1 and meta["figures"][-1] == build.BAND_KEY
assert [f["열쇠"] for f in by["대표단면"]["그림"]] == [build.fig_key(k) for k, _ in REPS]
assert by["대표단면"]["인쇄"]["쪽나눔"] == [28, 56, 84]
assert by["대표단면"]["칸"]["B30"]["값"] == "깎기 높이 15 m 이상"
assert by["대표단면"]["칸"]["E24"] == {"값": "N.G", "서식": build.ST_NG} # No.3 건기 1.31
assert by["대표단면"]["칸"]["F24"]["값"] == 12.5
assert by["표지"]["칸"]["C8"]["값"] == "검증 임도"
vals = book["계산값"]["s2"]
assert vals["B5"] == {"수": str(N)} and vals["B15"] == {"수": str(len(NG_AT))}
# 입력조건 — 물성 여섯 + 구간 한 줄 · 3D 한 줄
s3 = by["입력조건"]["칸"]
assert s3["A26"]["값"] == "풍화토" and s3["B32"]["값"].startswith("0 ~ 100 m 위 지층")
assert s3["A32"]["값"] == "붕적토" and s3["B13"]["값"] == 1.3
s6 = by["3D요약"]["칸"]
assert s6["D20"]["값"] == 1.42 and s6["H20"] == {"값": "N.G", "서식": build.ST_NG}
assert s6["E20"]["값"] == pytest.approx(1.11) # 0~200 m 안 2D 최소
assert (build.initial_dir(project) / build.REPORT).is_file()
assert saved["fingerprint"] == build.analysis_fingerprint(project)
def test_갱신_사용자몫_불변_덮임(project):
_create(project)
book, _ = store.load(project)
by = _sheets(book)
by["표지"]["칸"]["C10"] = {"값": "산림청", "서식": 5}
by["개요"]["칸"]["A22"] = {"값": "사용자 의견", "서식": 15}
store.save(project, book)
out = store.refresh(project)
assert out["overwritten"] == [] # 프로그램 몫을 안 고침
book, _ = store.load(project)
by = _sheets(book)
by["측점표"]["칸"]["A6"]["값"] = "고친 측점"
store.save(project, book)
r2 = project / build.RESULT2D
doc = json.loads(r2.read_text(encoding="utf-8"))
doc["stations"][3] = _summary(4, 0.5) | {"station_key": _key(3), "chainage_m": 60.0}
doc["input_fingerprint"] = "fp-2"
r2.write_text(json.dumps(doc), encoding="utf-8")
assert build.analysis_fingerprint(project) != store.load(project)[1]["fingerprint"]
try:
out = store.refresh(project)
except PrintError as error:
pytest.skip(f"브라우저 없음 — {error}")
assert out["overwritten"] == ["측점표"]
assert len(list((project / "E05_StabReport" / "_지난").glob("*.json"))) == 1
book, meta = store.load(project)
by = _sheets(book)
assert (
by["표지"]["칸"]["C10"]["값"] == "산림청" and by["개요"]["칸"]["A22"]["값"] == "사용자 의견"
)
assert by["측점표"]["칸"]["A6"]["값"] == "No.0" # 프로그램 몫은 다시 씀
assert book["계산값"]["s2"]["B15"] == {"수": str(len(NG_AT) - 1)} # 수식이 새 값을 따라감
assert meta["fingerprint"] == build.analysis_fingerprint(project)
assert store.reset(project)
book, _ = store.load(project)
assert (
"C10" not in _sheets(book)["표지"]["칸"] or "값" not in _sheets(book)["표지"]["칸"]["C10"]
)
def test_3D_없음_2D_없음(project):
shutil.rmtree(project / "E04_Stab3D")
inputs = build.gather(project)
book = build.form()
build.build_program(book, inputs)
by = _sheets(book)
assert by["3D요약"]["칸"]["A20"]["값"] == "3D 해석 전"
assert by["입력조건"]["칸"]["B7"]["값"] == "3D 해석 전"
(project / build.RESULT2D).unlink()
with pytest.raises(build.MissingInput):
build.gather(project)
def test_xlsx_되읽기_pdf_쪽수(project):
_create(project)
book, _ = store.load(project)
figs = store.figures_of(project, book)
assert len(figs) == len(REPS) + 1
wb = load_workbook(io.BytesIO(workbook_to_xlsx(book, figs)))
assert wb.sheetnames == ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약"]
assert len(wb["대표단면"]._images) == len(REPS) and len(wb["3D요약"]._images) == 1
assert [b.id for b in wb["대표단면"].row_breaks.brk] == [28, 56, 84]
assert wb["측점표"]["J9"].value == "N.G" and wb["측점표"]["J9"].fill.fgColor.rgb.endswith(
"FFC7CE"
)
book.pop("계산값", None)
try:
pdf = workbook_pdf(book, figs)
except PrintError as error:
pytest.skip(f"브라우저 없음 — {error}")
from pypdf import PdfReader
texts = [p.extract_text() for p in PdfReader(io.BytesIO(pdf)).pages]
stab = [i for i, t in enumerate(texts) if "산정 안전율" in t]
assert stab and all("산정 안전율" in texts[i] for i in stab)
# 표지 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 3 + 3D 1
assert len(texts) == 3 + len(stab) + len(REPS) + 1
def test_단면_그림_SVG():
svg = figure.section_svg(
title="No.3",
ground=[(-10.0, 96.0), (10.0, 104.0)],
surface=[(-10.0, 96.0), (-2.0, 100.0), (2.0, 100.0), (10.0, 104.0)],
rock=[(-10.0, 94.5), (10.0, 102.5)],
slips=[{"condition": "dry", "F": 1.3, "points": [[-5.0, 98.0], [0.0, 97.0], [5.0, 102.0]]}],
road=(-2.0, 2.0),
heights=[{"x": 10.0, "z0": 100.0, "z1": 104.0}],
wet=True,
)
assert svg.startswith("<svg") and "건기 임계면 Fs 1.30" in svg and "노폭 4.00 m" in svg
assert "H 4.00 m" in svg and svg.count("<polyline") >= 4