- spreadsheet_print_html: 통합문서 → 인쇄 HTML(DOM 없음 · 화면 엔진 값 · formatValue · 병합 · colgroup 폭 · 쪽 나눔 덩이 · thead 머리행 반복 · 그림 img · 시트마다 @page) - 서식 → CSS 를 클립보드에서 떼어 공용(medium · thick · double 굵기 반영) - 서버 Node 진입 prints · 파이썬 print_htmls · workbook_pdf(공용 PDF 엔진 page_size=None) - common_util_workbook_xlsx: openpyxl 로 값 · 식 · 서식 · 병합 · 폭 · 높이 · 틀 고정 · 인쇄 설정 · 쪽 나눔 · 그림(두 칸 닻) - common_util_svg_png: Playwright 로 SVG → PNG(PyMuPDF 안 씀) - 시험 test_spreadsheet_print(인쇄 HTML · xlsx 되읽기 · PNG 크기 · PDF 쪽 수) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
5.3 KiB
Python
117 lines
5.3 KiB
Python
"""산출근거 스프레드시트(통합문서) 서버 재계산 — 화면과 같은 TS 엔진을 Node 로 돌리는 껍데기.
|
|
|
|
CLAUDE.md 5장 ② — 계산은 `A00_Common/spreadsheet/` 한 벌. 여기는 번들을 부르기만 함
|
|
(`npm run build:spreadsheet` → `config/spreadsheet_node/`). [저장] 때 브라우저가 보낸 `계산값` 은
|
|
버리고 이 결과를 정본으로 적음.
|
|
|
|
결과 한 권 = `{시트 id: {A1: {수|글|참|오류}}}`(식 칸만) · 수는 십진 글.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import copy
|
|
from typing import Any
|
|
|
|
from common_util.common_util_node_bundle import ROOT, build_bundle, run_bundle_json
|
|
|
|
BUNDLE = ROOT / "config" / "spreadsheet_node" / "common_util_spreadsheet_node.js"
|
|
NPM_SCRIPT = "build:spreadsheet"
|
|
ENTRY = ROOT / "common_util" / "common_util_spreadsheet_node.ts"
|
|
SOURCES = (ROOT / "A00_Common" / "spreadsheet", ROOT / "ui_template" / "sheet")
|
|
|
|
|
|
class RecalcError(Exception):
|
|
"""번들 빌드 · 실행 실패 또는 엔진이 그 통합문서를 못 풂 — 저장을 막음(검산 없이 적지 않음)."""
|
|
|
|
|
|
def _stale() -> bool:
|
|
"""번들이 없거나 엔진 TS 원본보다 오래됐으면 참 — 낡으면 화면과 서버가 다른 값을 냄."""
|
|
if not BUNDLE.is_file():
|
|
return True
|
|
built_at = BUNDLE.stat().st_mtime
|
|
paths = [ENTRY, *(p for folder in SOURCES for p in folder.glob("*.ts"))]
|
|
return any(p.stat().st_mtime > built_at for p in paths)
|
|
|
|
|
|
def recalc_workbooks(books: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""통합문서 여럿 → 입력 차례 그대로 `계산값`. 하나라도 못 풀면 `RecalcError`."""
|
|
if _stale() and not build_bundle(NPM_SCRIPT):
|
|
raise RecalcError("스프레드시트 계산 번들을 못 만듦")
|
|
out = run_bundle_json(BUNDLE, NPM_SCRIPT, {"books": books})
|
|
if out is None:
|
|
raise RecalcError("스프레드시트 계산 번들 실행 실패")
|
|
values: list[dict[str, Any]] = []
|
|
for result in out["results"]:
|
|
if not result.get("ok"):
|
|
raise RecalcError(f"스프레드시트 계산 실패 — {result.get('까닭')}")
|
|
values.append(result["계산값"])
|
|
return values
|
|
|
|
|
|
def with_server_values(book: dict[str, Any]) -> dict[str, Any]:
|
|
"""[저장] 직전 — 브라우저 `계산값` 은 버리고 서버 풀이로 새로 적은 사본."""
|
|
clean = copy.deepcopy(book)
|
|
clean.pop("계산값", None)
|
|
clean["계산값"] = recalc_workbooks([clean])[0]
|
|
return clean
|
|
|
|
|
|
def area_tables(asks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""면적산출 시트 · 범위(또는 `이름`) → CAD 표 객체(41-1 · 104-1 · 변환은 번들 `areaTable` 한 벌) —
|
|
물음 차례 그대로 `{ok, 표, 시트, 범위, 값}` 또는 `{ok: False, 까닭}`(이름이 지워졌으면 `까닭` = 「이름없음」).
|
|
번들을 못 돌리면 `RecalcError`."""
|
|
if _stale() and not build_bundle(NPM_SCRIPT):
|
|
raise RecalcError("스프레드시트 계산 번들을 못 만듦")
|
|
out = run_bundle_json(BUNDLE, NPM_SCRIPT, {"tables": asks})
|
|
if out is None:
|
|
raise RecalcError("스프레드시트 계산 번들 실행 실패")
|
|
return out["tables"]
|
|
|
|
|
|
def define_names(asks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""면적 시트에 보낸 범위 이름을 더하거나 갱신(104-1 · 처음 도면으로 보낼 때 1번 · 번들 `defineAreaName`) —
|
|
물음 차례 그대로 `{ok, book}` 또는 `{ok: False, 까닭}`. 번들을 못 돌리면 `RecalcError`."""
|
|
if _stale() and not build_bundle(NPM_SCRIPT):
|
|
raise RecalcError("스프레드시트 계산 번들을 못 만듦")
|
|
out = run_bundle_json(BUNDLE, NPM_SCRIPT, {"names": asks})
|
|
if out is None:
|
|
raise RecalcError("스프레드시트 계산 번들 실행 실패")
|
|
return out["names"]
|
|
|
|
|
|
def print_htmls(asks: list[dict[str, Any]]) -> list[str]:
|
|
"""통합문서 → 인쇄 HTML(534-2 · 번들 `printHtml` 한 벌) —
|
|
ask = `{book, figures?: {열쇠: PNG 바이트}}`.
|
|
물음 차례 그대로 HTML. 하나라도 못 만들면 `RecalcError`."""
|
|
if _stale() and not build_bundle(NPM_SCRIPT):
|
|
raise RecalcError("스프레드시트 계산 번들을 못 만듦")
|
|
prints = [
|
|
{
|
|
"book": ask["book"],
|
|
"figures": {
|
|
key: "data:image/png;base64," + base64.b64encode(png).decode()
|
|
for key, png in (ask.get("figures") or {}).items()
|
|
},
|
|
}
|
|
for ask in asks
|
|
]
|
|
out = run_bundle_json(BUNDLE, NPM_SCRIPT, {"prints": prints})
|
|
if out is None:
|
|
raise RecalcError("스프레드시트 계산 번들 실행 실패")
|
|
htmls: list[str] = []
|
|
for result in out["prints"]:
|
|
if not result.get("ok"):
|
|
raise RecalcError(f"인쇄 HTML 실패 — {result.get('까닭')}")
|
|
htmls.append(result["html"])
|
|
return htmls
|
|
|
|
|
|
def workbook_pdf(book: dict[str, Any], figures: dict[str, bytes] | None = None) -> bytes:
|
|
"""통합문서 → PDF(요구 347-5) — 인쇄 HTML → 공용 PDF 엔진(쪽 크기 · 여백 = 시트마다 CSS @page).
|
|
실패 = `RecalcError` 또는 `common_util_print_pdf.PrintError`."""
|
|
from common_util.common_util_print_pdf import render_html_to_pdf
|
|
|
|
html = print_htmls([{"book": book, "figures": figures}])[0]
|
|
return render_html_to_pdf(html, page_size=None)
|