feat(534-5): E05 안정성평가 보고서 다듬기 — 목차(쪽 = 실제 PDF 쪽) · 머리말 사업명 · 꼬리말 n / N(표지 뺌) · 측점표 = 판정 일람표(대책 필요 열 · 쪽마다 고르게 나눔) · 대표 단면 그림(안전율 상자 · 지층 무늬 범례 · 성토 점 · 축척 막대 · 해석에 쓴 넓힌 지반선 · 흑백 선 모양 · 높이 = 엔진 값) · 부록 절편 계산표(대표 단면마다 지배 조건 한 쪽) · 「사용자 확정 전」 표기 한 규칙(기준 안전율 · 노면 하중 · 우기 지하수 갈래 · 물성 · 광역 기본값 줄) · 표지 꾸밈 · 옛 작업본 갱신 = 목차 자리 · 서식 붙임 · Build 나눔(Cells · Section · Print) · 공용 PDF 엔진에 머리말 · 꼬리말 칸 덧붙임(기본 동작 그대로)(요구 347-5 · 351)
시험 고침: 534-3 양식 시험의 시트 목록 · 쪽 수 기대(목차 · 부록 더함) · E05 시험 시트 수 7 → 9 · PDF = report_pdf(목차 쪽 번호 = 실제 · 꼬리말 · 머리말) · 부록 절편 · 측점표 쪽 나눔 · 옛 작업본 갱신 시험 덧. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,10 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from E02_StabGround.E02_StabGround_Model import load_defaults
|
||||
from E05_StabReport.E05_StabReport_Cells import UNCONFIRMED
|
||||
from E07_StabArea.E07_StabArea_Engine import CLASS_KEYS, LEVEL_KEYS
|
||||
from E07_StabArea.E07_StabArea_Router import RESULT, load
|
||||
from E07_StabArea.E07_StabArea_Router import RESULT, AreaRequest, load
|
||||
|
||||
LEVEL_NAMES = {"low": "낮음", "mid": "보통", "high": "높음"}
|
||||
|
||||
@@ -49,6 +51,35 @@ def _set(sheet: dict[str, Any], a1: str, value: Any, style: int | None = None) -
|
||||
sheet["칸"][a1] = cell
|
||||
|
||||
|
||||
UNCONFIRMED_KEYS = ( # (설정 칸, 글) — 기본값 그대로면 사용자 확정 전
|
||||
("c_root", "뿌리 점착 {c_root:g} kPa"),
|
||||
("soil_depth_m", "토층 {soil_depth_m:g} m"),
|
||||
("tr_lo", "T/R {tr_lo:g}~{tr_hi:g} m"),
|
||||
("level_lo_pct", "측점 문턱 {level_lo_pct:g} % · {level_hi_pct:g} %"),
|
||||
)
|
||||
PAIRS = {"tr_lo": "tr_hi", "level_lo_pct": "level_hi_pct"}
|
||||
|
||||
|
||||
def _unconfirmed(s: dict[str, Any]) -> str:
|
||||
"""기본값 그대로인 설정 · 물성(E02 붕적토 문헌 추정치) — 사용자 확정 전 한 줄."""
|
||||
fields = AreaRequest.model_fields
|
||||
same = [
|
||||
text.format(**s)
|
||||
for key, text in UNCONFIRMED_KEYS
|
||||
if all(
|
||||
k in s and float(s[k]) == float(fields[k].default) for k in (key, PAIRS.get(key, key))
|
||||
)
|
||||
]
|
||||
base = {m["id"]: m for m in load_defaults()["materials"]}.get(
|
||||
s.get("material_id") or "colluvium"
|
||||
)
|
||||
if base and all(
|
||||
float(s.get(k, -1)) == float(base[k]) for k in ("gamma_t", "gamma_sat", "c", "phi")
|
||||
):
|
||||
same.insert(0, f"토층 물성 = {base['name']} 문헌 추정치")
|
||||
return f"{' · '.join(same)} — {UNCONFIRMED}" if same else "없음(모두 사용자 입력)"
|
||||
|
||||
|
||||
def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[float, str]) -> None:
|
||||
"""s7 — 조건 넷 · 등급별 면적 여섯 · 측점 등급별 수 · 보통 · 높음 측점(22 행부터 한 줄씩)."""
|
||||
if not area:
|
||||
@@ -68,6 +99,7 @@ def fill_area(sheet: dict[str, Any], area: dict[str, Any] | None, labels: dict[f
|
||||
)
|
||||
_set(sheet, "B7", scenario_text(s))
|
||||
_set(sheet, "B8", str(area["computed_at"]).replace("T", " "))
|
||||
_set(sheet, "B9", _unconfirmed(s))
|
||||
for k in range(1, 7):
|
||||
_set(sheet, f"C{11 + k}", float(summary["areas_m2"][CLASS_KEYS[k]]))
|
||||
lo, hi = s["level_lo_pct"], s["level_hi_pct"]
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""E05 안정성평가 보고서 빌더(요구 347-5 · 351 · 요구서 4-3 · PLAN 534-3).
|
||||
"""E05 안정성평가 보고서 빌더(요구 347-5 · 351 · 요구서 4-3 · PLAN 534-3 · 534-5).
|
||||
|
||||
입력 = E02 지반 조건(`ground.json`) · E03 2D 파생 저장본(`stab2d_summary` 가장 최근 · 측점별
|
||||
`stab2d_station`) · 대표 단면(E03 자동 + `representative.json` 켬/끔) · E04 3D 저장본 요약(있으면) · E07 광역 저장본(있으면) ·
|
||||
E03 보강 검토 저장본(있으면 · 없으면 s8 뺌) ·
|
||||
`stab2d_station`) · 대표 단면(E03 자동 + `representative.json` 켬/끔) · E04 3D 저장본 요약
|
||||
(있으면) · E07 광역 저장본(있으면) · E03 보강 검토 저장본(있으면 · 없으면 s8 뺌) ·
|
||||
E01 복사본(측점 · 횡단 · 노선).
|
||||
양식 = `resources/master_template/report/stab_report.json`
|
||||
칸 자리 = 같은 폴더 `stab_report_slots.md`.
|
||||
|
||||
몫: 사용자(표지 s1 · 개요 s2) = 처음 만들 때만 채움 ·
|
||||
프로그램(입력조건 s3 · 측점표 s4 · 대표단면 s5 · 3D요약 s6 · 광역요약 s7 · 보강검토 s8)
|
||||
= [보고서 갱신] 때 양식에서 통째로 다시 씀.
|
||||
프로그램(목차 s9 · 입력조건 s3 · 측점표 s4 · 대표단면 s5 · 3D요약 s6 · 광역요약 s7 · 보강검토 s8 ·
|
||||
부록_절편 s10) = [보고서 갱신] 때 양식에서 통째로 다시 씀. 대표 단면 · 부록 = `_Section` ·
|
||||
목차 쪽 번호 · 머리말/꼬리말 · 긴 표 쪽 나눔 = `_Print` · 칸 공용 = `_Cells`.
|
||||
판정 글 칸 = 엔진 판정 값 + N.G 서식 10 · O.K 서식 11 을 직접 붙임
|
||||
(인쇄 HTML · xlsx 는 조건부 서식을 안 먹음).
|
||||
|
||||
@@ -20,7 +21,6 @@ storage(작업본 · 초기값 · 그림 · 옛 판) = `E05_StabReport_Store.py`
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
@@ -32,41 +32,37 @@ from common_util.common_util_derived_store import read_derived
|
||||
from E01_StabRegister import E01_StabRegister_Source as source
|
||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||
from E03_StabSection import E03_StabSection_View as view2d
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import final_surface
|
||||
from E03_StabSection.E03_StabSection_Router import KIND_STATION, _latest_summary, _overrides
|
||||
from E04_Stab3D import E04_Stab3D_Run as run3d
|
||||
from E05_StabReport import E05_StabReport_Area as area
|
||||
from E05_StabReport import E05_StabReport_Figure as figure
|
||||
from E05_StabReport import E05_StabReport_Print as printer
|
||||
from E05_StabReport import E05_StabReport_Reinforce as reinforce
|
||||
from E05_StabReport import E05_StabReport_Section as section
|
||||
from E05_StabReport.E05_StabReport_Cells import ( # noqa: F401 — 시험 · 다른 모듈이 build.* 로 씀
|
||||
A1,
|
||||
SIDE_NAMES,
|
||||
ST_NG,
|
||||
ST_OK,
|
||||
_copy_row,
|
||||
_criterion,
|
||||
_label_at,
|
||||
_labels,
|
||||
_seismic,
|
||||
_set,
|
||||
_verdict,
|
||||
fig_key,
|
||||
mark,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FORM_PATH = ROOT / "resources" / "master_template" / "report" / "stab_report.json"
|
||||
STAGE = "E05_StabReport"
|
||||
REPORT, META = "report.json", "report_meta.json"
|
||||
USER_SHEETS = ("s1", "s2")
|
||||
PROGRAM_SHEETS = ("s3", "s4", "s5", "s6", "s7", "s8")
|
||||
BLOCK = 28 # 대표단면 한 쪽 = 28 행
|
||||
PROGRAM_SHEETS = ("s9", "s3", "s4", "s5", "s6", "s7", "s8", "s10")
|
||||
REPRESENTATIVE = Path("E03_StabSection") / "representative.json"
|
||||
BAND_KEY = "fig/3d_band.png"
|
||||
LOWEST_N = view2d.REPRESENTATIVE_LOWEST
|
||||
CASES = ("dry", "wet", "seismic")
|
||||
SIDE_NAMES = {"cut": "깎기", "fill": "쌓기"}
|
||||
METHOD_NAMES = {
|
||||
"bishop": "Bishop",
|
||||
"janbu": "Janbu 보정",
|
||||
"janbu_corrected": "Janbu 보정",
|
||||
"ordinary": "Fellenius",
|
||||
"spencer": "Spencer",
|
||||
}
|
||||
REASONS = {
|
||||
"fill_height": "쌓기 높이 10 m 이상",
|
||||
"cut_height": "깎기 높이 15 m 이상",
|
||||
"lowest": "최소 안전율 하위",
|
||||
"user": "사용자 지정",
|
||||
}
|
||||
GROUND_NAMES = {"soil": "토사", "ripping_rock": "리핑암", "blasting_rock": "발파암", "fill": "성토"}
|
||||
ST_TEXT, ST_BODY, ST_FS, ST_NG, ST_OK, ST_NUM1, ST_PROP, ST_CHAIN = 5, 7, 8, 10, 11, 13, 16, 17
|
||||
A1 = re.compile(r"^([A-Z]+)(\d+)$")
|
||||
|
||||
|
||||
class MissingInput(Exception):
|
||||
@@ -86,10 +82,6 @@ def initial_dir(project_root: Path) -> Path:
|
||||
return Path(project_root) / "initial_snapshot" / STAGE
|
||||
|
||||
|
||||
def fig_key(station_key: str) -> str:
|
||||
return f"fig/{station_key}.png"
|
||||
|
||||
|
||||
def fig_path(folder: Path, key: str) -> Path:
|
||||
"""그림 열쇠 → 파일(윈도우 못 쓰는 글자 `:` 등은 `_`)."""
|
||||
name = key.removeprefix("fig/")
|
||||
@@ -108,9 +100,10 @@ def _ground(project_root: Path) -> dict[str, Any]:
|
||||
)
|
||||
|
||||
|
||||
def _station_result(project_root: Path, fp: str) -> dict[str, Any] | None:
|
||||
saved = read_derived(Path(project_root), KIND_STATION, fp)
|
||||
return (saved or {}).get("value", {}).get("out")
|
||||
def _station_result(project_root: Path, fp: str) -> tuple[dict[str, Any] | None, list | None]:
|
||||
"""측점 저장본 — (결과, 해석에 쓴 넓힌 지반선 · 없으면 None)."""
|
||||
value = (read_derived(Path(project_root), KIND_STATION, fp) or {}).get("value") or {}
|
||||
return value.get("out"), (value.get("inputs") or {}).get("ground")
|
||||
|
||||
|
||||
def _row2d(row: dict[str, Any], method: str) -> dict[str, Any]:
|
||||
@@ -204,7 +197,7 @@ def gather(project_root: Path) -> dict[str, Any]:
|
||||
reps = _representatives(r2)
|
||||
for item in reps:
|
||||
chainage = float(item["summary"]["chainage_m"])
|
||||
item["result"] = _station_result(project_root, item["summary"]["fp"])
|
||||
item["result"], item["ground"] = _station_result(project_root, item["summary"]["fp"])
|
||||
try:
|
||||
item["cross"] = source.cross_section(root, chainage)
|
||||
except KeyError:
|
||||
@@ -229,63 +222,6 @@ def gather(project_root: Path) -> dict[str, Any]:
|
||||
|
||||
|
||||
# ── 칸 쓰기 ──────────────────────────────────────────────────────────────────
|
||||
def _set(sheet: dict[str, Any], a1: str, value: Any, style: int | None = None) -> None:
|
||||
"""값(None = 지움) · 서식(None = 양식 그대로)."""
|
||||
cell = dict(sheet["칸"].get(a1) or {})
|
||||
cell.pop("식", None)
|
||||
if value is None or value == "":
|
||||
cell.pop("값", None)
|
||||
else:
|
||||
cell["값"] = round(value, 6) if isinstance(value, float) else value
|
||||
if style is not None:
|
||||
cell["서식"] = style
|
||||
if cell:
|
||||
sheet["칸"][a1] = cell
|
||||
else:
|
||||
sheet["칸"].pop(a1, None)
|
||||
|
||||
|
||||
def _copy_row(sheet: dict[str, Any], src: int, dst: int, cols: str) -> None:
|
||||
for col in cols:
|
||||
cell = sheet["칸"].get(f"{col}{src}")
|
||||
if cell and dst != src:
|
||||
sheet["칸"][f"{col}{dst}"] = {"서식": cell["서식"]} if "서식" in cell else {}
|
||||
|
||||
|
||||
def _shift(a1: str, dr: int) -> str:
|
||||
m = A1.match(a1)
|
||||
return f"{m.group(1)}{int(m.group(2)) + dr}"
|
||||
|
||||
|
||||
def _verdict(sheet: dict[str, Any], a1: str, ok: bool | None) -> None:
|
||||
if ok is None:
|
||||
_set(sheet, a1, None)
|
||||
else:
|
||||
_set(sheet, a1, "O.K" if ok else "N.G", ST_OK if ok else ST_NG)
|
||||
|
||||
|
||||
def _criterion(ground: dict[str, Any], case: str, side: str | None = None) -> float:
|
||||
sf = (ground.get("safety_factors") or {}).get(case) or {}
|
||||
if side in ("cut", "fill"):
|
||||
return float(sf[side])
|
||||
return max(float(sf["cut"]), float(sf["fill"])) # 표 하나 = 엄한 쪽
|
||||
|
||||
|
||||
def _seismic(ground: dict[str, Any]) -> bool:
|
||||
return bool((ground.get("seismic") or {}).get("enabled"))
|
||||
|
||||
|
||||
def _labels(stations: list[dict[str, Any]]) -> dict[float, str]:
|
||||
return {round(float(s["chainage_m"]), 3): s.get("label") or "" for s in stations}
|
||||
|
||||
|
||||
def _label_at(labels: dict[float, str], chainage: float) -> str:
|
||||
if not labels:
|
||||
return f"{chainage:.2f}"
|
||||
near = min(labels, key=lambda c: abs(c - chainage))
|
||||
return labels[near] or f"{chainage:.2f}"
|
||||
|
||||
|
||||
# ── 사용자 몫(처음 한 번) ────────────────────────────────────────────────────
|
||||
def fill_cover(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||||
"""표지 — 사업명 · 노선명(지역 · 임도 종류) · 작성일. 발주처 · 작성자 = 사용자가 씀."""
|
||||
@@ -307,9 +243,30 @@ WATER_TEXT = {
|
||||
}
|
||||
|
||||
|
||||
def _wet_text(water: dict[str, Any], base: dict[str, Any]) -> str:
|
||||
"""우기 지하수 갈래 글(E02 `wet_mode`) · 기본값이면 사용자 확정 전 표기."""
|
||||
mode = water.get("wet_mode") or "kds"
|
||||
if mode == "depth_ratio":
|
||||
ratio = float(water["depth_ratio"])
|
||||
return mark(f"지표 아래 깊이 = 비탈 높이 × {ratio:g}", ratio == float(base["depth_ratio"]))
|
||||
if mode == "ru":
|
||||
ru = float(water["ru"])
|
||||
return mark(f"간극수압비 r_u = {ru:g}", ru == float(base["ru"]))
|
||||
kds = (
|
||||
f"토사 · 풍화암: {WATER_TEXT.get(water.get('wet_soil'), '-')} · "
|
||||
f"암반: {WATER_TEXT.get(water.get('wet_rock'), '-')}"
|
||||
)
|
||||
if mode == "line":
|
||||
return f"사용자 수위선 {len(water.get('lines') or [])} 구간 · 구간 밖 = {kds} (사용자 입력)"
|
||||
same = all(water.get(k) == base.get(k) for k in ("wet_soil", "wet_rock"))
|
||||
return mark(f"KDS 기본 — {kds}", same)
|
||||
|
||||
|
||||
def fill_conditions(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||||
"""s3 입력조건."""
|
||||
ground = inputs["ground"]
|
||||
base = ground_model.load_defaults()
|
||||
base_sf = base["safety_factors"]
|
||||
_set(sheet, "B5", "한계평형해석 — Bishop 간편법(기본) · Janbu 보정 · Fellenius 비교")
|
||||
_set(sheet, "B6", "원호(격자 · 진입-탈출) + 비원호(꺾은선) · 측점마다 좌 · 우 비탈 따로")
|
||||
_set(
|
||||
@@ -326,16 +283,14 @@ def fill_conditions(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||||
_set(sheet, f"C{r}", "지진 검토 안 함")
|
||||
continue
|
||||
sf = ground["safety_factors"][case]
|
||||
sf_base = base_sf.get(case) or {}
|
||||
same = all(float(sf_base.get(k, -1)) == float(sf[k]) for k in ("cut", "fill"))
|
||||
_set(sheet, f"B{r}", _criterion(ground, case))
|
||||
_set(sheet, f"C{r}", f"깎기 {sf['cut']:.2f} · 쌓기 {sf['fill']:.2f} — 표 값 = 엄한 쪽")
|
||||
text = f"깎기 {sf['cut']:.2f} · 쌓기 {sf['fill']:.2f} — 표 값 = 엄한 쪽 · KDS 11 70 05"
|
||||
_set(sheet, f"C{r}", mark(text, same))
|
||||
water = ground.get("groundwater") or {}
|
||||
_set(sheet, "B17", WATER_TEXT.get(water.get("dry"), "-"))
|
||||
_set(
|
||||
sheet,
|
||||
"B18",
|
||||
f"토사 · 풍화암: {WATER_TEXT.get(water.get('wet_soil'), '-')} · "
|
||||
f"암반: {WATER_TEXT.get(water.get('wet_rock'), '-')}",
|
||||
)
|
||||
_set(sheet, "B18", _wet_text(water, base["groundwater"]))
|
||||
seismic = ground.get("seismic") or {}
|
||||
_set(
|
||||
sheet,
|
||||
@@ -344,8 +299,11 @@ def fill_conditions(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||||
if _seismic(ground)
|
||||
else "적용 안 함",
|
||||
)
|
||||
_set(sheet, "B22", float(ground.get("road_surcharge_kpa") or 0))
|
||||
defaults = {m["id"]: m for m in ground_model.load_defaults()["materials"]}
|
||||
load = float(ground.get("road_surcharge_kpa") or 0)
|
||||
_set(sheet, "B22", load)
|
||||
default_load = float(base["road_surcharge_kpa"])
|
||||
_set(sheet, "D22", mark("옹벽 배면 상재하중 준용", load == default_load))
|
||||
defaults = {m["id"]: m for m in base["materials"]}
|
||||
names = {m["id"]: m["name"] for m in ground.get("materials") or []}
|
||||
mapped = set((ground.get("label_map") or {}).values()) # 지반 라벨에 이은 지층만 「전체」
|
||||
rows: list[tuple[dict[str, Any], str]] = [
|
||||
@@ -374,7 +332,9 @@ def fill_conditions(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||||
float(base[f]) == float(m.get(f, -1)) for f in ground_model.MATERIAL_FIELDS
|
||||
)
|
||||
_set(
|
||||
sheet, f"G{r}", ("문헌 추정치(기본값)" if same else "사용자 입력") if "c" in m else None
|
||||
sheet,
|
||||
f"G{r}",
|
||||
("문헌 추정치 · 사용자 확정 전" if same else "사용자 입력") if "c" in m else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -387,7 +347,7 @@ def fill_stations(sheet: dict[str, Any], inputs: dict[str, Any]) -> int:
|
||||
ng = 0
|
||||
for i, st in enumerate(rows):
|
||||
r = 6 + i
|
||||
_copy_row(sheet, 6, r, "ABCDEFGHIJ")
|
||||
_copy_row(sheet, 6, r, "ABCDEFGHIJK")
|
||||
side = st.get("side")
|
||||
fs = st.get("fs_by_condition") or {}
|
||||
_set(sheet, f"A{r}", _label_at(labels, float(st["chainage_m"])))
|
||||
@@ -402,160 +362,10 @@ def fill_stations(sheet: dict[str, Any], inputs: dict[str, Any]) -> int:
|
||||
ok = None if verdict not in ("ok", "ng") else verdict == "ok"
|
||||
ng += ok is False
|
||||
_verdict(sheet, f"J{r}", ok)
|
||||
_set(sheet, f"K{r}", None if ok is None else "-" if ok else "대책 검토")
|
||||
return ng
|
||||
|
||||
|
||||
def _case_slip(result: dict[str, Any] | None, condition: str, side: str, method: str):
|
||||
"""조건 하나 — 그 비탈 역할(깎기 · 쌓기) 중 그 방법 최소 면 · 높이."""
|
||||
best = None
|
||||
for case in (result or {}).get("cases") or []:
|
||||
if case.get("condition") != condition:
|
||||
continue
|
||||
if side in ("cut", "fill") and case.get("role") not in (side, None):
|
||||
continue
|
||||
methods = (case.get("result") or {}).get("methods") or {}
|
||||
found = (methods.get(method) or {}).get("min") or next(
|
||||
(v["min"] for v in methods.values() if v.get("min")), None
|
||||
)
|
||||
if found and found.get("F") is not None and (best is None or found["F"] < best[0]["F"]):
|
||||
best = (found, case)
|
||||
return best
|
||||
|
||||
|
||||
def _block_copy(sheet: dict[str, Any], template: dict[str, Any], k: int) -> None:
|
||||
"""양식 1~28 행 덩이를 k 번째 자리로(칸 · 병합 · 행 높이 · 조건부서식)."""
|
||||
dr = BLOCK * k
|
||||
for a1, cell in template["칸"].items():
|
||||
if int(A1.match(a1).group(2)) <= BLOCK:
|
||||
sheet["칸"][_shift(a1, dr)] = copy.deepcopy(cell)
|
||||
for m in template.get("병합", []):
|
||||
sheet["병합"].append(":".join(_shift(p, dr) for p in m.split(":")))
|
||||
for r, info in (template.get("행") or {}).items():
|
||||
if int(r) <= BLOCK:
|
||||
sheet["행"][str(int(r) + dr)] = dict(info)
|
||||
for rule in template.get("조건부서식", []):
|
||||
moved = copy.deepcopy(rule)
|
||||
moved["id"] = f"{rule['id']}_{k}"
|
||||
moved["범위"] = [{**g, "r0": g["r0"] + dr, "r1": g["r1"] + dr} for g in rule["범위"]]
|
||||
sheet["조건부서식"].append(moved)
|
||||
|
||||
|
||||
CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
|
||||
|
||||
|
||||
def fill_sections(sheet: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[str, str]]:
|
||||
"""s5 대표단면 — 단면 하나 = 28 행 덩이 = 한 쪽 · 그림 일감 [(열쇠, SVG)]."""
|
||||
template = copy.deepcopy(sheet)
|
||||
reps = inputs["representatives"]
|
||||
ground = inputs["ground"]
|
||||
labels = _labels(inputs["stations"])
|
||||
sheet["칸"] = {}
|
||||
sheet["병합"], sheet["행"], sheet["조건부서식"], sheet["그림"] = [], {}, [], []
|
||||
if not reps:
|
||||
_block_copy(sheet, template, 0)
|
||||
_set(sheet, "A1", "대표 단면 — 해당 없음")
|
||||
sheet["인쇄"]["쪽나눔"] = []
|
||||
return []
|
||||
jobs: list[tuple[str, str]] = []
|
||||
for k, rep in enumerate(reps):
|
||||
dr = BLOCK * k
|
||||
_block_copy(sheet, template, k)
|
||||
st = rep["summary"]
|
||||
side = st.get("side")
|
||||
method = st.get("method") or "bishop"
|
||||
label = _label_at(labels, float(st["chainage_m"]))
|
||||
picks = {c: _case_slip(rep.get("result"), c, side, method) for c in CASES}
|
||||
dry = picks.get("dry")
|
||||
height = float(dry[1].get("height_m") or 0) if dry else 0.0
|
||||
mark = CIRCLED[k] if k < len(CIRCLED) else str(k + 1)
|
||||
_set(
|
||||
sheet,
|
||||
f"A{1 + dr}",
|
||||
f"대표 단면 {mark} — {label} · {SIDE_NAMES.get(side, side or '-')} · H {height:.1f} m",
|
||||
)
|
||||
reason = REASONS.get(rep.get("reason"), rep.get("reason") or "")
|
||||
if rep.get("reason") == "lowest":
|
||||
reason = f"{reason} {LOWEST_N}"
|
||||
_set(sheet, f"B{2 + dr}", reason)
|
||||
key = fig_key(rep["station_key"])
|
||||
sheet["그림"].append(
|
||||
{"열쇠": key, "닻": f"A{3 + dr}:H{22 + dr}", "이름": f"대표 단면 {label}"}
|
||||
)
|
||||
fs = st.get("fs_by_condition") or {}
|
||||
slips = []
|
||||
for r, case in zip((24, 25, 26), CASES):
|
||||
row = r + dr
|
||||
if case == "seismic" and not _seismic(ground):
|
||||
continue
|
||||
value = fs.get(case)
|
||||
crit = _criterion(ground, case, side)
|
||||
pick = picks.get(case)
|
||||
_set(sheet, f"B{row}", METHOD_NAMES.get(method, method))
|
||||
_set(sheet, f"C{row}", float(value) if value is not None else None)
|
||||
_set(sheet, f"D{row}", crit)
|
||||
_verdict(sheet, f"E{row}", None if value is None else float(value) >= crit - 1e-9)
|
||||
slip = pick[0] if pick else None
|
||||
center = (slip or {}).get("center")
|
||||
_set(sheet, f"F{row}", float(slip["radius"]) if slip and slip.get("radius") else None)
|
||||
_set(sheet, f"G{row}", float(center[0]) if center else None)
|
||||
_set(sheet, f"H{row}", float(center[1]) if center else None)
|
||||
if slip and slip.get("points"):
|
||||
slips.append({"condition": case, "F": slip.get("F"), "points": slip["points"]})
|
||||
design = (rep.get("cross") or {}).get("design") or {}
|
||||
kind = (dry[0].get("kind") if dry else None) or ""
|
||||
layers = rep.get("layers") or {}
|
||||
note = [
|
||||
{"circle": "활동면 원호", "polyline": "활동면 비원호"}.get(kind, ""),
|
||||
f"지반 {GROUND_NAMES.get(design.get('ground_type'), design.get('ground_type') or '-')}",
|
||||
f"토층 두께 {layers['soil_thickness_m']:g} m" if layers.get("soil_thickness_m") else "",
|
||||
]
|
||||
_set(sheet, f"B{27 + dr}", " · ".join(n for n in note if n))
|
||||
jobs.append((key, _section_figure(rep, slips, f"{label} — {reason}")))
|
||||
sheet["인쇄"]["쪽나눔"] = [BLOCK * (k + 1) for k in range(len(reps))]
|
||||
return jobs
|
||||
|
||||
|
||||
def _section_figure(rep: dict[str, Any], slips: list[dict[str, Any]], title: str) -> str:
|
||||
cross = rep.get("cross") or {}
|
||||
design = cross.get("design") or {}
|
||||
ground = sorted(
|
||||
(float(s["offset_m"]), float(s["z"]))
|
||||
for s in cross.get("samples") or []
|
||||
if s.get("valid", True) and s.get("z") is not None
|
||||
)
|
||||
line = sorted(
|
||||
(float(p["offset_m"]), float(p["elevation_m"])) for p in design.get("design_line") or []
|
||||
)
|
||||
surface = [tuple(p) for p in final_surface(ground, line)] if ground and line else ground
|
||||
thickness = (rep.get("layers") or {}).get("soil_thickness_m")
|
||||
rock = [(x, z - float(thickness)) for x, z in ground] if thickness else None
|
||||
edges = design.get("road_edges") or {}
|
||||
road = None
|
||||
if edges.get("left") and edges.get("right"):
|
||||
road = (float(edges["right"]["offset_m"]), float(edges["left"]["offset_m"]))
|
||||
heights = []
|
||||
if road and line:
|
||||
for case in (rep.get("result") or {}).get("cases") or []:
|
||||
if case.get("condition") != "dry" or not case.get("height_m"):
|
||||
continue
|
||||
left = case.get("side") == "left"
|
||||
edge = road[1] if left else road[0]
|
||||
day = line[-1][0] if left else line[0][0]
|
||||
z0, z1 = figure._interp(surface, edge), figure._interp(surface, day)
|
||||
if z0 is not None and z1 is not None:
|
||||
heights.append({"x": day, "z0": z0, "z1": z1})
|
||||
return figure.section_svg(
|
||||
title=title,
|
||||
ground=ground,
|
||||
surface=surface,
|
||||
rock=rock,
|
||||
slips=slips,
|
||||
road=road,
|
||||
heights=heights,
|
||||
wet=any(s["condition"] == "wet" for s in slips),
|
||||
)
|
||||
|
||||
|
||||
def fill_3d(sheet: dict[str, Any], inputs: dict[str, Any]) -> tuple[str, str]:
|
||||
"""s6 3D요약 — 구간 하나 = 한 줄(20 행부터) · 평면 위험 띠 그림 일감."""
|
||||
ground = inputs["ground"]
|
||||
@@ -620,14 +430,22 @@ def build_program(book: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[st
|
||||
"""양식 사본 `book` 의 프로그램 몫 시트를 채움 · 그림 일감."""
|
||||
by = {s["id"]: s for s in book["시트"]}
|
||||
fill_conditions(by["s3"], inputs)
|
||||
n = len(inputs["result2d"]["stations"])
|
||||
fill_stations(by["s4"], inputs)
|
||||
jobs = fill_sections(by["s5"], inputs)
|
||||
printer.balance_breaks(by["s4"], book, 6, 5 + n)
|
||||
jobs = section.fill_sections(by["s5"], inputs)
|
||||
section.fill_slices(by["s10"], inputs)
|
||||
jobs.append(fill_3d(by["s6"], inputs))
|
||||
area.fill_area(by["s7"], inputs["area"], _labels(inputs["stations"]))
|
||||
risky = [
|
||||
r for r in (inputs["area"] or {}).get("stations") or [] if r.get("level") in ("mid", "high")
|
||||
]
|
||||
printer.balance_breaks(by["s7"], book, 22, 21 + len(risky))
|
||||
if inputs.get("reinforce"):
|
||||
reinforce.fill_reinforce(by["s8"], inputs["reinforce"], _labels(inputs["stations"]))
|
||||
else: # 보강 검토가 없으면 시트를 뺌
|
||||
book["시트"] = [s for s in book["시트"] if s["id"] != "s8"]
|
||||
printer.fill_toc(by["s9"], book, None) # 쪽 번호 = 저장 전 `printer.number_toc`
|
||||
return jobs
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""E05 보고서 칸 쓰기 공용 — 이름표 · 서식 번호 · 칸 값 · 판정 · 기준 안전율 · 측점 이름.
|
||||
|
||||
(PLAN 534-3 · 534-5) 빌더 · 대표 단면(`_Section`) · 인쇄(`_Print`)가 같이 씀.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
CASES = ("dry", "wet", "seismic")
|
||||
SIDE_NAMES = {"cut": "깎기", "fill": "쌓기"}
|
||||
METHOD_NAMES = {
|
||||
"bishop": "Bishop",
|
||||
"janbu": "Janbu 보정",
|
||||
"janbu_corrected": "Janbu 보정",
|
||||
"ordinary": "Fellenius",
|
||||
"spencer": "Spencer",
|
||||
}
|
||||
REASONS = {
|
||||
"fill_height": "쌓기 높이 10 m 이상",
|
||||
"cut_height": "깎기 높이 15 m 이상",
|
||||
"lowest": "최소 안전율 하위",
|
||||
"user": "사용자 지정",
|
||||
}
|
||||
GROUND_NAMES = {"soil": "토사", "ripping_rock": "리핑암", "blasting_rock": "발파암", "fill": "성토"}
|
||||
ST_TEXT, ST_BODY, ST_FS, ST_NG, ST_OK, ST_NUM1, ST_PROP, ST_CHAIN = 5, 7, 8, 10, 11, 13, 16, 17
|
||||
A1 = re.compile(r"^([A-Z]+)(\d+)$")
|
||||
|
||||
|
||||
def fig_key(station_key: str) -> str:
|
||||
return f"fig/{station_key}.png"
|
||||
|
||||
|
||||
UNCONFIRMED = "기본값 · 사용자 확정 전"
|
||||
|
||||
|
||||
def mark(text: str, default: bool) -> str:
|
||||
"""사용자 확정 전 표기 한 규칙 — 기본값 그대로 = 「(기본값 · 사용자 확정 전)」 · 아니면
|
||||
「(사용자 입력)」."""
|
||||
return f"{text} ({UNCONFIRMED if default else '사용자 입력'})"
|
||||
|
||||
|
||||
def _set(sheet: dict[str, Any], a1: str, value: Any, style: int | None = None) -> None:
|
||||
"""값(None = 지움) · 서식(None = 양식 그대로)."""
|
||||
cell = dict(sheet["칸"].get(a1) or {})
|
||||
cell.pop("식", None)
|
||||
if value is None or value == "":
|
||||
cell.pop("값", None)
|
||||
else:
|
||||
cell["값"] = round(value, 6) if isinstance(value, float) else value
|
||||
if style is not None:
|
||||
cell["서식"] = style
|
||||
if cell:
|
||||
sheet["칸"][a1] = cell
|
||||
else:
|
||||
sheet["칸"].pop(a1, None)
|
||||
|
||||
|
||||
def _copy_row(sheet: dict[str, Any], src: int, dst: int, cols: str) -> None:
|
||||
for col in cols:
|
||||
cell = sheet["칸"].get(f"{col}{src}")
|
||||
if cell and dst != src:
|
||||
sheet["칸"][f"{col}{dst}"] = {"서식": cell["서식"]} if "서식" in cell else {}
|
||||
|
||||
|
||||
def _shift(a1: str, dr: int) -> str:
|
||||
m = A1.match(a1)
|
||||
return f"{m.group(1)}{int(m.group(2)) + dr}"
|
||||
|
||||
|
||||
def _verdict(sheet: dict[str, Any], a1: str, ok: bool | None) -> None:
|
||||
if ok is None:
|
||||
_set(sheet, a1, None)
|
||||
else:
|
||||
_set(sheet, a1, "O.K" if ok else "N.G", ST_OK if ok else ST_NG)
|
||||
|
||||
|
||||
def _criterion(ground: dict[str, Any], case: str, side: str | None = None) -> float:
|
||||
sf = (ground.get("safety_factors") or {}).get(case) or {}
|
||||
if side in ("cut", "fill"):
|
||||
return float(sf[side])
|
||||
return max(float(sf["cut"]), float(sf["fill"])) # 표 하나 = 엄한 쪽
|
||||
|
||||
|
||||
def _seismic(ground: dict[str, Any]) -> bool:
|
||||
return bool((ground.get("seismic") or {}).get("enabled"))
|
||||
|
||||
|
||||
def _labels(stations: list[dict[str, Any]]) -> dict[float, str]:
|
||||
return {round(float(s["chainage_m"]), 3): s.get("label") or "" for s in stations}
|
||||
|
||||
|
||||
def _label_at(labels: dict[float, str], chainage: float) -> str:
|
||||
if not labels:
|
||||
return f"{chainage:.2f}"
|
||||
near = min(labels, key=lambda c: abs(c - chainage))
|
||||
return labels[near] or f"{chainage:.2f}"
|
||||
@@ -1,8 +1,8 @@
|
||||
"""E05 보고서 그림 — 서버 SVG(요구 347-5 · PLAN 534-3). PNG 는 `common_util_svg_png` 가 만듦.
|
||||
|
||||
section_svg(…) → 대표 단면(712 × 533 · 4 : 3) — 원지반 · 공사 뒤 지표 · 암 경계 ·
|
||||
우기 지하수위 ·
|
||||
조건별 임계 활동면 · 노폭 · 비탈 높이 치수 · 표고 · 거리 눈금
|
||||
section_svg(…) → 대표 단면(712 × 640) — 원지반 · 공사 뒤 지표 · 지층(무늬) · 성토(점) ·
|
||||
우기 지하수위 · 조건별 임계 활동면(선 모양) · 노폭 · 비탈 높이 치수 ·
|
||||
표고 · 거리 눈금 · 축척 막대 · 안전율 상자 · 지층 범례
|
||||
band_svg(…) → 평면 위험 띠(684 × 320) — 노선 · 측점 2D 판정 색 · 3D 구간 띠
|
||||
|
||||
좌표 = B06 횡단 offset(좌 +) · 화면 왼쪽 = +offset
|
||||
@@ -16,7 +16,7 @@ import math
|
||||
from html import escape
|
||||
from typing import Any
|
||||
|
||||
SECTION_W, SECTION_H = 712, 533
|
||||
SECTION_W, SECTION_H = 712, 640
|
||||
BAND_W, BAND_H = 684, 320
|
||||
OK, NG, MUTED, INK = "#2E7D32", "#C00000", "#9E9E9E", "#222222"
|
||||
CONDITION_COLORS = {"dry": "#D32F2F", "wet": "#1565C0", "seismic": "#7B1FA2"}
|
||||
@@ -54,6 +54,38 @@ def _interp(line: list[tuple[float, float]], x: float) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _legend_rows(items: list[tuple[str, str]], width: float, size: int) -> list[list[int]]:
|
||||
"""범례 항목 [(종류, 글)] → 줄마다 항목 번호(폭을 넘으면 다음 줄)."""
|
||||
rows: list[list[int]] = [[]]
|
||||
used = 0.0
|
||||
for i, (_, label) in enumerate(items):
|
||||
w = 30 + _text_w(label, size)
|
||||
if rows[-1] and used + w > width:
|
||||
rows.append([])
|
||||
used = 0.0
|
||||
rows[-1].append(i)
|
||||
used += w
|
||||
return rows
|
||||
|
||||
|
||||
def _text_w(text: str, size: int) -> float:
|
||||
"""글 폭 근사(px) — 한글 · 기호 = 글자 크기 · 그 밖 = 0.6 배."""
|
||||
return sum(size if ord(ch) > 0x2000 else 0.6 * size for ch in text)
|
||||
|
||||
|
||||
PATTERNS = (
|
||||
"<pattern id='rockHatch' width='7' height='7' patternUnits='userSpaceOnUse' "
|
||||
"patternTransform='rotate(45)'><rect width='7' height='7' fill='#E2DACB'/>"
|
||||
"<line x1='0' y1='0' x2='0' y2='7' stroke='#7A6A55' stroke-width='1'/></pattern>"
|
||||
"<pattern id='fillDots' width='6' height='6' patternUnits='userSpaceOnUse'>"
|
||||
"<rect width='6' height='6' fill='#FBF5E8'/><circle cx='3' cy='3' r='0.9' fill='#8D6E63'/>"
|
||||
"</pattern>"
|
||||
"<marker id='t' viewBox='0 0 4 10' refX='2' refY='5' markerWidth='4' markerHeight='10' "
|
||||
"orient='auto'><path d='M2 0 L2 10' stroke='#222' stroke-width='1'/></marker>"
|
||||
)
|
||||
STRATA_FILL = {"soil": "#F3E9D8", "rock": "url(#rockHatch)", "fill": "url(#fillDots)"}
|
||||
|
||||
|
||||
def section_svg(
|
||||
*,
|
||||
title: str,
|
||||
@@ -64,16 +96,20 @@ def section_svg(
|
||||
road: tuple[float, float] | None,
|
||||
heights: list[dict[str, Any]],
|
||||
wet: bool,
|
||||
strata: list[dict[str, str]] | None = None,
|
||||
fs_box: list[dict[str, Any]] | None = None,
|
||||
) -> str:
|
||||
"""대표 단면 SVG.
|
||||
"""대표 단면 SVG(흑백 인쇄에도 읽힘 — 임계면 = 선 모양 · 지층 = 무늬).
|
||||
|
||||
ground · surface · rock = [(offset, z)] · slips = [{condition, F, points}] ·
|
||||
road = (오른 끝 offset, 왼 끝 offset) · heights = [{x, z0, z1}] 비탈 높이 치수.
|
||||
road = (오른 끝 offset, 왼 끝 offset) · heights = [{x, z0, z1, label?}] 비탈 높이 치수 ·
|
||||
strata = [{kind: soil | rock | fill, text}] 지층 범례 · fs_box = [{condition, F, required, ok}]
|
||||
안전율 상자 · title = 빈 글이면 안 씀(보고서 쪽 제목이 있음).
|
||||
"""
|
||||
pad_l, pad_r, pad_t, pad_b = 56, 16, 34, 64
|
||||
pad_l, pad_r, pad_t, pad_b = 58, 18, 30 if title else 22, 88
|
||||
pts = [*ground, *surface, *(p for s in slips for p in s["points"])]
|
||||
if not pts:
|
||||
return _empty(SECTION_W, SECTION_H, title)
|
||||
return _empty(SECTION_W, SECTION_H, title or "대표 단면")
|
||||
xs = [p[0] for p in pts]
|
||||
zs = [p[1] for p in pts]
|
||||
x_lo, x_hi = min(xs), max(xs)
|
||||
@@ -94,12 +130,24 @@ def section_svg(
|
||||
def screen(line: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
||||
return [(sx(x), sz(z)) for x, z in line]
|
||||
|
||||
def area(line: list[tuple[float, float]], y: float) -> str:
|
||||
"""선과 y 사이를 닫은 다각형 점 글."""
|
||||
body = screen(line)
|
||||
return " ".join(f"{a:.1f},{b:.1f}" for a, b in [*body, (body[-1][0], y), (body[0][0], y)])
|
||||
|
||||
bottom, top = sz(z_lo), pad_t
|
||||
out = [
|
||||
f"<svg xmlns='http://www.w3.org/2000/svg' width='{SECTION_W}' height='{SECTION_H}' "
|
||||
f"viewBox='0 0 {SECTION_W} {SECTION_H}'>",
|
||||
f"<rect width='{SECTION_W}' height='{SECTION_H}' fill='#ffffff'/>",
|
||||
_text(SECTION_W / 2, 20, title, 13, font_weight="bold", fill=INK),
|
||||
f"<defs>{PATTERNS}",
|
||||
]
|
||||
if surface:
|
||||
out.append(f"<clipPath id='below'><polygon points='{area(surface, bottom)}'/></clipPath>")
|
||||
if ground:
|
||||
out.append(f"<clipPath id='above'><polygon points='{area(ground, top)}'/></clipPath>")
|
||||
out += ["</defs>", f"<rect width='{SECTION_W}' height='{SECTION_H}' fill='#ffffff'/>"]
|
||||
if title:
|
||||
out.append(_text(SECTION_W / 2, 20, title, 13, font_weight="bold", fill=INK))
|
||||
# 눈금(표고 · 거리)
|
||||
step_z = _nice_step(z_hi - z_lo, 6)
|
||||
z = math.ceil(z_lo / step_z) * step_z
|
||||
@@ -107,58 +155,57 @@ def section_svg(
|
||||
y = sz(z)
|
||||
out.append(
|
||||
f"<line x1='{pad_l}' y1='{y:.1f}' x2='{SECTION_W - pad_r}' y2='{y:.1f}' "
|
||||
"stroke='#EEEEEE' stroke-width='1'/>"
|
||||
"stroke='#E6E6E6' stroke-width='1'/>"
|
||||
)
|
||||
out.append(_text(pad_l - 6, y + 4, f"{z:g}", 10, "end", fill="#666666"))
|
||||
out.append(_text(pad_l - 6, y + 4, f"{z:g}", 11, "end", fill="#444444"))
|
||||
z += step_z
|
||||
step_x = _nice_step(x_hi - x_lo, 8)
|
||||
x = math.ceil(x_lo / step_x) * step_x
|
||||
base_y = SECTION_H - pad_b + 14
|
||||
base_y = SECTION_H - pad_b + 15
|
||||
while x <= x_hi:
|
||||
out.append(_text(sx(x), base_y, f"{x:g}", 10, fill="#666666"))
|
||||
out.append(_text(sx(x), base_y, f"{x:g}", 11, fill="#444444"))
|
||||
x += step_x
|
||||
out.append(_text(pad_l - 6, pad_t - 8, "EL. (m)", 10, "end", fill="#666666"))
|
||||
out.append(_text(pad_l - 6, pad_t - 8, "EL.(m)", 10, "end", fill="#444444"))
|
||||
out.append(
|
||||
_text(SECTION_W - pad_r, base_y + 14, "offset (m) · 좌 +", 10, "end", fill="#666666")
|
||||
_text(SECTION_W - pad_r, base_y + 15, "offset (m) · 좌 +", 10, "end", fill="#444444")
|
||||
)
|
||||
# 지반 · 지층
|
||||
if ground:
|
||||
bottom = sz(z_lo)
|
||||
body = screen(ground)
|
||||
out.append(
|
||||
"<polygon points='"
|
||||
+ " ".join(
|
||||
f"{a:.1f},{b:.1f}" for a, b in [*body, (body[-1][0], bottom), (body[0][0], bottom)]
|
||||
# 축척 막대(가로 = 세로)
|
||||
bar = _nice_step((x_hi - x_lo) / 5, 1)
|
||||
bx, by, half = pad_l + 4, base_y + 12, bar * scale / 2
|
||||
out.append(
|
||||
f"<rect x='{bx:.1f}' y='{by - 5:.1f}' width='{half:.1f}' height='4' fill='{INK}'/>"
|
||||
f"<rect x='{bx + half:.1f}' y='{by - 5:.1f}' width='{half:.1f}' height='4' "
|
||||
f"fill='#ffffff' stroke='{INK}' stroke-width='0.8'/>"
|
||||
)
|
||||
out.append(_text(bx + 2 * half + 6, by, f"{bar:g} m · 가로 = 세로 축척", 10, "start", fill=INK))
|
||||
# 지반 · 지층 — 공사 뒤 지표 아래 = 몸체 · 원지반 위 = 성토(점) · 암(빗금)
|
||||
if surface:
|
||||
out.append(f"<polygon points='{area(surface, bottom)}' fill='{STRATA_FILL['soil']}'/>")
|
||||
if ground:
|
||||
out.append(
|
||||
f"<polygon points='{area(surface, bottom)}' fill='{STRATA_FILL['fill']}' "
|
||||
"clip-path='url(#above)'/>"
|
||||
)
|
||||
+ "' fill='#F3E9D8' stroke='none'/>"
|
||||
)
|
||||
if rock:
|
||||
rb = screen(rock)
|
||||
out.append(
|
||||
"<polygon points='"
|
||||
+ " ".join(
|
||||
f"{a:.1f},{b:.1f}" for a, b in [*rb, (rb[-1][0], sz(z_lo)), (rb[0][0], sz(z_lo))]
|
||||
)
|
||||
+ "' fill='#D9D2C5' stroke='none'/>"
|
||||
)
|
||||
out.append(_poly(rb, stroke="#795548", stroke_width="1.2", stroke_dasharray="6 3"))
|
||||
out.append(_poly(screen(ground), stroke="#8D6E63", stroke_width="1.4"))
|
||||
out.append(_poly(screen(surface), stroke=INK, stroke_width="2"))
|
||||
clip = " clip-path='url(#below)'" if surface else ""
|
||||
out.append(f"<polygon points='{area(rock, bottom)}' fill='{STRATA_FILL['rock']}'{clip}/>")
|
||||
out.append(_poly(screen(rock), stroke="#6D4C41", stroke_width="1", stroke_dasharray="6 3"))
|
||||
out.append(_poly(screen(ground), stroke="#6D4C41", stroke_width="1.3", stroke_dasharray="3 2"))
|
||||
out.append(_poly(screen(surface), stroke=INK, stroke_width="2.2"))
|
||||
if wet and surface: # 우기 지하수위 = 공사 뒤 지표(KDS 11 70 05 표 4.3-3) — ▽ 표시
|
||||
for frac in (0.2, 0.8):
|
||||
px, pz = surface[int(frac * (len(surface) - 1))]
|
||||
a, b = sx(px), sz(pz) - 4
|
||||
out.append(
|
||||
f"<polygon points='{a - 5:.1f},{b - 8:.1f} {a + 5:.1f},{b - 8:.1f} "
|
||||
f"{a:.1f},{b:.1f}' "
|
||||
"fill='none' stroke='#1565C0' stroke-width='1.2'/>"
|
||||
f"<polygon points='{a - 6:.1f},{b - 9:.1f} {a + 6:.1f},{b - 9:.1f} "
|
||||
f"{a:.1f},{b:.1f}' fill='none' stroke='#1565C0' stroke-width='1.4'/>"
|
||||
)
|
||||
# 임계 활동면
|
||||
for slip in slips:
|
||||
color = CONDITION_COLORS.get(slip["condition"], NG)
|
||||
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))
|
||||
out.append(_poly(screen(slip["points"]), stroke=color, stroke_width="2.6", **extra))
|
||||
# 치수 — 노폭 · 비탈 높이
|
||||
if road:
|
||||
r_off, l_off = road
|
||||
@@ -168,9 +215,9 @@ def section_svg(
|
||||
a, b = sx(l_off), sx(r_off)
|
||||
out.append(
|
||||
f"<line x1='{a:.1f}' y1='{y:.1f}' x2='{b:.1f}' y2='{y:.1f}' stroke='{INK}' "
|
||||
"stroke-width='0.8' marker-start='url(#t)' marker-end='url(#t)'/>"
|
||||
"stroke-width='0.9' marker-start='url(#t)' marker-end='url(#t)'/>"
|
||||
)
|
||||
out.append(_text((a + b) / 2, y - 4, f"노폭 {abs(l_off - r_off):.2f} m", 10, fill=INK))
|
||||
out.append(_text((a + b) / 2, y - 4, f"노폭 {abs(l_off - r_off):.2f} m", 11, fill=INK))
|
||||
for dim in heights:
|
||||
if abs(dim["z1"] - dim["z0"]) < 0.05:
|
||||
continue
|
||||
@@ -178,55 +225,92 @@ def section_svg(
|
||||
y0, y1 = sz(dim["z0"]), sz(dim["z1"])
|
||||
out.append(
|
||||
f"<line x1='{a:.1f}' y1='{y0:.1f}' x2='{a:.1f}' y2='{y1:.1f}' stroke='{INK}' "
|
||||
"stroke-width='0.8' marker-start='url(#t)' marker-end='url(#t)'/>"
|
||||
"stroke-width='0.9' marker-start='url(#t)' marker-end='url(#t)'/>"
|
||||
)
|
||||
left = a < SECTION_W / 2 # 화면 왼쪽 치수 = 글은 더 왼쪽(비탈과 안 겹침)
|
||||
label = f"H {abs(dim['z1'] - dim['z0']):.2f} m"
|
||||
out.append(
|
||||
_text(
|
||||
a - 4 if left else a + 4,
|
||||
(y0 + y1) / 2,
|
||||
label,
|
||||
10,
|
||||
"end" if left else "start",
|
||||
fill=INK,
|
||||
)
|
||||
)
|
||||
out.insert(
|
||||
2,
|
||||
"<defs><marker id='t' viewBox='0 0 4 10' refX='2' refY='5' markerWidth='4' "
|
||||
"markerHeight='10' orient='auto'><path d='M2 0 L2 10' stroke='#222' stroke-width='1'/>"
|
||||
"</marker></defs>",
|
||||
)
|
||||
# 범례
|
||||
legend = [("#8D6E63", "원지반", ""), (INK, "공사 뒤 지표", "")]
|
||||
label = dim.get("label") or f"H {abs(dim['z1'] - dim['z0']):.2f} m"
|
||||
# 화면 왼쪽 치수 = 글은 더 왼쪽(비탈과 안 겹침) · 그림 밖으로 나가면 반대쪽
|
||||
left = a < SECTION_W / 2
|
||||
if not left and a + 4 + _text_w(label, 11) > SECTION_W - 2:
|
||||
left = True
|
||||
if left and a - 4 - _text_w(label, 11) < 2:
|
||||
left = False
|
||||
anchor = "end" if left else "start"
|
||||
out.append(_text(a - 4 if left else a + 4, (y0 + y1) / 2, label, 11, anchor, fill=INK))
|
||||
if fs_box:
|
||||
out.append(_fs_box(fs_box, surface, sx, sz, pad_l, pad_t, SECTION_W - pad_r))
|
||||
# 범례 — 선 · 표시 · 지층 무늬(폭을 넘으면 다음 줄)
|
||||
items: list[tuple[str, str]] = [("#6D4C41|3 2", "원지반"), (f"{INK}|", "공사 뒤 지표")]
|
||||
if rock:
|
||||
legend.append(("#795548", "암 경계", "6 3"))
|
||||
items.append(("#6D4C41|6 3", "지층 경계"))
|
||||
for slip in slips:
|
||||
name = CONDITION_NAMES.get(slip["condition"], slip["condition"])
|
||||
f = slip.get("F")
|
||||
legend.append(
|
||||
(
|
||||
CONDITION_COLORS.get(slip["condition"], NG),
|
||||
f"{name} 임계면 Fs {f:.2f}" if f else name,
|
||||
SLIP_DASH.get(slip["condition"], ""),
|
||||
)
|
||||
)
|
||||
color = CONDITION_COLORS.get(slip["condition"], NG)
|
||||
label = f"{name} 임계면 Fs {f:.2f}" if f else name
|
||||
items.append((f"{color}|{SLIP_DASH.get(slip['condition'], '')}", label))
|
||||
if wet:
|
||||
legend.append(("#1565C0", "▽ 우기 지하수위 = 지표", ""))
|
||||
lx, ly = pad_l, SECTION_H - 22
|
||||
for color, label, dash in legend:
|
||||
dash_attr = f" stroke-dasharray='{dash}'" if dash else ""
|
||||
out.append(
|
||||
f"<line x1='{lx}' y1='{ly}' x2='{lx + 18}' y2='{ly}' stroke='{color}' "
|
||||
f"stroke-width='2'{dash_attr}/>"
|
||||
)
|
||||
out.append(_text(lx + 22, ly + 4, label, 10, "start", fill=INK))
|
||||
lx += 34 + sum(10 if ord(ch) > 0x2000 else 6 for ch in label) # 한글 · 기호 폭
|
||||
items.append(("▽", "우기 지하수위 = 지표"))
|
||||
for layer in strata or []:
|
||||
items.append((f"@{layer['kind']}", layer["text"]))
|
||||
ly = SECTION_H - 46
|
||||
for row in _legend_rows(items, SECTION_W - pad_l - pad_r, 11):
|
||||
lx = float(pad_l)
|
||||
for i in row:
|
||||
kind, label = items[i]
|
||||
if kind.startswith("@"):
|
||||
out.append(
|
||||
f"<rect x='{lx:.1f}' y='{ly - 9:.1f}' width='22' height='12' "
|
||||
f"fill='{STRATA_FILL[kind[1:]]}' stroke='#6D4C41' stroke-width='0.8'/>"
|
||||
)
|
||||
elif kind == "▽":
|
||||
out.append(
|
||||
f"<polygon points='{lx + 5:.1f},{ly - 9:.1f} {lx + 17:.1f},{ly - 9:.1f} "
|
||||
f"{lx + 11:.1f},{ly + 1:.1f}' fill='none' stroke='#1565C0' stroke-width='1.4'/>"
|
||||
)
|
||||
else:
|
||||
color, dash = kind.split("|")
|
||||
dash_attr = f" stroke-dasharray='{dash}'" if dash else ""
|
||||
out.append(
|
||||
f"<line x1='{lx:.1f}' y1='{ly - 3:.1f}' x2='{lx + 22:.1f}' y2='{ly - 3:.1f}' "
|
||||
f"stroke='{color}' stroke-width='2.4'{dash_attr}/>"
|
||||
)
|
||||
out.append(_text(lx + 27, ly + 1, label, 11, "start", fill=INK))
|
||||
lx += 30 + _text_w(label, 11)
|
||||
ly += 17
|
||||
out.append("</svg>")
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _fs_box(rows, surface, sx, sz, left: float, top: float, right: float) -> str:
|
||||
"""안전율 상자 — 하늘 쪽(지표가 낮은 끝) 위 모서리 · 조건 · Fs · 기준 · 판정."""
|
||||
width, line = 236, 19
|
||||
height = 24 + line * len(rows)
|
||||
# surface = offset 오름차순 · 화면 왼끝 = 큰 offset = 끝 점
|
||||
low_left = bool(surface) and sz(surface[-1][1]) > sz(surface[0][1])
|
||||
x = left + 6 if low_left else right - width - 6
|
||||
y = top + 6
|
||||
out = [
|
||||
f"<rect x='{x:.1f}' y='{y:.1f}' width='{width}' height='{height}' fill='#ffffff' "
|
||||
f"stroke='{INK}' stroke-width='1'/>",
|
||||
f"<line x1='{x:.1f}' y1='{y + 22:.1f}' x2='{x + width:.1f}' y2='{y + 22:.1f}' "
|
||||
f"stroke='{INK}' stroke-width='0.6'/>",
|
||||
]
|
||||
for cx, head, anchor in ((8, "조건", "start"), (78, "Fs", "end"), (132, "기준", "end")):
|
||||
out.append(_text(x + cx, y + 16, head, 11, anchor, font_weight="bold", fill=INK))
|
||||
out.append(_text(x + 196, y + 16, "판정", 11, font_weight="bold", fill=INK))
|
||||
for k, r in enumerate(rows):
|
||||
yy = y + 22 + line * (k + 1) - 5
|
||||
name = CONDITION_NAMES.get(r["condition"], r["condition"])
|
||||
f = "-" if r.get("F") is None else f"{r['F']:.2f}"
|
||||
verdict = "-" if r.get("ok") is None else "O.K" if r["ok"] else "N.G"
|
||||
color = INK if r.get("ok") is None else OK if r["ok"] else NG
|
||||
out.append(_text(x + 8, yy, name, 11, "start", fill=INK))
|
||||
out.append(_text(x + 78, yy, f, 11, "end", font_weight="bold", fill=INK))
|
||||
out.append(_text(x + 132, yy, f"≥ {r['required']:.2f}", 11, "end", fill=INK))
|
||||
out.append(_text(x + 196, yy, verdict, 11, font_weight="bold", fill=color))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def band_svg(
|
||||
*,
|
||||
title: str,
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"""E05 보고서 인쇄 — 목차 · 쪽 번호 · 머리말/꼬리말 · 긴 표 쪽 나눔(요구 347-5 · 351 · PLAN 534-5).
|
||||
|
||||
fill_toc(sheet, book, pages) 목차 줄(장 · 내용 · 쪽) — 쪽 없으면 「-」
|
||||
number_toc(book) 시트별 쪽 수를 재서 목차 쪽 번호를 채움(브라우저 한 번)
|
||||
report_pdf(book, figures) 표지(쪽 번호 없음) + 본문(머리말 사업명 · 꼬리말 n / N) 한 벌
|
||||
balance_breaks(sheet, book, first, last)
|
||||
긴 표 쪽 나눔 — 쪽마다 고르게(마지막 쪽에 한두 줄만 남지 않게)
|
||||
|
||||
쪽 번호 = 표지를 뺀 본문 차례(목차 = 1 쪽). 쪽 수 = 시트 하나씩 인쇄 HTML → PDF 쪽 수
|
||||
(시트 경계 = 늘 새 쪽 · `spreadsheet_print_html.ts`). 쪽 재기 · PDF = 공용 PDF 엔진(헤드리스).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import math
|
||||
from html import escape
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_print_pdf import merge_pdfs, render_htmls_to_pdfs
|
||||
from common_util.common_util_spreadsheet import print_htmls
|
||||
from E05_StabReport.E05_StabReport_Cells import A1, _set
|
||||
|
||||
COVER, TOC = "s1", "s9"
|
||||
TOC_FIRST = 4 # 목차 줄 = 4 행부터(4 행 서식)
|
||||
TITLES = {
|
||||
"s2": "개요 및 판정 요약",
|
||||
"s3": "입력 조건 — 해석 방법 · 기준 안전율 · 지하수 · 하중 · 물성치",
|
||||
"s4": "측점별 안정성 판정 일람표",
|
||||
"s5": "대표 단면 해석 결과 — 단면도 · 임계 활동면 · 안전율",
|
||||
"s6": "3D 해석 요약",
|
||||
"s7": "광역 자연사면 위험 요약",
|
||||
"s8": "보강 검토 — 비탈 완화 · 소단 · 벽 외적 안정",
|
||||
"s10": "대표 단면 절편 계산표",
|
||||
}
|
||||
APPENDIX = {"s10"}
|
||||
REPORT_NAME = "비탈면 안정성 검토 보고서"
|
||||
FONT = "font-family:'Malgun Gothic','맑은 고딕',sans-serif"
|
||||
PAPER_MM = {"A4": (210, 297), "A3": (297, 420)}
|
||||
PX_MM = 96 / 25.4
|
||||
|
||||
|
||||
def _row(a1: str) -> int:
|
||||
return int(A1.match(a1.split(":")[0]).group(2))
|
||||
|
||||
|
||||
def _visible(book: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return [s for s in book["시트"] if not s.get("숨김")]
|
||||
|
||||
|
||||
def _start_pages(book: dict[str, Any], pages: dict[str, int]) -> dict[str, int]:
|
||||
"""시트 id → 본문 시작 쪽(표지 뺌 · 첫 본문 시트 = 1)."""
|
||||
out, at = {}, 1
|
||||
for s in _visible(book):
|
||||
if s["id"] == COVER:
|
||||
continue
|
||||
out[s["id"]] = at
|
||||
at += pages.get(s["id"], 0)
|
||||
return out
|
||||
|
||||
|
||||
def fill_toc(sheet: dict[str, Any], book: dict[str, Any], pages: dict[str, int] | None) -> None:
|
||||
"""목차 — 보이는 본문 시트(표지 · 목차 뺌)마다 한 줄 · 장 번호 · 부록 · 시작 쪽."""
|
||||
tpl = {c: dict(sheet["칸"].get(f"{c}{TOC_FIRST}") or {}) for c in "ABC"}
|
||||
for a1 in [a for a in sheet["칸"] if _row(a) >= TOC_FIRST]:
|
||||
cell = sheet["칸"][a1]
|
||||
if _row(a1) == TOC_FIRST: # 양식 줄 = 서식만 남김
|
||||
cell.pop("값", None)
|
||||
else:
|
||||
del sheet["칸"][a1]
|
||||
height = (sheet.get("행") or {}).get(str(TOC_FIRST))
|
||||
start = _start_pages(book, pages) if pages else {}
|
||||
chapter = 0
|
||||
for i, s in enumerate(x for x in _visible(book) if x["id"] not in (COVER, TOC)):
|
||||
r = TOC_FIRST + i
|
||||
for c, cell in tpl.items():
|
||||
sheet["칸"][f"{c}{r}"] = {k: v for k, v in cell.items() if k != "값"}
|
||||
if height:
|
||||
sheet["행"][str(r)] = dict(height)
|
||||
if s["id"] in APPENDIX:
|
||||
mark = "부록"
|
||||
else:
|
||||
chapter += 1
|
||||
mark = f"{chapter}."
|
||||
_set(sheet, f"A{r}", mark)
|
||||
_set(sheet, f"B{r}", TITLES.get(s["id"], s["이름"]))
|
||||
_set(sheet, f"C{r}", start.get(s["id"], "-"))
|
||||
|
||||
|
||||
def _only(book: dict[str, Any], keep: set[str], hide: bool = False) -> dict[str, Any]:
|
||||
"""`keep` 시트만 보이는(hide = 참이면 `keep` 만 숨긴) 얕은 사본 · 브라우저 계산값 뺌."""
|
||||
sheets = [{**s, "숨김": True} if (s["id"] in keep) == hide else s for s in book["시트"]]
|
||||
return {**{k: v for k, v in book.items() if k != "계산값"}, "시트": sheets}
|
||||
|
||||
|
||||
def _figures(book: dict[str, Any], ids: set[str], figures: dict[str, bytes]) -> dict[str, bytes]:
|
||||
keys = {f["열쇠"] for s in book["시트"] if s["id"] in ids for f in s.get("그림") or []}
|
||||
return {k: v for k, v in figures.items() if k in keys}
|
||||
|
||||
|
||||
def sheet_pages(book: dict[str, Any]) -> dict[str, int]:
|
||||
"""보이는 시트마다 인쇄 쪽 수(그림 없이 — 그림 칸 크기는 행열이 정함)."""
|
||||
from pypdf import PdfReader
|
||||
|
||||
ids = [s["id"] for s in _visible(book)]
|
||||
htmls = print_htmls([{"book": _only(book, {sid})} for sid in ids])
|
||||
pdfs = render_htmls_to_pdfs([{"html": h, "page_size": None} for h in htmls])
|
||||
return {sid: len(PdfReader(io.BytesIO(pdf)).pages) for sid, pdf in zip(ids, pdfs)}
|
||||
|
||||
|
||||
def number_toc(book: dict[str, Any]) -> dict[str, int]:
|
||||
"""목차 시트가 있으면 쪽 수를 재서 쪽 번호를 채움(책을 고침) · 시트별 쪽 수."""
|
||||
pages = sheet_pages(book)
|
||||
toc = next((s for s in book["시트"] if s["id"] == TOC and not s.get("숨김")), None)
|
||||
if toc is not None:
|
||||
fill_toc(toc, book, pages)
|
||||
return pages
|
||||
|
||||
|
||||
def _header(project: str) -> str:
|
||||
return (
|
||||
f'<div style="width:100%;margin:0 12mm;font-size:8px;{FONT};color:#555;'
|
||||
"display:flex;justify-content:space-between;border-bottom:0.5px solid #999;"
|
||||
f'padding-bottom:2px"><span>{escape(project)}</span><span>{REPORT_NAME}</span></div>'
|
||||
)
|
||||
|
||||
|
||||
FOOTER = (
|
||||
f'<div style="width:100%;text-align:center;font-size:8px;{FONT};color:#555">'
|
||||
'- <span class="pageNumber"></span> / <span class="totalPages"></span> -</div>'
|
||||
)
|
||||
|
||||
|
||||
def report_pdf(book: dict[str, Any], figures: dict[str, bytes]) -> bytes:
|
||||
"""보고서 PDF — 목차 쪽 번호 = 실제 쪽 · 표지 = 쪽 번호 · 머리말 없음."""
|
||||
clean = _only(book, set(), hide=True)
|
||||
clean["시트"] = [
|
||||
{**s, "칸": {k: dict(v) for k, v in s["칸"].items()}, "행": dict(s.get("행") or {})}
|
||||
if s["id"] == TOC
|
||||
else s
|
||||
for s in clean["시트"]
|
||||
] # 목차만 고칠 사본
|
||||
number_toc(clean)
|
||||
ids = {s["id"] for s in _visible(clean)}
|
||||
cover = next((s for s in clean["시트"] if s["id"] == COVER), None)
|
||||
project = str(((cover or {}).get("칸", {}).get("C8") or {}).get("값") or "")
|
||||
asks, jobs = [], []
|
||||
if COVER in ids:
|
||||
asks.append({"book": _only(clean, {COVER}), "figures": _figures(clean, {COVER}, figures)})
|
||||
jobs.append({"page_size": None})
|
||||
if ids - {COVER}:
|
||||
asks.append({"book": _only(clean, {COVER}, hide=True), "figures": figures})
|
||||
jobs.append(
|
||||
{"page_size": None, "header_template": _header(project), "footer_template": FOOTER}
|
||||
)
|
||||
htmls = print_htmls(asks)
|
||||
pdfs = render_htmls_to_pdfs([{**job, "html": h} for job, h in zip(jobs, htmls)])
|
||||
return merge_pdfs(pdfs) if len(pdfs) > 1 else pdfs[0]
|
||||
|
||||
|
||||
# ── 긴 표 쪽 나눔 ────────────────────────────────────────────────────────────
|
||||
def _col_px(chars: float) -> int:
|
||||
"""`spreadsheet_headers.colCharsToPx` 와 같은 꼴."""
|
||||
return round(min(chars, 255) * 7 + 5)
|
||||
|
||||
|
||||
def _page_fit(sheet: dict[str, Any], book: dict[str, Any]) -> tuple[float, Any]:
|
||||
"""(쪽 하나 높이 px · 배율 반영) · 행 높이 px 함수 — 인쇄 HTML 과 같은 셈."""
|
||||
pr = sheet.get("인쇄") or {}
|
||||
w, h = PAPER_MM[pr.get("용지") or "A4"]
|
||||
if pr.get("방향") == "landscape":
|
||||
w, h = h, w
|
||||
m = {"위": 10, "아래": 10, "왼": 10, "오른": 10, **(pr.get("여백") or {})}
|
||||
last_col = max(
|
||||
(sum((ord(ch) - 64) * 26**i for i, ch in enumerate(reversed(A1.match(a).group(1))))
|
||||
for a in sheet["칸"]),
|
||||
default=1,
|
||||
) # fmt: skip
|
||||
cols = [_col_name(c) for c in range(1, last_col + 1)]
|
||||
default_w = (
|
||||
(sheet.get("기본") or {}).get("열폭") or (book.get("기본") or {}).get("열폭") or 8.43
|
||||
)
|
||||
table = sum(_col_px((sheet.get("열") or {}).get(c, {}).get("폭") or default_w) for c in cols)
|
||||
if (pr.get("맞춤") or {}).get("폭"):
|
||||
zoom = min(1.0, (w - m["왼"] - m["오른"]) * PX_MM / max(table, 1))
|
||||
else:
|
||||
zoom = (pr.get("배율") or 100) / 100
|
||||
default_pt = (book.get("기본") or {}).get("행높이") or 15
|
||||
|
||||
def row_px(r: int) -> float:
|
||||
return ((sheet.get("행") or {}).get(str(r), {}).get("높이") or default_pt) * 4 / 3
|
||||
|
||||
return (h - m["위"] - m["아래"]) * PX_MM / zoom, row_px
|
||||
|
||||
|
||||
def _col_name(n: int) -> str:
|
||||
out = ""
|
||||
while n:
|
||||
n, k = divmod(n - 1, 26)
|
||||
out = chr(65 + k) + out
|
||||
return out
|
||||
|
||||
|
||||
def balance_breaks(sheet: dict[str, Any], book: dict[str, Any], first: int, last: int) -> None:
|
||||
"""`first`~`last` 행 표를 쪽마다 고르게 나눔(쪽나눔 = 덩이 끝 행) — 한 쪽이면 그대로.
|
||||
|
||||
ponytail: 한 줄 글 행만 맞음(줄바꿈으로 키 큰 행은 셈 밖) · 어긋남 대비 한 줄 덜 담음.
|
||||
"""
|
||||
if last < first:
|
||||
return
|
||||
page, row_px = _page_fit(sheet, book)
|
||||
head = (sheet.get("인쇄") or {}).get("머리행")
|
||||
head_px = sum(row_px(r) for r in range(head[0], head[1] + 1)) if head else 0.0
|
||||
pre_px = sum(row_px(r) for r in range(1, head[0] if head else first))
|
||||
rows = list(range(first, last + 1))
|
||||
cap_first = max(1, math.floor((page - pre_px - head_px) / row_px(first)) - 1)
|
||||
cap_next = max(1, math.floor((page - head_px) / row_px(first)) - 1)
|
||||
caps = [cap_first]
|
||||
while sum(caps) < len(rows): # 쪽 수
|
||||
caps.append(cap_next)
|
||||
if len(caps) == 1:
|
||||
return
|
||||
breaks, at, left = [], 0, len(rows)
|
||||
for i, cap in enumerate(caps[:-1]):
|
||||
take = min(cap, math.ceil(left / (len(caps) - i)))
|
||||
at += take
|
||||
left -= take
|
||||
breaks.append(rows[at - 1])
|
||||
sheet["인쇄"]["쪽나눔"] = breaks
|
||||
@@ -29,11 +29,12 @@ from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_print_pdf import PrintError
|
||||
from common_util.common_util_spreadsheet import RecalcError, with_server_values, workbook_pdf
|
||||
from common_util.common_util_spreadsheet import RecalcError, with_server_values
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_workbook_xlsx import workbook_to_xlsx
|
||||
from config.config_db import get_db_pool
|
||||
from E05_StabReport import E05_StabReport_Build as build
|
||||
from E05_StabReport import E05_StabReport_Print as printer
|
||||
from E05_StabReport import E05_StabReport_Store as store
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["E05 Stab Report"])
|
||||
@@ -192,9 +193,7 @@ def _export(root: Path, book: dict[str, Any] | None, kind: str) -> bytes:
|
||||
figures = store.figures_of(root, book)
|
||||
if kind == "xlsx":
|
||||
return workbook_to_xlsx(with_server_values(book), figures)
|
||||
clean = dict(book)
|
||||
clean.pop("계산값", None) # 인쇄 HTML 이 다시 풂(브라우저 값 안 씀)
|
||||
return workbook_pdf(clean, figures)
|
||||
return printer.report_pdf(book, figures) # 인쇄 HTML 이 다시 풂(브라우저 값 안 씀)
|
||||
|
||||
|
||||
async def _export_response(project_id: UUID, book: dict[str, Any] | None, kind: str) -> Response:
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
"""E05 보고서 s5 대표단면 · s10 부록 절편 — 단면 하나 = 한 쪽(PLAN 534-3 · 534-5).
|
||||
|
||||
칸 자리 = `resources/master_template/report/stab_report_slots.md` s5 · s10.
|
||||
그림 = `E05_StabReport_Figure.section_svg`(값 셈 없음 · 해석 저장본 옮겨 그림).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from E03_StabSection import E03_StabSection_View as view2d
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import final_surface
|
||||
from E05_StabReport import E05_StabReport_Figure as figure
|
||||
from E05_StabReport.E05_StabReport_Cells import (
|
||||
A1,
|
||||
CASES,
|
||||
GROUND_NAMES,
|
||||
METHOD_NAMES,
|
||||
REASONS,
|
||||
SIDE_NAMES,
|
||||
_criterion,
|
||||
_label_at,
|
||||
_labels,
|
||||
_seismic,
|
||||
_set,
|
||||
_shift,
|
||||
_verdict,
|
||||
fig_key,
|
||||
)
|
||||
|
||||
BLOCK = 28 # 대표단면 한 쪽 = 28 행
|
||||
LOWEST_N = view2d.REPRESENTATIVE_LOWEST
|
||||
CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
|
||||
CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"}
|
||||
SLICE_COLS = "ABCDEFGHIJKLM"
|
||||
SLICE_TPL = 5 # 부록 절편 덩이 꼴 = 양식 1~5 행(제목 · 단위 · 머리 · 줄 · 합계)
|
||||
|
||||
|
||||
def _case_slip(result: dict[str, Any] | None, condition: str, side: str, method: str):
|
||||
"""조건 하나 — 그 비탈 역할(깎기 · 쌓기) 중 그 방법 최소 면 · 높이."""
|
||||
best = None
|
||||
for case in (result or {}).get("cases") or []:
|
||||
if case.get("condition") != condition:
|
||||
continue
|
||||
if side in ("cut", "fill") and case.get("role") not in (side, None):
|
||||
continue
|
||||
methods = (case.get("result") or {}).get("methods") or {}
|
||||
found = (methods.get(method) or {}).get("min") or next(
|
||||
(v["min"] for v in methods.values() if v.get("min")), None
|
||||
)
|
||||
if found and found.get("F") is not None and (best is None or found["F"] < best[0]["F"]):
|
||||
best = (found, case)
|
||||
return best
|
||||
|
||||
|
||||
def _block_copy(
|
||||
sheet: dict[str, Any], template: dict[str, Any], dr: int, rows: int, tag: str
|
||||
) -> None:
|
||||
"""양식 1~rows 행 덩이를 dr 행 아래로(칸 · 병합 · 행 높이 · 조건부서식)."""
|
||||
for a1, cell in template["칸"].items():
|
||||
if int(A1.match(a1).group(2)) <= rows:
|
||||
sheet["칸"][_shift(a1, dr)] = copy.deepcopy(cell)
|
||||
for m in template.get("병합", []):
|
||||
if int(A1.match(m.split(":")[0]).group(2)) <= rows:
|
||||
sheet["병합"].append(":".join(_shift(p, dr) for p in m.split(":")))
|
||||
for r, info in (template.get("행") or {}).items():
|
||||
if int(r) <= rows:
|
||||
sheet["행"][str(int(r) + dr)] = dict(info)
|
||||
for rule in template.get("조건부서식", []):
|
||||
moved = copy.deepcopy(rule)
|
||||
moved["id"] = f"{rule['id']}_{tag}"
|
||||
moved["범위"] = [{**g, "r0": g["r0"] + dr, "r1": g["r1"] + dr} for g in rule["범위"]]
|
||||
sheet["조건부서식"].append(moved)
|
||||
|
||||
|
||||
def _clear(sheet: dict[str, Any]) -> dict[str, Any]:
|
||||
"""시트를 비우고 양식 사본을 돌려줌."""
|
||||
template = copy.deepcopy(sheet)
|
||||
sheet["칸"] = {}
|
||||
sheet["병합"], sheet["행"], sheet["조건부서식"] = [], {}, []
|
||||
if "그림" in sheet:
|
||||
sheet["그림"] = []
|
||||
return template
|
||||
|
||||
|
||||
def _mark(k: int) -> str:
|
||||
return CIRCLED[k] if k < len(CIRCLED) else str(k + 1)
|
||||
|
||||
|
||||
def _picks(rep: dict[str, Any], ground: dict[str, Any]) -> dict[str, Any]:
|
||||
"""대표 단면 하나 — 조건별 (면, 경우) · 쓰는 조건 · 지배 조건(Fs / 기준 가장 낮음)."""
|
||||
st = rep["summary"]
|
||||
side, method = st.get("side"), st.get("method") or "bishop"
|
||||
used = [c for c in CASES if c != "seismic" or _seismic(ground)]
|
||||
picks = {c: _case_slip(rep.get("result"), c, side, method) for c in used}
|
||||
fs = st.get("fs_by_condition") or {}
|
||||
ratios = [
|
||||
(float(fs[c]) / _criterion(ground, c, side), c)
|
||||
for c in used
|
||||
if fs.get(c) is not None and picks.get(c)
|
||||
]
|
||||
governing = min(ratios)[1] if ratios else None
|
||||
return {"side": side, "method": method, "used": used, "picks": picks, "governing": governing}
|
||||
|
||||
|
||||
def fill_sections(sheet: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[str, str]]:
|
||||
"""s5 대표단면 — 단면 하나 = 28 행 덩이 = 한 쪽 · 그림 일감 [(열쇠, SVG)]."""
|
||||
reps = inputs["representatives"]
|
||||
ground = inputs["ground"]
|
||||
labels = _labels(inputs["stations"])
|
||||
template = _clear(sheet)
|
||||
if not reps:
|
||||
_block_copy(sheet, template, 0, BLOCK, "0")
|
||||
_set(sheet, "A1", "대표 단면 — 해당 없음")
|
||||
sheet["인쇄"]["쪽나눔"] = []
|
||||
return []
|
||||
jobs: list[tuple[str, str]] = []
|
||||
for k, rep in enumerate(reps):
|
||||
dr = BLOCK * k
|
||||
_block_copy(sheet, template, dr, BLOCK, str(k))
|
||||
st = rep["summary"]
|
||||
got = _picks(rep, ground)
|
||||
side, method, picks = got["side"], got["method"], got["picks"]
|
||||
label = _label_at(labels, float(st["chainage_m"]))
|
||||
dry = picks.get("dry")
|
||||
height = float(dry[1].get("height_m") or 0) if dry else 0.0
|
||||
_set(
|
||||
sheet,
|
||||
f"A{1 + dr}",
|
||||
f"대표 단면 {_mark(k)} — {label} · {SIDE_NAMES.get(side, side or '-')} · "
|
||||
f"H {height:.1f} m",
|
||||
)
|
||||
reason = REASONS.get(rep.get("reason"), rep.get("reason") or "")
|
||||
if rep.get("reason") == "lowest":
|
||||
reason = f"{reason} {LOWEST_N}"
|
||||
_set(sheet, f"B{2 + dr}", reason)
|
||||
key = fig_key(rep["station_key"])
|
||||
sheet["그림"].append(
|
||||
{"열쇠": key, "닻": f"A{3 + dr}:H{22 + dr}", "이름": f"대표 단면 {label}"}
|
||||
)
|
||||
fs = st.get("fs_by_condition") or {}
|
||||
slips, box = [], []
|
||||
for r, case in zip((24, 25, 26), CASES):
|
||||
row = r + dr
|
||||
if case not in got["used"]:
|
||||
_set(sheet, f"B{row}", "검토 안 함")
|
||||
continue
|
||||
value = fs.get(case)
|
||||
crit = _criterion(ground, case, side)
|
||||
ok = None if value is None else float(value) >= crit - 1e-9
|
||||
pick = picks.get(case)
|
||||
_set(sheet, f"B{row}", METHOD_NAMES.get(method, method))
|
||||
_set(sheet, f"C{row}", float(value) if value is not None else None)
|
||||
_set(sheet, f"D{row}", crit)
|
||||
_verdict(sheet, f"E{row}", ok)
|
||||
slip = pick[0] if pick else None
|
||||
center = (slip or {}).get("center")
|
||||
circle = bool(slip and slip.get("radius") and center)
|
||||
_set(sheet, f"F{row}", float(slip["radius"]) if circle else "-" if slip else None)
|
||||
_set(sheet, f"G{row}", float(center[0]) if circle else "-" if slip else None)
|
||||
_set(sheet, f"H{row}", float(center[1]) if circle else "-" if slip else None)
|
||||
if slip and slip.get("points"):
|
||||
slips.append({"condition": case, "F": slip.get("F"), "points": slip["points"]})
|
||||
box.append({"condition": case, "F": value, "required": crit, "ok": ok})
|
||||
design = (rep.get("cross") or {}).get("design") or {}
|
||||
kind = (dry[0].get("kind") if dry else None) or ""
|
||||
layers = rep.get("layers") or {}
|
||||
note = [
|
||||
{"circle": "활동면 원호", "polyline": "활동면 비원호"}.get(kind, ""),
|
||||
f"지반 {GROUND_NAMES.get(design.get('ground_type'), design.get('ground_type') or '-')}",
|
||||
f"토층 두께 {layers['soil_thickness_m']:g} m" if layers.get("soil_thickness_m") else "",
|
||||
f"부록 {_mark(k)} 절편 계산표" if got["governing"] else "",
|
||||
]
|
||||
_set(sheet, f"B{27 + dr}", " · ".join(n for n in note if n))
|
||||
jobs.append((key, _section_figure(rep, slips, box, ground)))
|
||||
sheet["인쇄"]["쪽나눔"] = [BLOCK * (k + 1) for k in range(len(reps))]
|
||||
return jobs
|
||||
|
||||
|
||||
def _strata(rep: dict[str, Any], ground: dict[str, Any], fill: bool) -> list[dict[str, str]]:
|
||||
"""지층 범례 — 성토(공사 뒤 지표가 원지반 위인 곳이 있으면) · 위 지층 · 아래 지층."""
|
||||
names = {m["id"]: m for m in ground.get("materials") or []}
|
||||
layers = rep.get("layers") or {}
|
||||
out = []
|
||||
picks = [
|
||||
("fill", (ground.get("label_map") or {}).get("fill") or "fill", fill),
|
||||
("soil", layers.get("upper"), True),
|
||||
("rock", layers.get("lower"), bool(layers.get("soil_thickness_m"))),
|
||||
]
|
||||
for kind, mid, show in picks:
|
||||
m = names.get(mid)
|
||||
if show and m:
|
||||
text = f"{m['name']} γt {m['gamma_t']:g} · c {m['c']:g} · φ {m['phi']:g}"
|
||||
out.append({"kind": kind, "text": text})
|
||||
return out
|
||||
|
||||
|
||||
def _section_figure(
|
||||
rep: dict[str, Any], slips: list[dict[str, Any]], box: list[dict[str, Any]], soil: dict
|
||||
) -> str:
|
||||
cross = rep.get("cross") or {}
|
||||
design = cross.get("design") or {}
|
||||
ground = sorted( # 해석에 쓴 넓힌 지반선(임계면이 횡단 표본 밖까지 감) · 없으면 횡단 표본
|
||||
(float(x), float(z)) for x, z in rep.get("ground") or []
|
||||
) or sorted(
|
||||
(float(s["offset_m"]), float(s["z"]))
|
||||
for s in cross.get("samples") or []
|
||||
if s.get("valid", True) and s.get("z") is not None
|
||||
)
|
||||
line = sorted(
|
||||
(float(p["offset_m"]), float(p["elevation_m"])) for p in design.get("design_line") or []
|
||||
)
|
||||
surface = [tuple(p) for p in final_surface(ground, line)] if ground and line else ground
|
||||
thickness = (rep.get("layers") or {}).get("soil_thickness_m")
|
||||
rock = [(x, z - float(thickness)) for x, z in ground] if thickness else None
|
||||
edges = design.get("road_edges") or {}
|
||||
road = None
|
||||
if edges.get("left") and edges.get("right"):
|
||||
road = (float(edges["right"]["offset_m"]), float(edges["left"]["offset_m"]))
|
||||
heights = []
|
||||
if road and line:
|
||||
for case in (rep.get("result") or {}).get("cases") or []:
|
||||
if case.get("condition") != "dry" or not case.get("height_m"):
|
||||
continue
|
||||
left = case.get("side") == "left"
|
||||
edge = road[1] if left else road[0]
|
||||
day = line[-1][0] if left else line[0][0]
|
||||
z_edge, z1 = figure._interp(surface, edge), figure._interp(surface, day)
|
||||
if z_edge is not None and z1 is not None: # 높이 = 엔진 값(비탈 끝에서 길 쪽으로)
|
||||
h = float(case["height_m"])
|
||||
z0 = z1 + h if z_edge > z1 else z1 - h
|
||||
heights.append({"x": day, "z0": z0, "z1": z1, "label": f"H {h:.2f} m"})
|
||||
filled = any(
|
||||
(z_g := figure._interp(ground, x)) is not None and z - z_g > 0.05 for x, z in surface
|
||||
)
|
||||
return figure.section_svg(
|
||||
title="",
|
||||
ground=ground,
|
||||
surface=surface,
|
||||
rock=rock,
|
||||
slips=slips,
|
||||
road=road,
|
||||
heights=heights,
|
||||
wet=any(s["condition"] == "wet" for s in slips),
|
||||
strata=_strata(rep, soil, filled),
|
||||
fs_box=box,
|
||||
)
|
||||
|
||||
|
||||
# ── s10 부록 절편 ────────────────────────────────────────────────────────────
|
||||
def fill_slices(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||||
"""s10 부록 절편 — 대표 단면마다 지배 조건(Fs / 기준 가장 낮음) 임계면 절편표 한 쪽."""
|
||||
ground = inputs["ground"]
|
||||
labels = _labels(inputs["stations"])
|
||||
template = _clear(sheet)
|
||||
data_tpl = {c: dict(template["칸"].get(f"{c}4") or {}) for c in SLICE_COLS}
|
||||
sum_tpl = {c: dict(template["칸"].get(f"{c}5") or {}) for c in SLICE_COLS}
|
||||
data_h = (template.get("행") or {}).get("4")
|
||||
sum_h = (template.get("행") or {}).get("5")
|
||||
dr, breaks = 0, []
|
||||
for k, rep in enumerate(inputs["representatives"]):
|
||||
got = _picks(rep, ground)
|
||||
cond = got["governing"]
|
||||
slip = got["picks"][cond][0] if cond else None
|
||||
rows = (slip or {}).get("slices") or []
|
||||
if not rows:
|
||||
continue
|
||||
_block_copy(sheet, template, dr, 3, str(k))
|
||||
label = _label_at(labels, float(rep["summary"]["chainage_m"]))
|
||||
F = float(slip["F"])
|
||||
_set(
|
||||
sheet,
|
||||
f"A{1 + dr}",
|
||||
f"부록 {_mark(k)} 절편 계산표 — {label} · {SIDE_NAMES.get(got['side'], '-')} · "
|
||||
f"{CONDITION_NAMES[cond]} · {METHOD_NAMES.get(got['method'], got['method'])} · "
|
||||
f"Fs {F:.3f}",
|
||||
)
|
||||
kind = {"circle": "원호", "polyline": "비원호"}.get(slip.get("kind"), "")
|
||||
_set(
|
||||
sheet,
|
||||
f"A{2 + dr}",
|
||||
f"대표 단면 {_mark(k)} 의 {CONDITION_NAMES[cond]} 임계 활동면({kind}) · "
|
||||
f"{template['칸']['A2']['값']}",
|
||||
)
|
||||
sums = [0.0, 0.0, 0.0, 0.0]
|
||||
for i, s in enumerate(rows):
|
||||
r = 4 + dr + i
|
||||
for c, cell in data_tpl.items():
|
||||
sheet["칸"][f"{c}{r}"] = dict(cell)
|
||||
if data_h:
|
||||
sheet["행"][str(r)] = dict(data_h)
|
||||
drive = (s["weight"] + s["surcharge"]) * math.sin(math.radians(s["alpha_deg"]))
|
||||
resist = s["shear_mobilized"] * F
|
||||
values = (
|
||||
i + 1, s["width"], s["height"], s["alpha_deg"], s["weight"], s["surcharge"],
|
||||
s["pore_pressure"], s["base_length"], s["c"], s["phi_deg"], s["normal_eff"],
|
||||
drive, resist,
|
||||
) # fmt: skip
|
||||
for c, v in zip(SLICE_COLS, values):
|
||||
_set(sheet, f"{c}{r}", float(v) if c != "A" else v)
|
||||
for j, v in enumerate((s["weight"], s["surcharge"], drive, resist)):
|
||||
sums[j] += v
|
||||
r = 4 + dr + len(rows)
|
||||
for c, cell in sum_tpl.items():
|
||||
sheet["칸"][f"{c}{r}"] = dict(cell)
|
||||
if sum_h:
|
||||
sheet["행"][str(r)] = dict(sum_h)
|
||||
_set(sheet, f"A{r}", "합계")
|
||||
for c, v in zip("EFLM", sums):
|
||||
_set(sheet, f"{c}{r}", v)
|
||||
dr = r
|
||||
breaks.append(r)
|
||||
if not breaks:
|
||||
_block_copy(sheet, template, 0, 2, "0")
|
||||
_set(sheet, "A1", "부록 — 절편 계산표 없음(대표 단면 없음)")
|
||||
sheet["인쇄"]["쪽나눔"] = breaks
|
||||
@@ -19,8 +19,10 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_print_pdf import PrintError
|
||||
from common_util.common_util_spreadsheet import with_server_values
|
||||
from common_util.common_util_svg_png import svgs_to_pngs
|
||||
from E05_StabReport import E05_StabReport_Print as printer
|
||||
from E05_StabReport.E05_StabReport_Build import (
|
||||
META,
|
||||
PROGRAM_SHEETS,
|
||||
@@ -64,6 +66,15 @@ def _write(project_root: Path, book: dict[str, Any], meta: dict[str, Any]) -> No
|
||||
atomic_write_json(folder / META, meta)
|
||||
|
||||
|
||||
def _numbered(book: dict[str, Any]) -> dict[str, Any]:
|
||||
"""목차 쪽 번호를 실제 쪽 수로(브라우저 없으면 「-」 그대로 · PDF 내보내기 때 다시 셈)."""
|
||||
try:
|
||||
printer.number_toc(book)
|
||||
except PrintError:
|
||||
pass
|
||||
return book
|
||||
|
||||
|
||||
def _meta(book: dict[str, Any], inputs: dict[str, Any], figures: list[str]) -> dict[str, Any]:
|
||||
return {
|
||||
"fingerprint": inputs["fingerprint"],
|
||||
@@ -79,7 +90,7 @@ def create(project_root: Path) -> dict[str, Any]:
|
||||
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)
|
||||
book = _numbered(with_server_values(book))
|
||||
folder = stage_dir(project_root)
|
||||
figures = _write_figures(folder, jobs)
|
||||
meta = _meta(book, inputs, figures)
|
||||
@@ -94,6 +105,22 @@ def create(project_root: Path) -> dict[str, Any]:
|
||||
return meta
|
||||
|
||||
|
||||
def _merge_styles(have: list[dict], form_styles: list[dict], sheets: Any) -> list[dict]:
|
||||
"""새 판 시트의 서식 번호(양식 기준)를 작업본 서식 표 번호로 — 없는 서식은 끝에 붙임
|
||||
(옛 작업본 · 사용자가 더한 서식과 번호가 엇갈리지 않게)."""
|
||||
styles = list(have)
|
||||
remap: dict[int, int] = {}
|
||||
for i, style in enumerate(form_styles):
|
||||
if style not in styles:
|
||||
styles.append(style)
|
||||
remap[i] = styles.index(style)
|
||||
for sheet in sheets:
|
||||
for cell in sheet["칸"].values():
|
||||
if "서식" in cell:
|
||||
cell["서식"] = remap.get(cell["서식"], cell["서식"])
|
||||
return styles
|
||||
|
||||
|
||||
def refresh(project_root: Path) -> dict[str, Any]:
|
||||
"""[보고서 갱신] — 프로그램 몫 시트 · 그림만 다시. 사용자 몫 · 사용자가 더한 시트는 그대로.
|
||||
덮인 시트에 사용자 고침이 있었으면 `overwritten`(시트 이름) · 옛 판 `_지난/`."""
|
||||
@@ -104,6 +131,7 @@ def refresh(project_root: Path) -> dict[str, Any]:
|
||||
fresh = form()
|
||||
jobs = build_program(fresh, inputs)
|
||||
built = {s["id"]: s for s in fresh["시트"] if s["id"] in PROGRAM_SHEETS}
|
||||
book = {**book, "서식": _merge_styles(book.get("서식") or [{}], fresh["서식"], built.values())}
|
||||
signs = (meta or {}).get("signs") or {}
|
||||
overwritten = [
|
||||
s["이름"]
|
||||
@@ -123,10 +151,13 @@ def refresh(project_root: Path) -> dict[str, Any]:
|
||||
for s in book["시트"]
|
||||
if s["id"] in built or s["id"] not in PROGRAM_SHEETS
|
||||
]
|
||||
sheets += [
|
||||
built[sid] for sid in PROGRAM_SHEETS if sid in built and sid not in have
|
||||
] # 지운 시트는 끝에 다시
|
||||
book = with_server_values({**book, "시트": sheets})
|
||||
order = [s["id"] for s in fresh["시트"]]
|
||||
for sid in order: # 없는 프로그램 몫(지움 · 새로 생긴 목차 · 부록) = 양식 차례 자리에
|
||||
if sid in built and sid not in have:
|
||||
before = {x for x in order[: order.index(sid)]}
|
||||
at = max((i + 1 for i, x in enumerate(sheets) if x["id"] in before), default=0)
|
||||
sheets.insert(at, built[sid])
|
||||
book = _numbered(with_server_values({**book, "시트": sheets}))
|
||||
figures = _write_figures(folder, jobs)
|
||||
new_meta = _meta(book, inputs, figures)
|
||||
_write(project_root, book, new_meta)
|
||||
|
||||
@@ -58,6 +58,12 @@ def _pdf_options(job: dict[str, Any]) -> dict[str, Any]:
|
||||
options.update(
|
||||
display_header_footer=True, header_template="<span></span>", footer_template=FOOTER
|
||||
)
|
||||
if job.get("header_template") or job.get("footer_template"): # 머리말 · 꼬리말 HTML(E05)
|
||||
options.update(
|
||||
display_header_footer=True,
|
||||
header_template=job.get("header_template") or "<span></span>",
|
||||
footer_template=job.get("footer_template") or "<span></span>",
|
||||
)
|
||||
return options
|
||||
|
||||
|
||||
@@ -102,7 +108,9 @@ def render_htmls_to_pdfs(jobs: list[dict[str, Any]], timeout_s: float = TIMEOUT_
|
||||
"""HTML 여러 벌 → PDF 여러 벌(차례 그대로 · 자식 프로세스 한 번).
|
||||
|
||||
job = {html, page_size="A4", landscape=False, margins=None, page_numbers=False,
|
||||
prepare_js=None, prepare_arg=None} — prepare_js = PDF 직전 page.evaluate(줄임 등).
|
||||
prepare_js=None, prepare_arg=None, header_template=None, footer_template=None} —
|
||||
prepare_js = PDF 직전 page.evaluate(줄임 등) · header/footer_template = 쪽마다 머리말 · 꼬리말
|
||||
HTML(Playwright `pageNumber` · `totalPages` 칸 · 주면 page_numbers 대신).
|
||||
"""
|
||||
jobs = [{"page_size": "A4", **job} for job in jobs]
|
||||
with _lock, tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
@@ -293,6 +293,156 @@
|
||||
"선": "thin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 8,
|
||||
"굵게": true,
|
||||
"채움": "#DDEBF7",
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"줄바꿈": true,
|
||||
"테두리": {
|
||||
"위": {
|
||||
"선": "thin"
|
||||
},
|
||||
"아래": {
|
||||
"선": "thin"
|
||||
},
|
||||
"왼": {
|
||||
"선": "thin"
|
||||
},
|
||||
"오른": {
|
||||
"선": "thin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 8,
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"형식": "0.00",
|
||||
"테두리": {
|
||||
"위": {
|
||||
"선": "thin"
|
||||
},
|
||||
"아래": {
|
||||
"선": "thin"
|
||||
},
|
||||
"왼": {
|
||||
"선": "thin"
|
||||
},
|
||||
"오른": {
|
||||
"선": "thin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 8,
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"형식": "0.0",
|
||||
"테두리": {
|
||||
"위": {
|
||||
"선": "thin"
|
||||
},
|
||||
"아래": {
|
||||
"선": "thin"
|
||||
},
|
||||
"왼": {
|
||||
"선": "thin"
|
||||
},
|
||||
"오른": {
|
||||
"선": "thin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 8,
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"형식": "0",
|
||||
"테두리": {
|
||||
"위": {
|
||||
"선": "thin"
|
||||
},
|
||||
"아래": {
|
||||
"선": "thin"
|
||||
},
|
||||
"왼": {
|
||||
"선": "thin"
|
||||
},
|
||||
"오른": {
|
||||
"선": "thin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 11,
|
||||
"가로": "left",
|
||||
"세로": "center",
|
||||
"테두리": {
|
||||
"아래": {
|
||||
"선": "hair"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 11,
|
||||
"가로": "right",
|
||||
"세로": "center",
|
||||
"형식": "0",
|
||||
"테두리": {
|
||||
"아래": {
|
||||
"선": "hair"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 11,
|
||||
"굵게": true,
|
||||
"가로": "left",
|
||||
"세로": "center",
|
||||
"테두리": {
|
||||
"아래": {
|
||||
"선": "hair"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 8,
|
||||
"굵게": true,
|
||||
"채움": "#F2F2F2",
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"형식": "0.00",
|
||||
"테두리": {
|
||||
"위": {
|
||||
"선": "thin"
|
||||
},
|
||||
"아래": {
|
||||
"선": "thin"
|
||||
},
|
||||
"왼": {
|
||||
"선": "thin"
|
||||
},
|
||||
"오른": {
|
||||
"선": "thin"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"크기": 22,
|
||||
"굵게": true,
|
||||
"가로": "center",
|
||||
"세로": "center",
|
||||
"테두리": {
|
||||
"위": {
|
||||
"선": "thick"
|
||||
},
|
||||
"아래": {
|
||||
"선": "thick"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"시트": [
|
||||
@@ -302,7 +452,7 @@
|
||||
"칸": {
|
||||
"B4": {
|
||||
"값": "비탈면 안정성 검토 보고서",
|
||||
"서식": 1
|
||||
"서식": 26
|
||||
},
|
||||
"C4": {
|
||||
"서식": 1
|
||||
@@ -416,6 +566,10 @@
|
||||
},
|
||||
"E15": {
|
||||
"서식": 2
|
||||
},
|
||||
"B18": {
|
||||
"값": "검토 기준 — 건설공사 비탈면 설계기준(KDS 11 70 05) · 한계평형해석(Bishop 간편법)",
|
||||
"서식": 2
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
@@ -427,7 +581,8 @@
|
||||
"C11:E11",
|
||||
"C12:E12",
|
||||
"B14:E14",
|
||||
"B15:E15"
|
||||
"B15:E15",
|
||||
"B18:E18"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
@@ -451,7 +606,7 @@
|
||||
},
|
||||
"행": {
|
||||
"4": {
|
||||
"높이": 60
|
||||
"높이": 72
|
||||
},
|
||||
"5": {
|
||||
"높이": 24
|
||||
@@ -476,6 +631,9 @@
|
||||
},
|
||||
"12": {
|
||||
"높이": 30
|
||||
},
|
||||
"18": {
|
||||
"높이": 24
|
||||
}
|
||||
},
|
||||
"보기": {
|
||||
@@ -496,6 +654,82 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s9",
|
||||
"이름": "목차",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "목 차",
|
||||
"서식": 1
|
||||
},
|
||||
"A3": {
|
||||
"값": "장",
|
||||
"서식": 6
|
||||
},
|
||||
"B3": {
|
||||
"값": "내용",
|
||||
"서식": 6
|
||||
},
|
||||
"C3": {
|
||||
"값": "쪽",
|
||||
"서식": 6
|
||||
},
|
||||
"A4": {
|
||||
"서식": 24
|
||||
},
|
||||
"B4": {
|
||||
"서식": 22
|
||||
},
|
||||
"C4": {
|
||||
"서식": 23
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
"A1:C1"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
"폭": 10
|
||||
},
|
||||
"B": {
|
||||
"폭": 64
|
||||
},
|
||||
"C": {
|
||||
"폭": 10
|
||||
}
|
||||
},
|
||||
"행": {
|
||||
"1": {
|
||||
"높이": 48
|
||||
},
|
||||
"2": {
|
||||
"높이": 12
|
||||
},
|
||||
"3": {
|
||||
"높이": 24
|
||||
},
|
||||
"4": {
|
||||
"높이": 26
|
||||
}
|
||||
},
|
||||
"보기": {
|
||||
"눈금선": false
|
||||
},
|
||||
"인쇄": {
|
||||
"용지": "A4",
|
||||
"방향": "portrait",
|
||||
"맞춤": {
|
||||
"폭": 1,
|
||||
"높이": 0
|
||||
},
|
||||
"여백": {
|
||||
"위": 30,
|
||||
"아래": 15,
|
||||
"왼": 20,
|
||||
"오른": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s2",
|
||||
"이름": "개요",
|
||||
@@ -1566,6 +1800,9 @@
|
||||
},
|
||||
"G26": {
|
||||
"서식": 5
|
||||
},
|
||||
"D22": {
|
||||
"서식": 5
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
@@ -1586,7 +1823,8 @@
|
||||
"B18:G18",
|
||||
"B19:G19",
|
||||
"A21:G21",
|
||||
"A24:G24"
|
||||
"A24:G24",
|
||||
"D22:G22"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
@@ -1679,7 +1917,7 @@
|
||||
"이름": "측점표",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "측점별 안전율",
|
||||
"값": "측점별 안정성 판정 일람표",
|
||||
"서식": 1
|
||||
},
|
||||
"B1": {
|
||||
@@ -1710,7 +1948,7 @@
|
||||
"서식": 1
|
||||
},
|
||||
"A2": {
|
||||
"값": "판정 — 모든 조건의 안전율이 기준 이상이면 O.K · 하나라도 미달이면 N.G",
|
||||
"값": "판정 — 모든 조건의 안전율이 기준 이상이면 O.K · 하나라도 미달이면 N.G · N.G 측점 = 대책 검토 필요",
|
||||
"서식": 2
|
||||
},
|
||||
"B2": {
|
||||
@@ -1841,21 +2079,32 @@
|
||||
},
|
||||
"J6": {
|
||||
"서식": 7
|
||||
},
|
||||
"K4": {
|
||||
"값": "대책 필요",
|
||||
"서식": 6
|
||||
},
|
||||
"K5": {
|
||||
"서식": 6
|
||||
},
|
||||
"K6": {
|
||||
"서식": 7
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
"A1:J1",
|
||||
"A2:J2",
|
||||
"A1:K1",
|
||||
"A2:K2",
|
||||
"A4:A5",
|
||||
"B4:B5",
|
||||
"C4:C5",
|
||||
"D4:F4",
|
||||
"G4:I4",
|
||||
"J4:J5"
|
||||
"J4:J5",
|
||||
"K4:K5"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
"폭": 16
|
||||
"폭": 20
|
||||
},
|
||||
"B": {
|
||||
"폭": 13
|
||||
@@ -1883,6 +2132,9 @@
|
||||
},
|
||||
"J": {
|
||||
"폭": 9
|
||||
},
|
||||
"K": {
|
||||
"폭": 12
|
||||
}
|
||||
},
|
||||
"행": {
|
||||
@@ -2175,64 +2427,64 @@
|
||||
"높이": 22
|
||||
},
|
||||
"3": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"4": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"5": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"6": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"7": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"8": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"9": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"10": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"11": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"12": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"13": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"14": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"15": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"16": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"17": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"18": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"19": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"20": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"21": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"22": {
|
||||
"높이": 20
|
||||
"높이": 24
|
||||
},
|
||||
"23": {
|
||||
"높이": 32
|
||||
@@ -2998,6 +3250,13 @@
|
||||
},
|
||||
"G22": {
|
||||
"서식": 16
|
||||
},
|
||||
"A9": {
|
||||
"값": "기본값",
|
||||
"서식": 4
|
||||
},
|
||||
"B9": {
|
||||
"서식": 5
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
@@ -3008,6 +3267,7 @@
|
||||
"B6:H6",
|
||||
"B7:H7",
|
||||
"B8:H8",
|
||||
"B9:H9",
|
||||
"A10:H10",
|
||||
"A20:H20"
|
||||
],
|
||||
@@ -3055,6 +3315,9 @@
|
||||
},
|
||||
"21": {
|
||||
"높이": 32
|
||||
},
|
||||
"9": {
|
||||
"높이": 32
|
||||
}
|
||||
},
|
||||
"보기": {
|
||||
@@ -3423,6 +3686,233 @@
|
||||
"오른": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s10",
|
||||
"이름": "부록_절편",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "부록 ① 절편 계산표",
|
||||
"서식": 3
|
||||
},
|
||||
"A2": {
|
||||
"값": "단위 — 길이 m · 힘 kN/m(폭 1 m) · 압력 · 점착력 kPa · 각 °",
|
||||
"서식": 2
|
||||
},
|
||||
"A3": {
|
||||
"값": "번호",
|
||||
"서식": 18
|
||||
},
|
||||
"A4": {
|
||||
"서식": 21
|
||||
},
|
||||
"A5": {
|
||||
"값": "합계",
|
||||
"서식": 18
|
||||
},
|
||||
"B3": {
|
||||
"값": "폭 b (m)",
|
||||
"서식": 18
|
||||
},
|
||||
"B4": {
|
||||
"서식": 19
|
||||
},
|
||||
"B5": {
|
||||
"서식": 25
|
||||
},
|
||||
"C3": {
|
||||
"값": "높이 h (m)",
|
||||
"서식": 18
|
||||
},
|
||||
"C4": {
|
||||
"서식": 19
|
||||
},
|
||||
"C5": {
|
||||
"서식": 25
|
||||
},
|
||||
"D3": {
|
||||
"값": "저면 경사 α (°)",
|
||||
"서식": 18
|
||||
},
|
||||
"D4": {
|
||||
"서식": 20
|
||||
},
|
||||
"D5": {
|
||||
"서식": 25
|
||||
},
|
||||
"E3": {
|
||||
"값": "무게 W (kN/m)",
|
||||
"서식": 18
|
||||
},
|
||||
"E4": {
|
||||
"서식": 19
|
||||
},
|
||||
"E5": {
|
||||
"서식": 25
|
||||
},
|
||||
"F3": {
|
||||
"값": "상재 Q (kN/m)",
|
||||
"서식": 18
|
||||
},
|
||||
"F4": {
|
||||
"서식": 19
|
||||
},
|
||||
"F5": {
|
||||
"서식": 25
|
||||
},
|
||||
"G3": {
|
||||
"값": "간극수압 u (kPa)",
|
||||
"서식": 18
|
||||
},
|
||||
"G4": {
|
||||
"서식": 19
|
||||
},
|
||||
"G5": {
|
||||
"서식": 25
|
||||
},
|
||||
"H3": {
|
||||
"값": "저면 길이 l (m)",
|
||||
"서식": 18
|
||||
},
|
||||
"H4": {
|
||||
"서식": 19
|
||||
},
|
||||
"H5": {
|
||||
"서식": 25
|
||||
},
|
||||
"I3": {
|
||||
"값": "점착력 c (kPa)",
|
||||
"서식": 18
|
||||
},
|
||||
"I4": {
|
||||
"서식": 19
|
||||
},
|
||||
"I5": {
|
||||
"서식": 25
|
||||
},
|
||||
"J3": {
|
||||
"값": "마찰각 φ (°)",
|
||||
"서식": 18
|
||||
},
|
||||
"J4": {
|
||||
"서식": 20
|
||||
},
|
||||
"J5": {
|
||||
"서식": 25
|
||||
},
|
||||
"K3": {
|
||||
"값": "유효 수직력 N' (kN/m)",
|
||||
"서식": 18
|
||||
},
|
||||
"K4": {
|
||||
"서식": 19
|
||||
},
|
||||
"K5": {
|
||||
"서식": 25
|
||||
},
|
||||
"L3": {
|
||||
"값": "구동력 (W+Q)·sinα (kN/m)",
|
||||
"서식": 18
|
||||
},
|
||||
"L4": {
|
||||
"서식": 19
|
||||
},
|
||||
"L5": {
|
||||
"서식": 25
|
||||
},
|
||||
"M3": {
|
||||
"값": "저항력 S (kN/m)",
|
||||
"서식": 18
|
||||
},
|
||||
"M4": {
|
||||
"서식": 19
|
||||
},
|
||||
"M5": {
|
||||
"서식": 25
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
"A1:M1",
|
||||
"A2:M2"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
"폭": 5
|
||||
},
|
||||
"B": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"C": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"D": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"E": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"F": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"G": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"H": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"I": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"J": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"K": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"L": {
|
||||
"폭": 7.6
|
||||
},
|
||||
"M": {
|
||||
"폭": 7.6
|
||||
}
|
||||
},
|
||||
"행": {
|
||||
"1": {
|
||||
"높이": 24
|
||||
},
|
||||
"2": {
|
||||
"높이": 16
|
||||
},
|
||||
"3": {
|
||||
"높이": 34
|
||||
},
|
||||
"4": {
|
||||
"높이": 12
|
||||
},
|
||||
"5": {
|
||||
"높이": 14
|
||||
}
|
||||
},
|
||||
"보기": {
|
||||
"눈금선": false
|
||||
},
|
||||
"인쇄": {
|
||||
"용지": "A4",
|
||||
"방향": "portrait",
|
||||
"맞춤": {
|
||||
"폭": 1,
|
||||
"높이": 0
|
||||
},
|
||||
"여백": {
|
||||
"위": 15,
|
||||
"아래": 15,
|
||||
"왼": 10,
|
||||
"오른": 10
|
||||
},
|
||||
"쪽나눔": [
|
||||
5
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"활성": "s1"
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
|
||||
## 공통
|
||||
|
||||
- 몫: 사용자 몫(표지 · 개요) = 처음 만들 때만 채움 · 다시 만들 때(보고서 갱신) 안 건드림. 프로그램 몫(입력조건 · 측점표 · 대표단면 · 3D요약) = 갱신 때 통째 다시 씀.
|
||||
- 서식 번호(`서식` 표): 1 title · 2 sub · 3 section · 4 label · 5 text · 6 head · 7 body(가운데 글) · 8 fs(0.00) · 9 int(0) · 10 ng(N.G 빨강 채움) · 11 ok(O.K 초록) · 12 notice · 13 num1(#,##0.0) · 14 num0(#,##0) · 15 para(위 맞춤 긴 글) · 16 prop(0.0) · 17 chain(#,##0.00).
|
||||
- 몫: 사용자 몫(표지 · 개요) = 처음 만들 때만 채움 · 다시 만들 때(보고서 갱신) 안 건드림. 프로그램 몫(목차 · 입력조건 · 측점표 · 대표단면 · 3D요약 · 광역요약 · 보강검토 · 부록_절편) = 갱신 때 통째 다시 씀 · 작업본에 없는 프로그램 몫 시트는 양식 차례 자리에 끼움 · 새 서식은 작업본 서식 표 끝에 붙여 번호를 바꿈.
|
||||
- 시트 차례: 표지 · 목차 · 개요 · 입력조건 · 측점표 · 대표단면 · 3D요약 · 광역요약 · (보강검토) · 부록_절편.
|
||||
- PDF: 표지 = 쪽 번호 · 머리말 없음 · 본문 = 머리말(왼 사업명 = 표지 `C8` · 오른 보고서 이름) · 꼬리말 「- n / N -」(목차 = 1 쪽).
|
||||
- 사용자 확정 전 표기 한 규칙 — 기본값 그대로 = 「(기본값 · 사용자 확정 전)」 · 바꿈 = 「(사용자 입력)」 · 물성 = 「문헌 추정치 · 사용자 확정 전」.
|
||||
- 서식 번호(`서식` 표): 1 title · 2 sub · 3 section · 4 label · 5 text · 6 head · 7 body(가운데 글) · 8 fs(0.00) · 9 int(0) · 10 ng(N.G 빨강 채움) · 11 ok(O.K 초록) · 12 notice · 13 num1(#,##0.0) · 14 num0(#,##0) · 15 para(위 맞춤 긴 글) · 16 prop(0.0) · 17 chain(#,##0.00) · 18 head8 · 19 num8(0.00) · 20 num8(0.0) · 21 int8 · 22 목차 내용 · 23 목차 쪽 · 24 목차 장 · 25 sum8(0.00 · 회색) · 26 표지 제목.
|
||||
- 판정 글(`O.K` · `N.G`) 칸 = 엔진 판정을 값으로 씀 · N.G 칸은 서식 10 · O.K 칸은 서식 11 을 직접 붙임 — 인쇄 HTML · xlsx 는 `조건부서식` 을 안 먹이므로(화면만). 양식의 `조건부서식`(글포함 `N.G` → 빨강)은 사용자가 화면에서 고친 판정 글용.
|
||||
- 수식이 보는 끝 행 = 20000(측점표 · 3D요약). 측점이 이보다 많으면 개요 수식의 `20000` 을 같이 늘림.
|
||||
- 빈 값은 칸을 안 적거나 `값` 을 안 둠(빈 글 `""` 금지).
|
||||
@@ -19,6 +22,16 @@
|
||||
| C8:E8 · C9:E9 · C10:E10 · C11:E11 · C12:E12 | 사업명 · 노선명 · 발주처 · 작성일 · 작성자 값(병합 · 서식 5) — 왼위 칸 `C8` … `C12` 에 값 |
|
||||
| B14:E14 | 「문헌 추정치 · 현장 시험 없음」(고정 글 · 건드리지 않음) |
|
||||
|
||||
## s9 목차 (프로그램 몫)
|
||||
|
||||
| 칸 | 내용 |
|
||||
| --- | --- |
|
||||
| `A1:C1` | 제목(고정 글) |
|
||||
| 3 행 | 머리 — 장 · 내용 · 쪽 |
|
||||
| 4 행부터 보이는 본문 시트 하나 = 한 줄 | A 장(`1.` … · 부록 = `부록`)(24) · B 내용(22) · C 시작 쪽(23 · 표지 뺀 본문 차례 · 목차 = 1) |
|
||||
|
||||
- 쪽 번호 = 시트 하나씩 인쇄 → PDF 쪽 수로 셈(보고서 만들 때 · PDF 내보내기 때 다시) · 브라우저가 없으면 「-」.
|
||||
|
||||
## s2 개요 (사용자 몫 · 수식은 양식에 들어 있음)
|
||||
|
||||
- 빌더가 쓸 것: 없음(수식이 `측점표` · `입력조건` 을 가리킴). 의견 칸 `A22:F30`(병합 · 서식 15) = 사용자 글.
|
||||
@@ -34,11 +47,12 @@
|
||||
| | `C12` `C13` `C14` (C:G 병합) | 비고 글 |
|
||||
| 지하수 규칙 | `B17` `B18` `B19` (B:G 병합) | 건기 · 우기 · 지진 시 규칙 글 |
|
||||
| 노면 하중 | `B22` | 수(kPa · 서식 16) |
|
||||
| | `D22` (D:G 병합) | 비고 — 사용자 확정 전 표기 |
|
||||
| 지반 물성치 | **26 행부터 한 줄씩** · 머리 25 행(머리 줄 반복) | A 지층 · B 적용 구간(전체 또는 구간 글) · C γt · D γsat · E c · F φ(서식 16) · G 출처(서식 5) · A·B = 서식 7 |
|
||||
|
||||
- 지반 물성치가 표의 맨 끝이라 줄 수가 늘어도 다른 칸이 안 밀림. 구간별 덮어쓰기 줄 = 같은 표에 줄 추가(B 열에 구간).
|
||||
|
||||
## s4 측점표 (프로그램 몫 · 머리 줄 4~5 행 쪽마다 반복 · 가로 · 폭 1쪽)
|
||||
## s4 측점표 (프로그램 몫 · 판정 일람표 · 머리 줄 4~5 행 쪽마다 반복 · 가로 · 폭 1쪽)
|
||||
|
||||
- 자료는 **6 행부터 측점 하나 = 한 줄** · 서식은 6 행 서식을 줄마다 그대로.
|
||||
|
||||
@@ -50,9 +64,10 @@
|
||||
| D · E · F | 건기 · 우기 · 지진 안전율 Fs(지진 끔 = 칸 없음) | 8 |
|
||||
| G · H · I | 건기 · 우기 · 지진 기준 안전율 | 8 |
|
||||
| J | 판정 `O.K` · `N.G`(엔진 판정 값) | N.G = 10 · O.K = 11 |
|
||||
| K | 대책 필요(`대책 검토` · O.K = `-`) | 7 |
|
||||
|
||||
- 개요 · 3D 수식이 `A6:A20000` · `B` · `D` · `E` · `F` · `J` 열을 읽음 → 이 열 배치 바꾸지 않음.
|
||||
- 줄 수 = 측점 수(시험: 측점 수 = 줄 수). 쪽 나눔은 자동(`인쇄.쪽나눔` 안 씀).
|
||||
- 줄 수 = 측점 수(시험: 측점 수 = 줄 수). 쪽 나눔 = 빌더가 쪽마다 고르게(`인쇄.쪽나눔` · 마지막 쪽에 한두 줄만 남지 않게).
|
||||
|
||||
## s5 대표단면 (프로그램 몫 · 단면 하나 = 28 행 덩이 = 한 쪽)
|
||||
|
||||
@@ -62,14 +77,14 @@
|
||||
| --- | --- | --- |
|
||||
| 1 | `A1:H1` 병합 | 단면 제목(`대표 단면 ① — 측점 · 구분 · 높이`) |
|
||||
| 2 | `B2:H2` 병합 | 선정 사유 글 |
|
||||
| 3~22 | 그림 `A3:H22` | 그림 한 장 — 그림 칸 크기 약 712 × 533 px(가로 : 세로 = 4 : 3) · 열쇠 `fig/<측점 키>.png` · 닻은 `dr` 만큼 내림 |
|
||||
| 3~22 | 그림 `A3:H22` | 그림 한 장 — 행 24 pt · 그림 칸 712 × 640 px · 안전율 상자 · 지층 범례(무늬) · 축척 막대 · 열쇠 `fig/<측점 키>.png` · 닻은 `dr` 만큼 내림 |
|
||||
| 23 | 머리(고정 글) | 조건 · 해석 방법 · 안전율 · 기준 · 판정 · 활동면 반경 R · 중심 x · 중심 y |
|
||||
| 24 · 25 · 26 | A = 건기 · 우기 · 지진(고정 글) | B 해석 방법 글(서식 7) · C Fs(8) · D 기준(8) · E 판정(7→ N.G 10 · O.K 11) · F 반경 · G 중심 x · H 중심 y(13) |
|
||||
| 27 | `B27:H27` 병합 | 비고 글 |
|
||||
| 28 | 빈 줄 | 쪽 끝 |
|
||||
|
||||
- `인쇄.쪽나눔` = `[28, 56, …, 28 × 단면 수]`(마지막 덩이 끝 쪽 나눔은 있어도 빈 쪽이 안 생김).
|
||||
- 지진을 안 쓰면 26 행(덩이 안) 칸을 비움.
|
||||
- 지진을 안 쓰면 26 행(덩이 안) B = 「검토 안 함」. 비원호 면 = F · G · H 「-」. 비고 끝 = 「부록 ⓝ 절편 계산표」.
|
||||
|
||||
## s6 3D요약 (프로그램 몫 · 머리 줄 19 행 반복)
|
||||
|
||||
@@ -85,6 +100,7 @@
|
||||
| 칸 | 내용 |
|
||||
| --- | --- |
|
||||
| `B5` `B6` `B7` `B8` (각 B:H 병합 · 서식 5) | 노선 띠 · 토층 물성 · 시나리오(표준 SINMAP 습윤지수 · 최악 완전 포화) · 해석 때 글 |
|
||||
| `B9` (B:H 병합) | 기본값 그대로인 설정 · 물성 — 사용자 확정 전 한 줄 |
|
||||
| `C12`~`C17` | 등급 1~6(안정 · 보통 안정 · 준안정 · 위험 하한 · 위험 상한 · 불안정) 면적 m²(서식 14) · `D` 비율 = 양식 수식 · `C18` 합계 = 양식 수식 |
|
||||
| `E11`~`G14` | 측점 등급 표 — 낮음 · 보통 · 높음별 SI<1 셀 비율 기준 글(7) · 측점 수(14) |
|
||||
| 22 행부터 보통 · 높음 측점 하나 = 한 줄 | A 측점(서식 7) · B 누가거리(17) · C 최소 Fs · D 최소 SI(8) · E 측점 등급 + SI<1 비율 글(7) · F 위험 면적 m²(14) · G 최대 경사 °(16) · 없으면 `A22` 「위험 측점 없음」 |
|
||||
@@ -100,3 +116,17 @@
|
||||
| 그 아래 벽 하나 × 경우 하나 = 한 줄 | A 측점(7) · B 벽 이름 · 평상시 / 지진 시 · 토압법 · 배면 수위(5) · C 활동 · E 전도 · G 지지력 Fs(8) · D · F · H 기준(8 · KDS 11 80 05) · I 판정(7 → 10 · 11) · 없으면 「벽 검토 없음」 |
|
||||
|
||||
- 6 행 · 10 행 서식을 줄마다 복사 · 저장본 = (측점 · 종류 · 비탈)마다 가장 최근 하나.
|
||||
|
||||
## s10 부록_절편 (프로그램 몫 · 대표 단면마다 한 쪽)
|
||||
|
||||
양식 1~5 행 = 덩이 꼴(1 제목 · 2 단위 · 3 머리 · 4 절편 줄 서식 · 5 합계 서식). 대표 단면마다 지배 조건(Fs / 기준 가장 낮음) 임계면의 절편표.
|
||||
|
||||
| 덩이 안 행 | 칸 | 내용 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `A:M` 병합 | 「부록 ⓝ 절편 계산표 — 측점 · 구분 · 조건 · 방법 · Fs」 |
|
||||
| 2 | `A:M` 병합 | 임계면 · 단위 글 |
|
||||
| 3 | 머리 | 번호 · 폭 b · 높이 h · 저면 경사 α · 무게 W · 상재 Q · 간극수압 u · 저면 길이 l · c · φ · 유효 수직력 N' · 구동력 (W+Q)·sinα · 저항력 S |
|
||||
| 4 … | 절편 하나 = 한 줄 | 서식 21(번호) · 20(α · φ) · 19(그 밖) |
|
||||
| 끝 | 합계 | A 「합계」 · E ΣW · F ΣQ · L Σ구동력 · M Σ저항력(서식 25) |
|
||||
|
||||
- 저항력 S = 동원 전단 × Fs = c·l + N'·tanφ · 덩이 끝마다 쪽 나눔 · 대표 단면 없음 = `A1` 「부록 — 절편 계산표 없음」.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""534-3(빌더 · storage · 길) — E05 안정성평가 보고서 `E05_StabReport_Build` · `_Store` · `_Router`.
|
||||
"""534-3 · 534-5(빌더 · storage · 길 · 인쇄) — E05 안정성평가 보고서 `E05_StabReport_Build` ·
|
||||
`_Section` · `_Print` · `_Store` · `_Router`.
|
||||
|
||||
가짜 E 프로젝트(E03 파생 저장본 꼴 2D 요약 · 측점별 저장본 · 대표 단면 켬/끔 · 지반 조건 ·
|
||||
3D 저장본 하나 · 복사본 읽기는 바꿔 끼움)로 측점 수 = 줄 수 · N.G 수 · 그림 파일 수 ·
|
||||
갱신 뒤 사용자 몫 불변 · 덮임 목록 · 초기화 · 판 409 · xlsx 되읽기 · PDF 쪽 수.
|
||||
갱신 뒤 사용자 몫 불변 · 덮임 목록 · 초기화 · 판 409 · xlsx 되읽기 · PDF 쪽 수 ·
|
||||
목차 쪽 번호 = 실제 · 부록 절편 · 측점표 쪽 나눔 고르게 · 옛 작업본 갱신(목차 자리 · 서식 붙임).
|
||||
"""
|
||||
|
||||
import io
|
||||
@@ -15,11 +17,11 @@ from openpyxl import load_workbook
|
||||
|
||||
from common_util.common_util_derived_store import derived_dir, write_derived
|
||||
from common_util.common_util_print_pdf import PrintError
|
||||
from common_util.common_util_spreadsheet import workbook_pdf
|
||||
from common_util.common_util_workbook_xlsx import workbook_to_xlsx
|
||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||
from E05_StabReport import E05_StabReport_Build as build
|
||||
from E05_StabReport import E05_StabReport_Figure as figure
|
||||
from E05_StabReport import E05_StabReport_Print as printer
|
||||
from E05_StabReport import E05_StabReport_Store as store
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
@@ -29,6 +31,7 @@ REPS = [("grid:3", "lowest"), ("grid:12", "cut_height"), ("grid:20", "fill_heigh
|
||||
# 자동 대표(높이 · 하위 5) 중 REPS 만 남기는 사용자 끔
|
||||
OFF = {"grid:0": False, "grid:5": False, "grid:10": False, "grid:17": False, "grid:24": False}
|
||||
KIND_SUMMARY, KIND_STATION = "stab2d_summary", "stab2d_station"
|
||||
SLICES = 24 # 임계면 절편 수
|
||||
REQUIRED = {"dry": {"cut": 1.5, "fill": 1.5}, "wet": {"cut": 1.2, "fill": 1.3}}
|
||||
|
||||
|
||||
@@ -82,9 +85,24 @@ def _write_summary(root: Path, fp: str, rows: list[dict]) -> None:
|
||||
write_derived(root, KIND_SUMMARY, fp, value)
|
||||
|
||||
|
||||
def _slice(i: int) -> dict:
|
||||
return {
|
||||
"width": 0.5, "height": 1.0 + i / 10, "alpha_deg": 10.0 + i, "weight": 9.0 + i,
|
||||
"surcharge": 0.0, "pore_pressure": 2.0, "base_length": 0.51, "c": 15.0,
|
||||
"phi_deg": 30.0, "normal_eff": 8.0 + i, "shear_mobilized": 6.0 + i / 10,
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def _slip(F: float) -> dict:
|
||||
pts = [[-6.0 + k, 101.0 - 0.3 * (6 - abs(k - 6))] for k in range(13)]
|
||||
return {"F": F, "kind": "circle", "points": pts, "center": [0.0, 112.0], "radius": 12.5}
|
||||
return {
|
||||
"F": F,
|
||||
"kind": "circle",
|
||||
"points": pts,
|
||||
"center": [0.0, 112.0],
|
||||
"radius": 12.5,
|
||||
"slices": [_slice(i) for i in range(SLICES)],
|
||||
}
|
||||
|
||||
|
||||
def _station_doc(i: int) -> dict:
|
||||
@@ -208,6 +226,29 @@ def test_처음_만듦_측점수_NG_그림(project):
|
||||
j = [c for a1, c in by["측점표"]["칸"].items() if a1.startswith("J") and int(a1[1:]) >= 6]
|
||||
assert sum(c.get("값") == "N.G" and c["서식"] == build.ST_NG for c in j) == len(NG_AT)
|
||||
assert sum(c.get("값") == "O.K" and c["서식"] == build.ST_OK for c in j) == N - len(NG_AT)
|
||||
k = [
|
||||
c.get("값")
|
||||
for a1, c in by["측점표"]["칸"].items()
|
||||
if a1.startswith("K") and int(a1[1:]) >= 6
|
||||
]
|
||||
assert k.count("대책 검토") == len(NG_AT) and k.count("-") == N - len(NG_AT)
|
||||
# 측점표 쪽 나눔 = 고르게(덩이 크기 차 ≤ 1 · 한 줄 꼬리 없음)
|
||||
cuts = by["측점표"]["인쇄"]["쪽나눔"]
|
||||
sizes = [b - a for a, b in zip([5, *cuts], [*cuts, 5 + N])]
|
||||
assert len(sizes) >= 2 and max(sizes) - min(sizes) <= 1
|
||||
# 부록 절편 — 대표 단면마다 지배 조건 한 덩이 · 저항력 = 동원 전단 × Fs · 덩이 끝 쪽 나눔
|
||||
s10 = by["부록_절편"]["칸"]
|
||||
block = 3 + SLICES + 1
|
||||
assert by["부록_절편"]["인쇄"]["쪽나눔"] == [block * (i + 1) for i in range(len(REPS))]
|
||||
assert "건기" in s10["A1"]["값"] and s10["A1"]["값"].startswith("부록 ① 절편 계산표 — No.3")
|
||||
assert s10["A4"]["값"] == 1 and s10["M4"]["값"] == pytest.approx(6.0 * 1.31)
|
||||
assert s10[f"A{block}"]["값"] == "합계"
|
||||
assert s10[f"E{block}"]["값"] == pytest.approx(sum(9.0 + i for i in range(SLICES)))
|
||||
assert book["서식"][s10["B4"]["서식"]]["크기"] == 8
|
||||
# 목차 — 본문 시트 차례 · 쪽 번호(목차 = 1)
|
||||
toc = by["목차"]["칸"]
|
||||
assert [toc[f"B{r}"]["값"] for r in (4, 5)] == [printer.TITLES["s2"], printer.TITLES["s3"]]
|
||||
assert toc["A10"]["값"] == "부록" # 보강검토 없음 → 본문 6 + 부록 and toc["C4"]["값"] == 2
|
||||
figs = sorted((project / "E05_StabReport" / "fig").glob("*.png"))
|
||||
assert len(figs) == len(REPS) + 1 and meta["figures"][-1] == build.BAND_KEY
|
||||
assert [f["열쇠"] for f in by["대표단면"]["그림"]] == [build.fig_key(k) for k, _ in REPS]
|
||||
@@ -356,6 +397,29 @@ def test_갱신_사용자몫_불변_덮임(project):
|
||||
)
|
||||
|
||||
|
||||
def test_옛_작업본_갱신_목차자리_서식(project):
|
||||
"""534-5 전 작업본(목차 · 부록 없음 · 서식 18 개) → 갱신 = 목차는 표지 다음 · 부록은 끝 ·
|
||||
새 서식 붙임(옛 서식 번호 그대로) · 사용자 몫 불변."""
|
||||
_create(project)
|
||||
book, meta = store.load(project)
|
||||
book["시트"] = [s for s in book["시트"] if s["id"] not in ("s9", "s10")]
|
||||
book["서식"] = book["서식"][:18] + [{"크기": 30}] # 사용자가 더한 서식 = 18 번
|
||||
_sheets(book)["표지"]["칸"]["C10"] = {"값": "산림청", "서식": 18}
|
||||
meta["signs"] = {k: v for k, v in meta["signs"].items() if k not in ("s9", "s10")}
|
||||
store.save(project, book)
|
||||
(project / "E05_StabReport" / build.META).write_text(json.dumps(meta), encoding="utf-8")
|
||||
store.refresh(project)
|
||||
book, _ = store.load(project)
|
||||
ids = [s["id"] for s in book["시트"]]
|
||||
assert ids[:3] == ["s1", "s9", "s2"] and ids[-1] == "s10"
|
||||
by = _sheets(book)
|
||||
assert by["표지"]["칸"]["C10"] == {"값": "산림청", "서식": 18} and book["서식"][18] == {
|
||||
"크기": 30
|
||||
}
|
||||
head = book["서식"][by["부록_절편"]["칸"]["A3"]["서식"]]
|
||||
assert head["크기"] == 8 and head["굵게"] and len(book["서식"]) == len(build.form()["서식"]) + 1
|
||||
|
||||
|
||||
def test_3D_없음_2D_없음(project):
|
||||
shutil.rmtree(project / "E04_Stab3D")
|
||||
inputs = build.gather(project)
|
||||
@@ -375,24 +439,36 @@ def test_xlsx_되읽기_pdf_쪽수(project):
|
||||
figs = store.figures_of(project, book)
|
||||
assert len(figs) == len(REPS) + 1
|
||||
wb = load_workbook(io.BytesIO(workbook_to_xlsx(book, figs)))
|
||||
assert wb.sheetnames == ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약"]
|
||||
assert wb.sheetnames == [
|
||||
"표지", "목차", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약", "부록_절편",
|
||||
] # fmt: skip
|
||||
assert len(wb["대표단면"]._images) == len(REPS) and len(wb["3D요약"]._images) == 1
|
||||
assert [b.id for b in wb["대표단면"].row_breaks.brk] == [28, 56, 84]
|
||||
assert wb["측점표"]["J9"].value == "N.G" and wb["측점표"]["J9"].fill.fgColor.rgb.endswith(
|
||||
"FFC7CE"
|
||||
)
|
||||
book.pop("계산값", None)
|
||||
try:
|
||||
pdf = workbook_pdf(book, figs)
|
||||
pdf = printer.report_pdf(book, figs)
|
||||
except PrintError as error:
|
||||
pytest.skip(f"브라우저 없음 — {error}")
|
||||
from pypdf import PdfReader
|
||||
|
||||
texts = [p.extract_text() for p in PdfReader(io.BytesIO(pdf)).pages]
|
||||
stab = [i for i, t in enumerate(texts) if "산정 안전율" in t]
|
||||
assert stab and all("산정 안전율" in texts[i] for i in stab)
|
||||
# 표지 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 3 + 3D 1 + 광역 1
|
||||
assert len(texts) == 3 + len(stab) + len(REPS) + 1 + 1
|
||||
assert len(stab) == len(book["시트"][4]["인쇄"]["쪽나눔"]) + 1 # 측점표 = 나눈 덩이 수
|
||||
# 표지 1 + 목차 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 3 + 3D 1 + 광역 1 + 부록 3
|
||||
assert len(texts) == 4 + len(stab) + len(REPS) + 1 + 1 + len(REPS)
|
||||
body = len(texts) - 1
|
||||
assert f"/ {body}" not in texts[0] and "검증 임도" not in texts[0].split("보고서")[0]
|
||||
assert all(f"- {i} / {body} -" in texts[i] and "검증 임도" in texts[i] for i in range(1, body))
|
||||
# 목차 쪽 번호 = 실제 쪽(본문 n 쪽 = PDF n + 1 번째 쪽)
|
||||
toc = texts[1]
|
||||
marks = {"측점별 안정성 판정 일람표": "산정 안전율", printer.TITLES["s5"]: "대표 단면 ①"}
|
||||
marks[printer.TITLES["s10"]] = "절편 계산표"
|
||||
for title, inside in marks.items():
|
||||
line = next(x for x in toc.splitlines() if title in x)
|
||||
page = int(line.split()[-1])
|
||||
assert inside in texts[page], (title, page)
|
||||
|
||||
|
||||
def test_단면_그림_SVG():
|
||||
@@ -430,7 +506,7 @@ def test_길_저장_409_갱신_초기화_내보내기(project, monkeypatch):
|
||||
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"]["시트"]) == 7
|
||||
assert got["editable"] and got["stale"] is False and len(got["book"]["시트"]) == 9
|
||||
book = got["book"]
|
||||
_sheets(book)["표지"]["칸"]["C10"] = {"값": "산림청", "서식": 5}
|
||||
assert client.put(url, json={"rev": "옛판", "book": book}).status_code == 409
|
||||
|
||||
@@ -36,7 +36,10 @@ STYLE_KEYS = {
|
||||
} # fmt: skip
|
||||
PRINT_KEYS = {"용지", "방향", "배율", "맞춤", "여백", "머리행", "쪽나눔"}
|
||||
A1 = re.compile(r"^([A-Z]{1,3})(\d+)$")
|
||||
NAMES = ["표지", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약", "보강검토"]
|
||||
NAMES = [
|
||||
"표지", "목차", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약", "보강검토",
|
||||
"부록_절편",
|
||||
] # fmt: skip
|
||||
|
||||
needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
@@ -148,7 +151,8 @@ 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", "s8"]
|
||||
ids = ["s1", "s9", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s10"]
|
||||
assert [s["id"] for s in book["시트"]] == ids
|
||||
|
||||
|
||||
def test_양식_인쇄_쪽나눔_머리행_그림():
|
||||
@@ -161,6 +165,8 @@ def test_양식_인쇄_쪽나눔_머리행_그림():
|
||||
figs = {n: len(s.get("그림", [])) for n, s in by.items()}
|
||||
assert figs == {
|
||||
"표지": 0,
|
||||
"목차": 0,
|
||||
"부록_절편": 0,
|
||||
"개요": 0,
|
||||
"입력조건": 0,
|
||||
"측점표": 0,
|
||||
@@ -231,8 +237,9 @@ 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 + 보강 1(양식 빈 판)
|
||||
assert len(pages) == 1 + 1 + 1 + len(stab) + 2 + 1 + 1 + 1
|
||||
# 표지 1 + 목차 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 2 + 3D 1 + 광역 1 + 보강 1 + 부록 1
|
||||
# (양식 빈 판)
|
||||
assert len(pages) == 1 + 1 + 1 + 1 + len(stab) + 2 + 1 + 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
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { LocaleEntry } from "./ui_template_locale_common";
|
||||
export const ui_locales_e05 = {
|
||||
/* --- E05 보고서 --- */
|
||||
E05_Note: [
|
||||
"표지 · 개요는 사용자 몫 — [보고서 갱신] 때 그대로 둡니다. 입력조건 · 측점표 · 대표단면 · 3D요약은 해석 결과로 다시 씁니다.",
|
||||
"Cover and summary are yours — kept on [Refresh report]. Conditions, stations, sections and 3D sheets are rewritten from results.",
|
||||
"표지 · 개요는 사용자 몫 — [보고서 갱신] 때 그대로 둡니다. 목차 · 입력조건 · 측점표 · 대표단면 · 3D · 광역 · 보강 · 부록은 해석 결과로 다시 씁니다.",
|
||||
"Cover and summary are yours — kept on [Refresh report]. Contents, conditions, stations, sections, 3D, area, reinforcement and appendix sheets are rewritten from results.",
|
||||
],
|
||||
E05_Btn_Refresh: ["보고서 갱신", "Refresh report"],
|
||||
E05_Btn_Xlsx: ["엑셀", "Excel"],
|
||||
|
||||
Reference in New Issue
Block a user