Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1

This commit is contained in:
2026-10-25 20:58:15 +09:00
5 changed files with 74 additions and 11 deletions
+13 -2
View File
@@ -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
+5 -2
View File
@@ -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:
+3 -3
View File
@@ -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)}
+1 -1
View File
@@ -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;
}
+52 -3
View File
@@ -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)