From 340e90d6cc17ded3b9f3dd034c616dd2d3a7e593 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 20:58:02 +0900 Subject: [PATCH] =?UTF-8?q?fix(534-3):=20E05=20=EA=B8=B8=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=C2=B7=20=EA=B0=B1=EC=8B=A0=20=C2=B7=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=EA=B0=80=20=EC=98=9B=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=9D=84=20=EB=B6=80=EB=A6=84=20=C2=B7=20=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85=20=EC=88=98=20=EA=BC=B4=20=EB=A7=9E=EC=B6=A4?= =?UTF-8?q?=20=C2=B7=20=ED=99=94=EB=A9=B4=20=EB=86=92=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 길: 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 --- E05_StabReport/E05_StabReport_Build.py | 15 +++++- E05_StabReport/E05_StabReport_Figure.py | 7 ++- E05_StabReport/E05_StabReport_Router.py | 6 +-- E05_StabReport/E05_StabReport_UI_Style.css | 2 +- resources/tester/test_534_3_e05_report.py | 55 ++++++++++++++++++++-- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/E05_StabReport/E05_StabReport_Build.py b/E05_StabReport/E05_StabReport_Build.py index 6606c2a6c..d924938cf 100644 --- a/E05_StabReport/E05_StabReport_Build.py +++ b/E05_StabReport/E05_StabReport_Build.py @@ -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 diff --git a/E05_StabReport/E05_StabReport_Figure.py b/E05_StabReport/E05_StabReport_Figure.py index 9f39a4a91..69e4f636c 100644 --- a/E05_StabReport/E05_StabReport_Figure.py +++ b/E05_StabReport/E05_StabReport_Figure.py @@ -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: diff --git a/E05_StabReport/E05_StabReport_Router.py b/E05_StabReport/E05_StabReport_Router.py index 9132c1d7a..b27f334db 100644 --- a/E05_StabReport/E05_StabReport_Router.py +++ b/E05_StabReport/E05_StabReport_Router.py @@ -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)} diff --git a/E05_StabReport/E05_StabReport_UI_Style.css b/E05_StabReport/E05_StabReport_UI_Style.css index f958c575c..fda06c9da 100644 --- a/E05_StabReport/E05_StabReport_UI_Style.css +++ b/E05_StabReport/E05_StabReport_UI_Style.css @@ -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; } diff --git a/resources/tester/test_534_3_e05_report.py b/resources/tester/test_534_3_e05_report.py index d46f0b3e5..e4dfc66f6 100644 --- a/resources/tester/test_534_3_e05_report.py +++ b/resources/tester/test_534_3_e05_report.py @@ -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("= 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)