feat(536-3): E 보강 검토 앞부분 — 벽 외적 안정(활동 · 전도 · 지지력 · KDS 11 80 05) · 비탈 완화 · 소단 가정 재해석 · 최소 완화 찾기 · E03 「보강 검토」 탭 · E05 보강검토 시트(있을 때만)

- E03_StabSection_WallCheck: Coulomb · Rankine · M-O 토압 · 상재하중 · 수압 · 양압력 · Meyerhof 지지력 · 평상시 / 지진 시 판정
- E03_StabSection_WhatIf: 한 쪽 비탈 1:n(+ 소단) 다시 그림 · 원지반 만나는 가장 완만한 n · 성긴 걸음 + 이분 최소 완화(완만할수록 늘 안전하지 않음)
- E03_StabSection_Router_WhatIf: POST whatif · wallcheck · GET whatif · 저장본 _derived/stab2d_whatif(+ _view) · 원본 측점 · 요약 그대로
- E05: s8 보강검토(양식 · 자리표) · 저장본 없으면 시트 뺌 · 갱신 때 사라진 프로그램 몫 뺌
- 시험 test_536_3 12 · 534-3 양식 시험 시트 8(보강검토 빈 판 1쪽)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 23:59:39 +09:00
co-authored by Claude Opus 5.5
parent caf073a126
commit ab3eee034d
14 changed files with 2038 additions and 9 deletions
@@ -3078,6 +3078,351 @@
21
]
}
},
{
"id": "s8",
"이름": "보강검토",
"칸": {
"A1": {
"값": "보강 검토 (가정)",
"서식": 1
},
"B1": {
"서식": 1
},
"C1": {
"서식": 1
},
"D1": {
"서식": 1
},
"E1": {
"서식": 1
},
"F1": {
"서식": 1
},
"G1": {
"서식": 1
},
"H1": {
"서식": 1
},
"I1": {
"서식": 1
},
"A2": {
"값": "E 안 가정 검토 — 비탈 완화 · 소단은 형상만 바꿔 다시 해석 · 설계(임도)에 안 돌려 씀 · 문헌 추정치",
"서식": 2
},
"B2": {
"서식": 2
},
"C2": {
"서식": 2
},
"D2": {
"서식": 2
},
"E2": {
"서식": 2
},
"F2": {
"서식": 2
},
"G2": {
"서식": 2
},
"H2": {
"서식": 2
},
"I2": {
"서식": 2
},
"A4": {
"값": "1. 비탈 완화 · 소단 가정 — 전후 안전율 (Bishop)",
"서식": 3
},
"B4": {
"서식": 3
},
"C4": {
"서식": 3
},
"D4": {
"서식": 3
},
"E4": {
"서식": 3
},
"F4": {
"서식": 3
},
"G4": {
"서식": 3
},
"H4": {
"서식": 3
},
"I4": {
"서식": 3
},
"A5": {
"값": "측점",
"서식": 6
},
"B5": {
"값": "비탈",
"서식": 6
},
"C5": {
"값": "가정",
"서식": 6
},
"D5": {
"값": "조건",
"서식": 6
},
"E5": {
"값": "전 Fs",
"서식": 6
},
"F5": {
"값": "후 Fs",
"서식": 6
},
"G5": {
"값": "기준",
"서식": 6
},
"H5": {
"값": "전 판정",
"서식": 6
},
"I5": {
"값": "후 판정",
"서식": 6
},
"A6": {
"서식": 7
},
"B6": {
"서식": 7
},
"C6": {
"서식": 5
},
"D6": {
"서식": 7
},
"E6": {
"서식": 8
},
"F6": {
"서식": 8
},
"G6": {
"서식": 8
},
"H6": {
"서식": 7
},
"I6": {
"서식": 7
},
"A8": {
"값": "2. 벽 외적 안정 — 활동 · 전도 · 지지력 (KDS 11 80 05)",
"서식": 3
},
"B8": {
"서식": 3
},
"C8": {
"서식": 3
},
"D8": {
"서식": 3
},
"E8": {
"서식": 3
},
"F8": {
"서식": 3
},
"G8": {
"서식": 3
},
"H8": {
"서식": 3
},
"I8": {
"서식": 3
},
"A9": {
"값": "측점",
"서식": 6
},
"B9": {
"값": "벽 · 경우",
"서식": 6
},
"C9": {
"값": "활동 Fs",
"서식": 6
},
"D9": {
"값": "기준",
"서식": 6
},
"E9": {
"값": "전도 Fs",
"서식": 6
},
"F9": {
"값": "기준",
"서식": 6
},
"G9": {
"값": "지지력 Fs",
"서식": 6
},
"H9": {
"값": "기준",
"서식": 6
},
"I9": {
"값": "판정",
"서식": 6
},
"A10": {
"서식": 7
},
"B10": {
"서식": 5
},
"C10": {
"서식": 8
},
"D10": {
"서식": 8
},
"E10": {
"서식": 8
},
"F10": {
"서식": 8
},
"G10": {
"서식": 8
},
"H10": {
"서식": 8
},
"I10": {
"서식": 7
},
"A12": {
"값": "가정 · 벽 검토 없음 — E03 [보강 검토] 뒤 [보고서 갱신]",
"서식": 5
},
"B12": {
"서식": 5
},
"C12": {
"서식": 5
},
"D12": {
"서식": 5
},
"E12": {
"서식": 5
},
"F12": {
"서식": 5
},
"G12": {
"서식": 5
},
"H12": {
"서식": 5
},
"I12": {
"서식": 5
}
},
"병합": [
"A1:I1",
"A2:I2",
"A4:I4",
"A8:I8",
"A12:I12"
],
"열": {
"A": {
"폭": 11
},
"B": {
"폭": 14
},
"C": {
"폭": 20
},
"D": {
"폭": 8
},
"E": {
"폭": 9
},
"F": {
"폭": 9
},
"G": {
"폭": 9
},
"H": {
"폭": 9
},
"I": {
"폭": 9
}
},
"행": {
"1": {
"높이": 36
},
"2": {
"높이": 30
},
"4": {
"높이": 22
},
"5": {
"높이": 22
},
"8": {
"높이": 22
},
"9": {
"높이": 22
}
},
"보기": {
"눈금선": false
},
"인쇄": {
"용지": "A4",
"방향": "portrait",
"맞춤": {
"폭": 1,
"높이": 0
},
"여백": {
"위": 15,
"아래": 15,
"왼": 10,
"오른": 10
}
}
}
],
"활성": "s1"
@@ -90,3 +90,13 @@
| 22 행부터 보통 · 높음 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 측점 등급 + SI<1 비율 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 |
- 22 행 서식을 줄마다 복사. 측점 등급 = 영향 구역(측점 앞뒤 ½ 간격 × 띠 폭) 안 SI < 1(등급 4 이상) 셀 비율 — 낮음 < 5 % · 보통 5~20 % · 높음 ≥ 20 %(문턱 = E07 설정).
## s8 보강검토 (프로그램 몫 · E03 보강 검토 저장본 · 없으면 시트를 뺌)
| 칸 | 내용 |
| --- | --- |
| 6 행부터 가정 하나 × 조건 하나 = 한 줄 | A 측점 · B 비탈(좌 · 우 · 깎기 · 쌓기)(서식 7) · C 가정 글(1:n 전 → 후 · 소단 폭 / 높이 · 최소 완화)(5) · D 조건(7) · E 전 Fs · F 후 Fs · G 기준(8) · H 전 판정 · I 후 판정(7 → N.G 10 · O.K 11) · 없으면 「가정 없음」 |
| 가정 줄 다음 한 줄 띄고 `8` · `9` 행 꼴(제목 3 · 머리 6) | 2. 벽 외적 안정 |
| 그 아래 벽 하나 × 경우 하나 = 한 줄 | A 측점(7) · B 벽 이름 · 평상시 / 지진 시 · 토압법 · 배면 수위(5) · C 활동 · E 전도 · G 지지력 Fs(8) · D · F · H 기준(8 · KDS 11 80 05) · I 판정(7 → 10 · 11) · 없으면 「벽 검토 없음」 |
- 6 행 · 10 행 서식을 줄마다 복사 · 저장본 = (측점 · 종류 · 비탈)마다 가장 최근 하나.
@@ -36,7 +36,7 @@ STYLE_KEYS = {
} # fmt: skip
PRINT_KEYS = {"용지", "방향", "배율", "맞춤", "여백", "머리행", "쪽나눔"}
A1 = re.compile(r"^([A-Z]{1,3})(\d+)$")
NAMES = ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약"]
NAMES = ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약", "보강검토"]
needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
@@ -148,7 +148,7 @@ def test_양식_계약():
book = _form()
_contract(book)
assert [s["이름"] for s in book["시트"]] == NAMES
assert [s["id"] for s in book["시트"]] == ["s1", "s2", "s3", "s4", "s5", "s6", "s7"]
assert [s["id"] for s in book["시트"]] == ["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8"]
def test_양식_인쇄_쪽나눔_머리행_그림():
@@ -167,6 +167,7 @@ def test_양식_인쇄_쪽나눔_머리행_그림():
"대표단면": 1,
"3D요약": 1,
"광역요약": 0,
"보강검토": 0,
}
# N.G 빨강 조건부 서식 — 측점표 판정 열 · 서식 표에 N.G 칸 서식(빌더가 인쇄 · xlsx 용으로 직접 씀)
assert by["측점표"]["조건부서식"][0]["범위"][0]["c0"] == 9
@@ -230,8 +231,8 @@ def test_견본_pdf_쪽수_머리행(png):
)
sections = [i for i, t in enumerate(texts) if "대표 단면" in t and "안전율 Fs" in t]
assert len(sections) == 2 # 단면마다 한 쪽
# 표지 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 2 + 3D 1 + 광역 1
assert len(pages) == 1 + 1 + 1 + len(stab) + 2 + 1 + 1
# 표지 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 2 + 3D 1 + 광역 1 + 보강 1(양식 빈 판)
assert len(pages) == 1 + 1 + 1 + len(stab) + 2 + 1 + 1 + 1
assert all(float(pages[i].mediabox.width) > float(pages[i].mediabox.height) for i in stab)
html = spreadsheet.print_htmls([{"book": book, "figures": {k: png for k in figs}}])[0]
assert html.count("<img ") == 3 # 단면 2 + 3D 띠 1
@@ -0,0 +1,328 @@
# -*- coding: utf-8 -*-
"""536-3 E03 보강 검토 — 벽 외적 안정 · 비탈 완화 · 소단 가정 재해석 · E05 보강검토 시트.
- 벽 외적 안정(순수): 교과서 계수표(Coulomb · Rankine · Meyerhof) · M-O 닫힌 꼴 · 손계산 예제
(직사각 벽 · 상재하중 · 수압 · 뒷굽 위 흙) ±2 %
- 가정 형상(순수): 1:n 비탈이 원지반과 만나는 점 = 해석해 · 소단 꼭짓점 · 최소 완화 찾기(단조 · 창)
- 길(검증_sub2 동결 복사 · 벽 측점 + 하나): 지금 기울기 그대로 = 원본 안전율(±3 %) · 최소 완화가
목표 안전율을 맞춤 · 원본 측점 · 요약 저장본 그대로 · 벽 측점 외적 안정 · 벽 쪽 비탈 가정 거절 ·
E05 보강검토 시트(있을 때만)
./venv/Scripts/python.exe -m pytest resources/tester/test_536_3_e03_reinforce.py -q
"""
from __future__ import annotations
import asyncio
import math
import shutil
import sys
import tempfile
from pathlib import Path
from uuid import UUID
import pytest
from fastapi import HTTPException
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from common_util import common_util_project_copy as cp # noqa: E402
from common_util.common_util_station_key import chainage_key # noqa: E402
from E01_StabRegister import E01_StabRegister_Source as src # noqa: E402
from E02_StabGround import E02_StabGround_Router as e02 # noqa: E402
from E03_StabSection import E03_StabSection_Router as router # noqa: E402
from E03_StabSection import E03_StabSection_Router_WhatIf as wf # noqa: E402
from E03_StabSection import E03_StabSection_Worker as worker # noqa: E402
from E03_StabSection import E03_StabSection_WallCheck as wc # noqa: E402
from E03_StabSection import E03_StabSection_WhatIf as whatif # noqa: E402
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402
SOURCE_ID = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(읽기만)
PID = "e" * 8 + "-" + "e" * 4 + "-" + "e" * 4 + "-" + "e" * 4 + "-" + "e" * 12
WALL_KEY = "351.00" # 검증_sub2 기슭막이 측점
SOIL = {"gamma_t": 18.0, "gamma_sat": 19.0, "c": 0.0, "phi": 30.0}
def close(a: float, b: float, tol: float = 0.02) -> bool:
return abs(a - b) <= tol * abs(b)
# ── 벽 외적 안정 ─────────────────────────────────────────────────────────────
def test_토압_지지력_계수_교과서표():
"""Das(기초공학) 표 — Coulomb δ = 2/3φ · Rankine 경사 배면 · Meyerhof 지지력 계수."""
assert close(wc.ka_coulomb(30, 20, 0, 0), 0.2973)
assert close(wc.ka_coulomb(30, 0, 0, 0), 1 / 3)
assert close(wc.ka_rankine(30, 0), 1 / 3)
assert close(wc.ka_rankine(30, 10), 0.3495)
nc, nq, ng = wc.bearing_factors(30)
assert close(nc, 30.14) and close(nq, 18.40) and close(ng, 15.67)
assert wc.bearing_factors(0) == (5.14, 1.0, 0.0)
def test_MO_닫힌꼴():
"""δ = θ = β = 0 · kv = 0 — KAE = cos²(φ−ψ) / (cos²ψ [1 + √(sinφ sin(φ−ψ) / cosψ)]²) · ψ = 0 이면 Coulomb."""
for phi, kh in ((30, 0.2), (35, 0.15)):
psi = math.atan(kh)
f = math.radians(phi)
want = math.cos(f - psi) ** 2 / (
math.cos(psi) ** 2
* (1 + math.sqrt(math.sin(f) * math.sin(f - psi) / math.cos(psi))) ** 2
)
assert close(wc.ka_coulomb(phi, 0, 0, 0, math.degrees(psi)), want, 1e-9)
assert close(wc.ka_coulomb(30, 0, 0, 0, math.degrees(math.atan(0.2))), 0.4733)
assert wc.ka_coulomb(32, 21, 5, 10, 0.0) == pytest.approx(wc.ka_coulomb(32, 21, 5, 10))
RECT = [[0, 0], [2, 0], [2, 4], [0, 4]] # B 2 · H 4 · 배면 = +x(뒷굽 x = 2)
def test_직사각_벽_손계산():
"""Rankine · q 0 · 물 없음 · μ 0.5 — W 184 · Pa 48(H/3) · 활동 1.917 · 전도 2.875 ·
e = 1 − 0.6522 · q_max · Meyerhof q_ult(유효폭 · 경사 계수)."""
r = wc.check_wall(
RECT, +1, backfill=SOIL, foundation=SOIL, method="rankine",
surcharge_kpa=0.0, base_friction=0.5,
) # fmt: skip
W, Pa = 2 * 4 * 23.0, 0.5 * 18 * 16 / 3
assert close(r["loads"]["W"], W) and close(r["loads"]["Pa"], Pa) and r["loads"]["W_soil"] == 0
assert close(r["fs"]["sliding"], W * 0.5 / Pa)
m_res, m_over = W * 1.0, Pa * 4 / 3
assert close(r["fs"]["overturning"], m_res / m_over)
xbar = (m_res - m_over) / W
e = 1.0 - xbar
q_max = W / 2 * (1 + 6 * e / 2) if e <= 2 / 6 else 2 * W / (3 * xbar)
alpha = math.degrees(math.atan(Pa / W))
q_ult = 0.5 * 18 * (2 - 2 * e) * 15.668 * (1 - alpha / 30) ** 2
assert close(r["base"]["q_max"], q_max) and close(r["fs"]["bearing"], q_ult / q_max)
assert r["case"] == "normal" and r["required"] == wc.REQUIRED["normal"]
assert r["ok"] == {k: r["fs"][k] >= r["required"][k] for k in r["fs"]}
def test_상재하중_수압_양압력():
"""q 10 · 배면 수위 = 벽 높이 — Pa = Ka(½γ'H² + qH) · Pw = ½γwH² · U = ½γwHB · ΣH · V."""
r = wc.check_wall(
RECT, +1, backfill=SOIL, foundation=SOIL, method="rankine",
surcharge_kpa=10.0, water_ratio=1.0, base_friction=0.5,
) # fmt: skip
g = SOIL["gamma_sat"] - wc.GAMMA_W
Pa = (0.5 * g * 16 + 10 * 4) / 3
Pw, U = 0.5 * wc.GAMMA_W * 16, 0.5 * wc.GAMMA_W * 4 * 2
assert close(r["loads"]["Pa"], Pa) and close(r["loads"]["Pw"], Pw) and close(r["loads"]["U"], U)
assert close(r["loads"]["H"], Pa + Pw) and close(r["loads"]["V"], 184 - U)
arm = (0.5 * g * 16 * 4 / 3 + 40 * 2) / (0.5 * g * 16 + 40) # 삼각 + 사각 합력 높이
assert close(r["loads"]["Pa_arm"], arm)
m_over = Pa * arm + Pw * 4 / 3 + U * 4 / 3
assert close(r["loads"]["M_overturn"], m_over)
def test_L형_벽_뒷굽_위_흙_지진():
"""역 T 형(앞굽 0 · 뒷굽 3 · 벽체 0.6~1.0) — Rankine 가상면 위 흙 = (3 − 1.0) × 4.5 × γ ·
Coulomb 가상면 θ = atan(2 / 5) · 지진 시 = M-O 증분 0.6H + 관성 · 기준 = 지진 시 표."""
L = [[0, 0], [3, 0], [3, 0.5], [1.0, 0.5], [1.0, 5], [0.6, 5], [0.6, 0.5], [0, 0.5]]
r = wc.check_wall(L, +1, backfill=SOIL, foundation=SOIL, method="rankine")
assert close(r["loads"]["W_soil"], 2.0 * 4.5 * 18)
c = wc.check_wall(L, +1, backfill=SOIL, foundation=SOIL)
assert close(c["geometry"]["theta_deg"], math.degrees(math.atan(2 / 5)))
# 가상면(x = 3 − 0.4z)과 벽체(x = 1.0) 사이 · 0.5 ≤ z ≤ 5 → ∫(2 − 0.4z)dz = 4.05 m²
assert close(c["loads"]["W_soil"], 4.05 * 18)
s = wc.check_wall(L, +1, backfill=SOIL, foundation=SOIL, kh=0.15)
assert s["case"] == "seismic" and s["required"] == wc.REQUIRED["seismic"]
assert s["coefficients"]["kae"] > c["coefficients"]["ka"]
assert s["loads"]["H"] > c["loads"]["H"] and s["fs"]["sliding"] < c["fs"]["sliding"]
def test_배면이_반대쪽이면_거울():
a = wc.check_wall(RECT, +1, backfill=SOIL, foundation=SOIL)
b = wc.check_wall([[-x, z] for x, z in RECT], -1, backfill=SOIL, foundation=SOIL)
for k in a["fs"]:
assert a["fs"][k] == pytest.approx(b["fs"][k])
# ── 가정 형상 · 최소 완화 찾기 ────────────────────────────────────────────────
GROUND = [(-40.0, 120.0), (0.0, 100.0), (40.0, 80.0)] # 1:2 비탈(좌 = +x 낮음)
LINE = [(-3.0, 101.5), (-2.0, 100.0), (2.0, 100.0), (3.0, 99.5)]
def test_비탈_다시_그림_해석해():
"""지반 z = 100 − x/2 · 우(−x) 깎기 시작 (−2, 100) · 1:n — u = −x 에서 (u − 2)/n = u/2 →
u = 2 / (1 − n/2) · z = 100 + u/2 · n ≥ 2 = 못 만남. 좌 쌓기 (2, 100) 1:1.5 → (8, 96)."""
for n in (0.5, 1.0, 1.5):
line = whatif.reshape_side(LINE, GROUND, side="right", role="cut", start_x=-2.0, ratio=n)
u = 2.0 / (1.0 - n / 2)
x, z = min(line)
assert x == pytest.approx(-u) and z == pytest.approx(100.0 + u / 2)
assert [p for p in line if p[0] > -2.0 + 1e-9] == [list(p) for p in LINE[2:]]
gentle = dict(side="right", role="cut", start_x=-2.0, ratio=2.5)
assert whatif.reshape_side(LINE, GROUND, **gentle) is None
line = whatif.reshape_side(
LINE, GROUND, side="right", role="cut", start_x=-2.0, ratio=0.5, bench_w=1.0, bench_h=1.0
)
corners = [p for p in line if -3.6 < p[0] < -2.4]
assert [[round(x, 6), round(z, 6)] for x, z in corners] == [[-3.5, 101.0], [-2.5, 101.0]]
fill = whatif.reshape_side(LINE, GROUND, side="left", role="fill", start_x=2.0, ratio=1.5)
assert max(fill) == pytest.approx([8.0, 96.0])
assert [p for p in fill if p[0] < 2.0 - 1e-9] == [list(p) for p in LINE[:2]]
assert (
whatif.reshape_side(LINE, GROUND, side="left", role="fill", start_x=2.0, ratio=3.0) is None
)
def test_최소_완화_찾기():
found = whatif.search_ratio(lambda n: n >= 1.23, 0.4)
assert found["found"] == pytest.approx(1.25)
window = whatif.search_ratio(lambda n: 1.0 <= n <= 1.3, 0.4) # 완만할수록 늘 안전하지 않음
assert 1.0 <= window["found"] <= 1.3
assert whatif.search_ratio(lambda n: False, 0.4, max_ratio=1.1)["found"] is None
assert whatif.search_ratio(lambda n: True, 0.4) == {"found": 0.4, "tried": [(0.4, True)]}
assert len(whatif.search_ratio(lambda n: n >= 3.9, 0.4)["tried"]) <= 10
# ── 길(검증_sub2 동결 복사) ──────────────────────────────────────────────────
async def _copy(target: Path) -> None:
import aiomysql
from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER
connection = await aiomysql.connect(
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, autocommit=True
)
try:
await cp.copy_project(
connection, SOURCE_ID, scope="stability", new_id=PID, project_root=target
)
finally:
connection.close()
@pytest.fixture(scope="module")
def frozen():
folder = Path(tempfile.mkdtemp(prefix="aislo_e03_rf_"))
try:
asyncio.run(_copy(folder / "copy"))
except Exception as error: # noqa: BLE001
shutil.rmtree(folder, ignore_errors=True)
pytest.skip(f"검증 원본 복사 실패: {error}")
yield folder / "copy"
shutil.rmtree(folder, ignore_errors=True)
def _fast(cross, design, **kw):
"""진짜 엔진 — Bishop 원호만(빠르게)."""
return analyze_station(cross, design, **{**kw, "methods": ("bishop",), "noncircular": False})
@pytest.fixture(scope="module")
def project(frozen):
"""벽 측점 + 깎기 비탈 있는 벽 없는 측점 하나 · 진짜 엔진(빠르게) · [해석] 한 번."""
mp = pytest.MonkeyPatch()
original = src.stations
def pick(root):
rows = original(root)
wall = [r for r in rows if chainage_key(r["chainage_m"]) == WALL_KEY]
rest = [r for r in rows if chainage_key(r["chainage_m"]) != WALL_KEY]
return wall + rest[10:11]
mp.setattr(router.src, "stations", pick)
async def fake_project(_pid):
return frozen, {"company_id": 1, "project_kind": "STABILITY"}
mp.setattr(e02, "_project", fake_project)
mp.setattr(worker, "analyze_station", _fast)
mp.setattr(wf, "analyze_station", _fast)
mp.setattr(router, "WORKERS", 1)
router._JOBS.clear()
async def go():
await router.analyze2d(UUID(PID))
while router._TASKS:
await asyncio.gather(*list(router._TASKS))
asyncio.run(go())
yield frozen
mp.undo()
def _stamps(root: Path) -> dict[str, bytes]:
out = {}
for kind in (router.KIND_STATION, router.KIND_SUMMARY):
for p in (root / "_derived" / kind).glob("*.json"):
out[str(p)] = p.read_bytes() # 읽기가 고친 때를 건드림(최근 표시) — 내용만 견줌
return out
def _plain_station(root: Path) -> tuple[str, str, float, dict]:
summary, _ = router._latest_summary(root, "")
row = next(r for r in summary["stations"] if r["key"] != WALL_KEY)
side = next(s for s in row["sides"] if s["role"] == "cut")["side"]
return row["key"], side, row["cases"]["dry"][side]["fs"], row
def test_지금_기울기_그대로면_원본_안전율(project):
key, side, before, _ = _plain_station(project)
rec = wf.run_whatif(project, key, wf.WhatIfRequest(side=side))
assert rec["assume"]["ratio"] == rec["assume"]["ratio_before"]
assert close(rec["after"]["dry"]["fs"], before, 0.03)
assert rec["view"]["analyses"] and rec["before"]["dry"]["fs"] == before
def test_최소_완화가_목표를_맞춤_원본_불변(project):
key, side, before, _ = _plain_station(project)
stamps = _stamps(project)
target = round(before + 0.02, 3)
rec = wf.run_whatif(
project,
key,
wf.WhatIfRequest(side=side, search=True, condition="dry", target_fs=target),
)
search = rec["search"]
if search["found"] is None: # 완화가 깎기 높이를 키워 오히려 낮아지는 지형 — 끝값까지 봄
assert all(not ok for _, ok in search["tried"])
else:
assert rec["after"]["dry"]["fs"] >= target - 1e-9
assert rec["assume"]["ratio"] == search["found"] > rec["assume"]["ratio_before"]
steeper = [n for n, ok in search["tried"] if n < search["found"]]
assert all(not ok for n, ok in search["tried"] if n in steeper)
assert _stamps(project) == stamps # 원본 측점 · 요약 저장본 그대로
again = wf.run_whatif(
project,
key,
wf.WhatIfRequest(side=side, search=True, condition="dry", target_fs=target),
)
assert again["computed_at"] == rec["computed_at"] # 같은 가정 = 저장본
def test_벽_측점_외적_안정_벽쪽_가정_거절(project):
rec = wf.run_wallcheck(project, WALL_KEY, wf.WallRequest(kh=0.15))
assert len(rec["walls"]) == 1
wall = rec["walls"][0]
assert [c["case"] for c in wall["cases"]] == ["normal", "seismic"]
for c in wall["cases"]:
assert c["required"] == wc.REQUIRED[c["case"]]
assert all(v > 0 for v in c["fs"].values())
assert c["geometry"]["H"] == pytest.approx(1.5, abs=0.05)
with pytest.raises(HTTPException) as error:
wf.run_whatif(project, WALL_KEY, wf.WhatIfRequest(side=wall["side"]))
assert error.value.status_code == 422
def test_E05_보강검토_시트(project, monkeypatch):
from E05_StabReport import E05_StabReport_Build as build
recs = wf.records(project)
assert {r["type"] for r in recs} == {"slope", "wall"}
book = build.form()
build.build_program(book, build.gather(project))
s8 = next(s for s in book["시트"] if s["id"] == "s8")
cells = s8["칸"]
slopes = [r for r in recs if r["type"] == "slope"]
rows = sum(len(r["after"]) for r in slopes)
assert all(cells[f"D{6 + i}"]["값"] in ("건기", "우기", "지진") for i in range(rows))
wall_rows = [a1 for a1, c in cells.items() if a1[0] == "B" and "평상시" in str(c.get("값"))]
assert len(wall_rows) == 1 and cells[f"I{wall_rows[0][1:]}"]["값"] in ("O.K", "N.G")
monkeypatch.setattr(build.reinforce, "read", lambda root: [])
empty = build.form()
build.build_program(empty, build.gather(project))
assert "s8" not in {s["id"] for s in empty["시트"]} # 없으면 시트 뺌