시험 고침: 534-3 양식 시험의 시트 목록 · 쪽 수 기대(목차 · 부록 더함) · E05 시험 시트 수 7 → 9 · PDF = report_pdf(목차 쪽 번호 = 실제 · 꼬리말 · 머리말) · 부록 절편 · 측점표 쪽 나눔 · 옛 작업본 갱신 시험 덧. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
535 lines
23 KiB
Python
535 lines
23 KiB
Python
"""534-3 · 534-5(빌더 · storage · 길 · 인쇄) — E05 안정성평가 보고서 `E05_StabReport_Build` ·
|
|
`_Section` · `_Print` · `_Store` · `_Router`.
|
|
|
|
가짜 E 프로젝트(E03 파생 저장본 꼴 2D 요약 · 측점별 저장본 · 대표 단면 켬/끔 · 지반 조건 ·
|
|
3D 저장본 하나 · 복사본 읽기는 바꿔 끼움)로 측점 수 = 줄 수 · N.G 수 · 그림 파일 수 ·
|
|
갱신 뒤 사용자 몫 불변 · 덮임 목록 · 초기화 · 판 409 · xlsx 되읽기 · PDF 쪽 수 ·
|
|
목차 쪽 번호 = 실제 · 부록 절편 · 측점표 쪽 나눔 고르게 · 옛 작업본 갱신(목차 자리 · 서식 붙임).
|
|
"""
|
|
|
|
import io
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from openpyxl import load_workbook
|
|
|
|
from common_util.common_util_derived_store import derived_dir, write_derived
|
|
from common_util.common_util_print_pdf import PrintError
|
|
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_Print as printer
|
|
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"), ("grid:12", "cut_height"), ("grid:20", "fill_height")]
|
|
# 자동 대표(높이 · 하위 5) 중 REPS 만 남기는 사용자 끔
|
|
OFF = {"grid:0": False, "grid:5": False, "grid:10": False, "grid:17": False, "grid:24": False}
|
|
KIND_SUMMARY, KIND_STATION = "stab2d_summary", "stab2d_station"
|
|
SLICES = 24 # 임계면 절편 수
|
|
REQUIRED = {"dry": {"cut": 1.5, "fill": 1.5}, "wet": {"cut": 1.2, "fill": 1.3}}
|
|
|
|
|
|
def _key(i: int) -> str:
|
|
return f"grid:{i}"
|
|
|
|
|
|
def _summary(i: int, bump: float = 0.0) -> dict:
|
|
"""E03 요약 줄 꼴 — 지배 비탈(홀수 = 깎기 왼쪽 · 짝수 = 쌓기 오른쪽)만 낮은 안전율."""
|
|
ng = i in NG_AT and not bump
|
|
dry = 1.31 if ng else 1.7 + i / 100 + bump
|
|
role = "cut" if i % 2 else "fill"
|
|
side = "left" if role == "cut" else "right"
|
|
other = "right" if side == "left" else "left"
|
|
heights = {"cut": 15.0 if i == 12 else 6.0, "fill": 10.0 if i == 20 else 6.0}
|
|
fs = {"dry": dry, "wet": dry - 0.2}
|
|
cases = {
|
|
cond: {
|
|
side: {"fs": fs[cond], "required": REQUIRED[cond][role]},
|
|
other: {"fs": 9.0, "required": REQUIRED[cond]["fill" if role == "cut" else "cut"]},
|
|
}
|
|
for cond in fs
|
|
}
|
|
return {
|
|
"key": _key(i),
|
|
"chainage_m": 20.0 * i,
|
|
"label": f"No.{i}",
|
|
"fp": f"st{i}",
|
|
"sides": [
|
|
{"side": "left", "role": "cut", "height_m": heights["cut"]},
|
|
{"side": "right", "role": "fill", "height_m": heights["fill"]},
|
|
],
|
|
"cases": cases,
|
|
"min": {c: min(v["fs"] for v in cases[c].values()) for c in cases},
|
|
"verdict": {
|
|
c: "ok" if all(v["fs"] >= v["required"] for v in cases[c].values()) else "ng"
|
|
for c in cases
|
|
},
|
|
"error": None,
|
|
}
|
|
|
|
|
|
def _write_summary(root: Path, fp: str, rows: list[dict]) -> None:
|
|
value = {
|
|
"fingerprint": fp,
|
|
"computed_at": "2026-10-25T10:00:00",
|
|
"conditions": ["dry", "wet"],
|
|
"method": "bishop",
|
|
"stations": rows,
|
|
}
|
|
write_derived(root, KIND_SUMMARY, fp, value)
|
|
|
|
|
|
def _slice(i: int) -> dict:
|
|
return {
|
|
"width": 0.5, "height": 1.0 + i / 10, "alpha_deg": 10.0 + i, "weight": 9.0 + i,
|
|
"surcharge": 0.0, "pore_pressure": 2.0, "base_length": 0.51, "c": 15.0,
|
|
"phi_deg": 30.0, "normal_eff": 8.0 + i, "shear_mobilized": 6.0 + i / 10,
|
|
} # fmt: skip
|
|
|
|
|
|
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,
|
|
"slices": [_slice(i) for i in range(SLICES)],
|
|
}
|
|
|
|
|
|
def _station_doc(i: int) -> dict:
|
|
s = _summary(i)
|
|
cases = []
|
|
for cond in ("dry", "wet"):
|
|
F = s["min"][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"
|
|
_write_summary(root, "fp-1", [_summary(i) for i in range(N)])
|
|
for key, _ in REPS:
|
|
i = int(key.split(":")[1])
|
|
write_derived(root, KIND_STATION, f"st{i}", {"out": _station_doc(i), "error": None}, keep=N)
|
|
(root / "E03_StabSection").mkdir()
|
|
(root / "E03_StabSection" / "representative.json").write_text(
|
|
json.dumps({"overrides": OFF}), 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,
|
|
"condition": "wet",
|
|
"reference_fs": 1.3,
|
|
"verdict": "ok",
|
|
},
|
|
}
|
|
),
|
|
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)
|
|
k = [
|
|
c.get("값")
|
|
for a1, c in by["측점표"]["칸"].items()
|
|
if a1.startswith("K") and int(a1[1:]) >= 6
|
|
]
|
|
assert k.count("대책 검토") == len(NG_AT) and k.count("-") == N - len(NG_AT)
|
|
# 측점표 쪽 나눔 = 고르게(덩이 크기 차 ≤ 1 · 한 줄 꼬리 없음)
|
|
cuts = by["측점표"]["인쇄"]["쪽나눔"]
|
|
sizes = [b - a for a, b in zip([5, *cuts], [*cuts, 5 + N])]
|
|
assert len(sizes) >= 2 and max(sizes) - min(sizes) <= 1
|
|
# 부록 절편 — 대표 단면마다 지배 조건 한 덩이 · 저항력 = 동원 전단 × Fs · 덩이 끝 쪽 나눔
|
|
s10 = by["부록_절편"]["칸"]
|
|
block = 3 + SLICES + 1
|
|
assert by["부록_절편"]["인쇄"]["쪽나눔"] == [block * (i + 1) for i in range(len(REPS))]
|
|
assert "건기" in s10["A1"]["값"] and s10["A1"]["값"].startswith("부록 ① 절편 계산표 — No.3")
|
|
assert s10["A4"]["값"] == 1 and s10["M4"]["값"] == pytest.approx(6.0 * 1.31)
|
|
assert s10[f"A{block}"]["값"] == "합계"
|
|
assert s10[f"E{block}"]["값"] == pytest.approx(sum(9.0 + i for i in range(SLICES)))
|
|
assert book["서식"][s10["B4"]["서식"]]["크기"] == 8
|
|
# 목차 — 본문 시트 차례 · 쪽 번호(목차 = 1)
|
|
toc = by["목차"]["칸"]
|
|
assert [toc[f"B{r}"]["값"] for r in (4, 5)] == [printer.TITLES["s2"], printer.TITLES["s3"]]
|
|
assert toc["A10"]["값"] == "부록" # 보강검토 없음 → 본문 6 + 부록 and toc["C4"]["값"] == 2
|
|
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"] == {"값": "O.K", "서식": build.ST_OK} # E04 판정
|
|
assert s6["E20"]["값"] == pytest.approx(1.11) # 0~200 m 안 2D 우기 최소(3D 와 같은 조건)
|
|
assert (build.initial_dir(project) / build.REPORT).is_file()
|
|
assert saved["fingerprint"] == build.analysis_fingerprint(project)
|
|
|
|
|
|
def test_광역요약_E07_저장본(project):
|
|
"""E07 저장본 없음 = 「광역 해석 전」 · 생김 = 지문 바뀜 → 갱신 뒤 등급 면적 · 위험 측점 줄."""
|
|
_create(project)
|
|
book, meta = store.load(project)
|
|
assert _sheets(book)["광역요약"]["칸"]["B5"]["값"].startswith("광역 해석 전")
|
|
keys = ("stable", "moderate", "quasi", "lower", "upper", "defended")
|
|
area = {
|
|
"settings": {
|
|
"band_m": 100.0,
|
|
"cell_m": 2.0,
|
|
"gamma_t": 17.5,
|
|
"gamma_sat": 18.5,
|
|
"c": 5.0,
|
|
"c_hi": 10.0,
|
|
"phi": 27.0,
|
|
"phi_hi": 30.0,
|
|
"c_root": 0.0,
|
|
"soil_depth_m": 1.5,
|
|
"scenario": "sinmap",
|
|
"tr_lo": 2000.0,
|
|
"tr_hi": 3000.0,
|
|
"level_lo_pct": 5.0,
|
|
"level_hi_pct": 20.0,
|
|
},
|
|
"computed_at": "2026-10-25T21:00:00",
|
|
"stations": [
|
|
{
|
|
"chainage_m": 0.0,
|
|
"cells": 9,
|
|
"fs_min": 1.6,
|
|
"si_min": 1.6,
|
|
"worst_class": 1,
|
|
"risk_area_m2": 0.0,
|
|
"risk_ratio": 0.0,
|
|
"level": "low",
|
|
"slope_max_deg": 12.0,
|
|
"label": "No.0",
|
|
},
|
|
{
|
|
"chainage_m": 20.0,
|
|
"cells": 9,
|
|
"fs_min": 0.8,
|
|
"si_min": 0.0,
|
|
"worst_class": 6,
|
|
"risk_area_m2": 36.0,
|
|
"risk_ratio": 1.0,
|
|
"level": "high",
|
|
"slope_max_deg": 41.5,
|
|
"label": "No.1",
|
|
},
|
|
{
|
|
"chainage_m": 40.0,
|
|
"cells": 9,
|
|
"fs_min": 0.95,
|
|
"si_min": 0.7,
|
|
"worst_class": 4,
|
|
"risk_area_m2": 8.0,
|
|
"risk_ratio": 0.0556,
|
|
"level": "mid",
|
|
"slope_max_deg": 33.0,
|
|
"label": "No.2",
|
|
},
|
|
],
|
|
"summary": {
|
|
"areas_m2": dict(zip(keys, (100.0, 20.0, 12.0, 8.0, 0.0, 36.0))),
|
|
"station_levels": {"low": 1, "mid": 1, "high": 1},
|
|
},
|
|
}
|
|
path = project / "E07_StabArea" / "_derived" / "area.json"
|
|
path.parent.mkdir(parents=True)
|
|
path.write_text(json.dumps(area, ensure_ascii=False), encoding="utf-8")
|
|
assert build.analysis_fingerprint(project) != meta["fingerprint"]
|
|
store.refresh(project)
|
|
book, _ = store.load(project)
|
|
s7 = _sheets(book)["광역요약"]["칸"]
|
|
assert [s7[f"C{r}"]["값"] for r in range(12, 18)] == [100.0, 20.0, 12.0, 8.0, 0.0, 36.0]
|
|
assert [s7[f"A{r}"]["값"] for r in (22, 23)] == ["No.1", "No.2"] and "A24" not in s7
|
|
assert (
|
|
s7["E22"]["값"] == "높음 (100.0 %)"
|
|
and s7["E23"]["값"] == "보통 (5.6 %)"
|
|
and s7["F23"]["값"] == 8.0
|
|
and [s7[f"G{r}"]["값"] for r in (12, 13, 14)] == [1, 1, 1]
|
|
and s7["F13"]["값"] == "5 ~ 20 %"
|
|
and s7["B7"]["값"].startswith("표준 — SINMAP")
|
|
)
|
|
vals = book["계산값"]["s7"]
|
|
assert vals["C18"] == {"수": "176"} and vals["D17"]["수"].startswith("20.45")
|
|
|
|
|
|
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)
|
|
rows = [_summary(i) for i in range(N)]
|
|
rows[3] = _summary(3, 0.5)
|
|
_write_summary(project, "fp-2", rows)
|
|
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_옛_작업본_갱신_목차자리_서식(project):
|
|
"""534-5 전 작업본(목차 · 부록 없음 · 서식 18 개) → 갱신 = 목차는 표지 다음 · 부록은 끝 ·
|
|
새 서식 붙임(옛 서식 번호 그대로) · 사용자 몫 불변."""
|
|
_create(project)
|
|
book, meta = store.load(project)
|
|
book["시트"] = [s for s in book["시트"] if s["id"] not in ("s9", "s10")]
|
|
book["서식"] = book["서식"][:18] + [{"크기": 30}] # 사용자가 더한 서식 = 18 번
|
|
_sheets(book)["표지"]["칸"]["C10"] = {"값": "산림청", "서식": 18}
|
|
meta["signs"] = {k: v for k, v in meta["signs"].items() if k not in ("s9", "s10")}
|
|
store.save(project, book)
|
|
(project / "E05_StabReport" / build.META).write_text(json.dumps(meta), encoding="utf-8")
|
|
store.refresh(project)
|
|
book, _ = store.load(project)
|
|
ids = [s["id"] for s in book["시트"]]
|
|
assert ids[:3] == ["s1", "s9", "s2"] and ids[-1] == "s10"
|
|
by = _sheets(book)
|
|
assert by["표지"]["칸"]["C10"] == {"값": "산림청", "서식": 18} and book["서식"][18] == {
|
|
"크기": 30
|
|
}
|
|
head = book["서식"][by["부록_절편"]["칸"]["A3"]["서식"]]
|
|
assert head["크기"] == 8 and head["굵게"] and len(book["서식"]) == len(build.form()["서식"]) + 1
|
|
|
|
|
|
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 해석 전"
|
|
shutil.rmtree(derived_dir(project, KIND_SUMMARY))
|
|
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요약", "광역요약", "부록_절편",
|
|
] # fmt: skip
|
|
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"
|
|
)
|
|
try:
|
|
pdf = printer.report_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 len(stab) == len(book["시트"][4]["인쇄"]["쪽나눔"]) + 1 # 측점표 = 나눈 덩이 수
|
|
# 표지 1 + 목차 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 3 + 3D 1 + 광역 1 + 부록 3
|
|
assert len(texts) == 4 + len(stab) + len(REPS) + 1 + 1 + len(REPS)
|
|
body = len(texts) - 1
|
|
assert f"/ {body}" not in texts[0] and "검증 임도" not in texts[0].split("보고서")[0]
|
|
assert all(f"- {i} / {body} -" in texts[i] and "검증 임도" in texts[i] for i in range(1, body))
|
|
# 목차 쪽 번호 = 실제 쪽(본문 n 쪽 = PDF n + 1 번째 쪽)
|
|
toc = texts[1]
|
|
marks = {"측점별 안정성 판정 일람표": "산정 안전율", printer.TITLES["s5"]: "대표 단면 ①"}
|
|
marks[printer.TITLES["s10"]] = "절편 계산표"
|
|
for title, inside in marks.items():
|
|
line = next(x for x in toc.splitlines() if title in x)
|
|
page = int(line.split()[-1])
|
|
assert inside in texts[page], (title, page)
|
|
|
|
|
|
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
|
|
|
|
|
|
def test_길_저장_409_갱신_초기화_내보내기(project, monkeypatch):
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from common_util.common_util_auth import verify_session
|
|
from E05_StabReport import E05_StabReport_Router as router_mod
|
|
|
|
async def fake_project(_pid):
|
|
return project, 3
|
|
|
|
monkeypatch.setattr(router_mod, "_project", fake_project)
|
|
app = FastAPI()
|
|
app.include_router(router_mod.router)
|
|
app.dependency_overrides[verify_session] = lambda: {"role": "USER", "company_id": 3}
|
|
client = TestClient(app)
|
|
url = "/api/projects/3d3776a1-2973-4385-8dcc-fdc6e49a900e/stab/report"
|
|
try:
|
|
got = client.get(url).json()
|
|
except PrintError as error:
|
|
pytest.skip(f"브라우저 없음 — {error}")
|
|
assert got["editable"] and got["stale"] is False and len(got["book"]["시트"]) == 9
|
|
book = got["book"]
|
|
_sheets(book)["표지"]["칸"]["C10"] = {"값": "산림청", "서식": 5}
|
|
assert client.put(url, json={"rev": "옛판", "book": book}).status_code == 409
|
|
saved = client.put(url, json={"rev": got["rev"], "book": book}).json()
|
|
assert saved["rev"] != got["rev"]
|
|
assert client.post(f"{url}/refresh", json={"rev": got["rev"]}).status_code == 409
|
|
out = client.post(f"{url}/refresh", json={"rev": saved["rev"]}).json()
|
|
assert out["overwritten"] == [] and _sheets(out["book"])["표지"]["칸"]["C10"]["값"] == "산림청"
|
|
fig = client.get(f"{url}/fig", params={"key": build.fig_key("grid:3")})
|
|
assert fig.status_code == 200 and fig.content[:4] == b"\x89PNG"
|
|
assert client.get(f"{url}/fig", params={"key": "../x"}).status_code == 400
|
|
x = client.post(f"{url}/xlsx", json={"book": out["book"]})
|
|
assert x.status_code == 200 and load_workbook(io.BytesIO(x.content)).sheetnames[0] == "표지"
|
|
back = client.post(f"{url}/reset").json()
|
|
assert "값" not in _sheets(back["book"])["표지"]["칸"].get("C10", {})
|
|
|
|
|
|
def test_서명_화면_왕복_같음(project):
|
|
"""JS 를 거친 책(10.0 → 10)도 같은 서명 — 안 고친 시트를 덮임으로 알리지 않음."""
|
|
book = build.form()
|
|
build.build_program(book, build.gather(project))
|
|
s3 = _sheets(book)["입력조건"]
|
|
assert s3["칸"]["B22"]["값"] == 10.0
|
|
trip = json.loads(json.dumps(s3).replace("10.0", "10"))
|
|
assert trip["칸"]["B22"]["값"] == 10 and build.sheet_sign(trip) == build.sheet_sign(s3)
|