Files
Aislo/E05_StabReport/E05_StabReport_Store.py
T
eomsangdonandClaude Opus 5.5 e7afaa6664 feat(534-3): E05 안정성평가 보고서 — 빌더 · storage · 길 · 화면
- 빌더: E02 지반 · E03 2D 요약 · 측점별 저장본 · 대표 단면 · E04 3D 요약 → 프로그램 몫 시트(입력조건 · 측점표 · 대표단면 · 3D요약) · 사용자 몫(표지 · 개요)은 처음 한 번 · N.G 서식 10 · O.K 서식 11 직접 · 해석 지문
- 그림: 서버 SVG(지층 · 암 경계 · 우기 지하수위 · 임계면 · 노폭 · 비탈 높이 · 평면 위험 띠) → PNG(공용 svg_png) · fig/<측점 키>.png
- storage: 작업본 report.json · meta(지문 · 시트 서명) · 초기값 initial_snapshot · 갱신 덮임 목록 · 옛 판 _지난/
- 길: GET(없으면 처음 만듦) · PUT 판 409 + 서버 재계산 · refresh · reset · xlsx · pdf(POST 화면 책 · GET 작업본) · 그림
- 화면: 내장 엑셀(시트 탭 · 그림 겹판) · 캐시 · [저장] · [초기화] · [보고서 갱신] · 해석 바뀜 띠 · [엑셀] · [PDF] · [PDF 미리보기]
- 시험 test_534_3_e05_report 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:34:40 +09:00

160 lines
6.1 KiB
Python

"""E05 보고서 storage — 작업본 · 초기값 · 그림 · 옛 판(요구 347-5 · PLAN 534-3).
storage(E 프로젝트 루트):
E05_StabReport/report.json 작업본(Workbook · 서버 재계산 `계산값` 포함)
E05_StabReport/report_meta.json 해석 지문 · 만든 때 · 프로그램 몫 시트 서명(덮임 알림용)
E05_StabReport/fig/<열쇠>.png 그림(열쇠 = `fig/<측점 키>.png` · `fig/3d_band.png`)
E05_StabReport/_지난/<때>.json 갱신이 덮은 옛 판(덮인 시트에 사용자 고침이 있었을 때)
initial_snapshot/E05_StabReport/ 초기값 = 처음 만든 직후 한 벌(report · meta · fig)
시트 채우기 · 그림 SVG = `E05_StabReport_Build.py` · 데이터 3층 = CLAUDE.md 5장.
"""
from __future__ import annotations
import hashlib
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any
from common_util.common_util_json import atomic_write_json
from common_util.common_util_spreadsheet import with_server_values
from common_util.common_util_svg_png import svgs_to_pngs
from E05_StabReport.E05_StabReport_Build import (
META,
PROGRAM_SHEETS,
REPORT,
_read,
build_program,
fig_path,
fill_cover,
form,
gather,
initial_dir,
sheet_sign,
stage_dir,
)
def revision(project_root: Path) -> str | None:
path = stage_dir(project_root) / REPORT
return hashlib.sha1(path.read_bytes()).hexdigest()[:16] if path.is_file() else None
def load(project_root: Path) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
folder = stage_dir(project_root)
return _read(folder / REPORT), _read(folder / META)
def _write_figures(folder: Path, jobs: list[tuple[str, str]]) -> list[str]:
pngs = svgs_to_pngs([svg for _, svg in jobs])
target = Path(folder) / "fig"
if target.is_dir():
shutil.rmtree(target)
target.mkdir(parents=True)
for (key, _), png in zip(jobs, pngs):
fig_path(folder, key).write_bytes(png)
return [key for key, _ in jobs]
def _write(project_root: Path, book: dict[str, Any], meta: dict[str, Any]) -> None:
folder = stage_dir(project_root)
atomic_write_json(folder / REPORT, book)
atomic_write_json(folder / META, meta)
def _meta(book: dict[str, Any], inputs: dict[str, Any], figures: list[str]) -> dict[str, Any]:
return {
"fingerprint": inputs["fingerprint"],
"built_at": datetime.now().isoformat(timespec="seconds"),
"signs": {s["id"]: sheet_sign(s) for s in book["시트"] if s["id"] in PROGRAM_SHEETS},
"figures": figures,
}
def create(project_root: Path) -> dict[str, Any]:
"""처음 만듦 — 양식 전부 · 사용자 몫 처음 채움 · 초기값 한 벌. 돌려줌 = meta."""
inputs = gather(project_root)
book = form()
fill_cover(next(s for s in book["시트"] if s["id"] == "s1"), inputs)
jobs = build_program(book, inputs)
book = with_server_values(book)
folder = stage_dir(project_root)
figures = _write_figures(folder, jobs)
meta = _meta(book, inputs, figures)
_write(project_root, book, meta)
init = initial_dir(project_root)
if init.is_dir():
shutil.rmtree(init)
init.mkdir(parents=True)
for name in (REPORT, META):
shutil.copy2(folder / name, init / name)
shutil.copytree(folder / "fig", init / "fig")
return meta
def refresh(project_root: Path) -> dict[str, Any]:
"""[보고서 갱신] — 프로그램 몫 시트 · 그림만 다시. 사용자 몫 · 사용자가 더한 시트는 그대로.
덮인 시트에 사용자 고침이 있었으면 `overwritten`(시트 이름) · 옛 판 `_지난/`."""
book, meta = load(project_root)
if book is None:
return {"created": True, "overwritten": [], **create(project_root)}
inputs = gather(project_root)
fresh = form()
jobs = build_program(fresh, inputs)
built = {s["id"]: s for s in fresh["시트"] if s["id"] in PROGRAM_SHEETS}
signs = (meta or {}).get("signs") or {}
overwritten = [
s["이름"]
for s in book["시트"]
if s["id"] in built and signs.get(s["id"]) and sheet_sign(s) != signs[s["id"]]
]
folder = stage_dir(project_root)
if overwritten:
old = folder / "_지난"
old.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
shutil.copy2(folder / REPORT, old / f"{stamp}.json")
have = {s["id"] for s in book["시트"]}
sheets = [built.get(s["id"], s) for s in book["시트"]]
sheets += [built[sid] for sid in PROGRAM_SHEETS if sid not in have] # 지운 시트는 끝에 다시
book = with_server_values({**book, "시트": sheets})
figures = _write_figures(folder, jobs)
new_meta = _meta(book, inputs, figures)
_write(project_root, book, new_meta)
return {"created": False, "overwritten": overwritten, **new_meta}
def reset(project_root: Path) -> bool:
"""[초기화] — 초기값을 작업본에 덮어씀(재계산 아님) · 초기값 없으면 False."""
init = initial_dir(project_root)
if not (init / REPORT).is_file():
return False
folder = stage_dir(project_root)
folder.mkdir(parents=True, exist_ok=True)
for name in (REPORT, META):
shutil.copy2(init / name, folder / name)
if (folder / "fig").is_dir():
shutil.rmtree(folder / "fig")
shutil.copytree(init / "fig", folder / "fig")
return True
def save(project_root: Path, book: dict[str, Any]) -> None:
"""[저장] — 브라우저 `계산값` 은 버리고 서버 풀이로 · meta(지문 · 서명)는 그대로."""
clean = with_server_values(book)
atomic_write_json(stage_dir(project_root) / REPORT, clean)
def figures_of(project_root: Path, book: dict[str, Any]) -> dict[str, bytes]:
"""책이 가리키는 그림 열쇠 → PNG(있는 것만) — xlsx · PDF 내보내기용."""
folder = stage_dir(project_root)
out: dict[str, bytes] = {}
for sheet in book.get("시트") or []:
for fig in sheet.get("그림") or []:
path = fig_path(folder, fig["열쇠"])
if path.is_file():
out[fig["열쇠"]] = path.read_bytes()
return out