fix(534-3): E05 길 저장 · 갱신 · 초기화가 옛 모듈을 부름 · 시트 서명 수 꼴 맞춤 · 화면 높이
- 길: asyncio.to_thread(build.save · refresh · reset) → store(나눈 storage 모듈) — PUT 500 고침 - 시트 서명: 수를 float 로 맞춤 — 화면(JS)을 거친 10.0 → 10 을 사용자 고침으로 잘못 알리던 덮임 알림 고침 - 화면: 본문 칸이 min-height 뿐이라 내장 엑셀 격자 높이 0 — 본문 높이 calc(100dvh - 머리) - 그림: 우기 · 지진 임계면 점선(건기와 겹쳐도 보임) · 해석 방법 글 짧게(Bishop) - 시험: 길(TestClient — 판 409 · 저장 · 갱신 · 그림 · xlsx · 초기화) · 서명 화면 왕복 같음 — 7 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ LOWEST_N = 5 # representative.json 이 없을 때 — 최소 안전율 하위 N
|
||||
CASES = ("dry", "wet", "seismic")
|
||||
SIDE_NAMES = {"cut": "깎기", "fill": "쌓기"}
|
||||
METHOD_NAMES = {
|
||||
"bishop": "Bishop 간편법",
|
||||
"bishop": "Bishop",
|
||||
"janbu": "Janbu 보정",
|
||||
"janbu_corrected": "Janbu 보정",
|
||||
"ordinary": "Fellenius",
|
||||
@@ -589,5 +589,16 @@ def sheet_sign(sheet: dict[str, Any]) -> str:
|
||||
"행": {k: v for k, v in (sheet.get("행") or {}).items() if v},
|
||||
"열": {k: v for k, v in (sheet.get("열") or {}).items() if v},
|
||||
}
|
||||
text = json.dumps(body, ensure_ascii=False, sort_keys=True)
|
||||
text = json.dumps(_numbers(body), ensure_ascii=False, sort_keys=True)
|
||||
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _numbers(value: Any) -> Any:
|
||||
"""수는 float 로 — 화면(JS)을 거치면 10.0 이 10 으로 옴(같은 값을 다르다고 보지 않게)."""
|
||||
if isinstance(value, dict):
|
||||
return {k: _numbers(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_numbers(v) for v in value]
|
||||
if isinstance(value, int | float) and not isinstance(value, bool):
|
||||
return round(float(value), 9)
|
||||
return value
|
||||
|
||||
@@ -21,6 +21,7 @@ BAND_W, BAND_H = 684, 320
|
||||
OK, NG, MUTED, INK = "#2E7D32", "#C00000", "#9E9E9E", "#222222"
|
||||
CONDITION_COLORS = {"dry": "#D32F2F", "wet": "#1565C0", "seismic": "#7B1FA2"}
|
||||
CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"}
|
||||
SLIP_DASH = {"wet": "7 4", "seismic": "2 3"} # 건기 · 우기 임계면이 겹쳐도 둘 다 보이게
|
||||
FONT = "font-family='Malgun Gothic, sans-serif'"
|
||||
|
||||
|
||||
@@ -155,7 +156,9 @@ def section_svg(
|
||||
# 임계 활동면
|
||||
for slip in slips:
|
||||
color = CONDITION_COLORS.get(slip["condition"], NG)
|
||||
out.append(_poly(screen(slip["points"]), stroke=color, stroke_width="2.2"))
|
||||
dash = SLIP_DASH.get(slip["condition"])
|
||||
extra = {"stroke_dasharray": dash} if dash else {}
|
||||
out.append(_poly(screen(slip["points"]), stroke=color, stroke_width="2.2", **extra))
|
||||
# 치수 — 노폭 · 비탈 높이
|
||||
if road:
|
||||
r_off, l_off = road
|
||||
@@ -206,7 +209,7 @@ def section_svg(
|
||||
(
|
||||
CONDITION_COLORS.get(slip["condition"], NG),
|
||||
f"{name} 임계면 Fs {f:.2f}" if f else name,
|
||||
"",
|
||||
SLIP_DASH.get(slip["condition"], ""),
|
||||
)
|
||||
)
|
||||
if wet:
|
||||
|
||||
@@ -137,7 +137,7 @@ async def put_report(project_id: UUID, payload: SaveRequest) -> dict[str, Any]:
|
||||
status_code=409, detail="다른 곳에서 보고서가 바뀌었습니다 — 다시 여세요."
|
||||
)
|
||||
try:
|
||||
await asyncio.to_thread(build.save, root, payload.book)
|
||||
await asyncio.to_thread(store.save, root, payload.book)
|
||||
except RecalcError as error:
|
||||
raise _recalc_errors(error) from error
|
||||
return {"status": "success", **await asyncio.to_thread(_state, root)}
|
||||
@@ -153,7 +153,7 @@ async def refresh_report(project_id: UUID, payload: RevRequest) -> dict[str, Any
|
||||
status_code=409, detail="다른 곳에서 보고서가 바뀌었습니다 — 다시 여세요."
|
||||
)
|
||||
try:
|
||||
result = await asyncio.to_thread(build.refresh, root)
|
||||
result = await asyncio.to_thread(store.refresh, root)
|
||||
except build.MissingInput as error:
|
||||
raise HTTPException(status_code=409, detail=str(error)) from error
|
||||
except (RecalcError, PrintError) as error:
|
||||
@@ -169,7 +169,7 @@ async def refresh_report(project_id: UUID, payload: RevRequest) -> dict[str, Any
|
||||
async def reset_report(project_id: UUID) -> dict[str, Any]:
|
||||
root, _ = await _project(project_id)
|
||||
async with _lock(project_id):
|
||||
if not await asyncio.to_thread(build.reset, root):
|
||||
if not await asyncio.to_thread(store.reset, root):
|
||||
raise HTTPException(status_code=404, detail="초기값이 없습니다.")
|
||||
return {"status": "success", **await asyncio.to_thread(_state, root)}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.e05-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
height: calc(100dvh - var(--app-header-h)); /* 본문 칸은 min-height 뿐 — 100% 가 안 풀림 */
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""534-3(빌더 · storage) — E05 안정성평가 보고서 `E05_StabReport_Build` · `_Store`.
|
||||
"""534-3(빌더 · storage · 길) — E05 안정성평가 보고서 `E05_StabReport_Build` · `_Store` · `_Router`.
|
||||
|
||||
가짜 E 프로젝트(2D 요약 · 측점별 저장본 · 대표 단면 · 지반 조건 · 3D 저장본 하나 · 복사본 읽기는 바꿔 끼움)로
|
||||
측점 수 = 줄 수 · N.G 수 · 그림 파일 수 · 갱신 뒤 사용자 몫 불변 · 덮임 목록 · 초기화 · xlsx 되읽기 · PDF 쪽 수.
|
||||
가짜 E 프로젝트(2D 요약 · 측점별 저장본 · 대표 단면 · 지반 조건 · 3D 저장본 하나 ·
|
||||
복사본 읽기는 바꿔 끼움)로 측점 수 = 줄 수 · N.G 수 · 그림 파일 수 · 갱신 뒤 사용자 몫 불변 ·
|
||||
덮임 목록 · 초기화 · 판 409 · xlsx 되읽기 · PDF 쪽 수.
|
||||
"""
|
||||
|
||||
import io
|
||||
@@ -294,3 +295,51 @@ def test_단면_그림_SVG():
|
||||
)
|
||||
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"]["시트"]) == 6
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user