Files
Aislo/B09_Estimation/draft/B09_Draft_Print_Drawings_Router.py
T
eomsangdonandClaude Opus 5.5 b8ef5d033d feat(535-2): B09 도면 PDF PyMuPDF(AGPL) 제거 — 장마다 SVG 쪽 → 공용 PDF 엔진(Chromium) · pypdf 합치기
- 점선 = 미리 자른 실선 조각(Chromium 이 점선을 채운 윤곽으로 풀어 PDF 3 배 커지는 것 막음)
- 글 안 빈칸 그대로(white-space: pre) · 세운 글 방향 = 화면(openwebcad canvas)과 같게
- 못 넣은 그림 = data: 아님 · base64 깨짐
- test_302_2: PDF 읽기 pymupdf → pypdf · add_drawing_page(pymupdf 문서) 시험 → drawing_page(SVG) 로 고침 · 점선 조각 · 빈칸 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:42:54 +09:00

108 lines
4.1 KiB
Python

"""인쇄 미리보기 도면(PLAN 302-2) — 목록 · PDF 길.
GET /api/projects/{id}/print/drawings
→ [{id, source, kind, label, number, confirmed, paper, images_skipped}]
POST /api/projects/{id}/print/drawings-pdf {ids?} → application/pdf
목록 차례 · 장마다 한 쪽 · 머리 X-Aislo-Pages ·
X-Aislo-Skipped-Images(못 넣은 그림 {id: 수} JSON)
로그인 · 회사 · 프로젝트 권한은 이 라우터가 스스로 검(B09 라우터가 include 하든 앱이 바로 붙이든).
저장 안 함 — 매번 만듦 · B07 정본 · 확정 상태 그대로.
PDF = 장마다 SVG 쪽 → 공용 PDF 엔진(Chromium · 브라우저 한 번) → pypdf 로 한 벌.
"""
from __future__ import annotations
import asyncio
import json
import logging
from uuid import UUID
from fastapi import APIRouter, Depends, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from B09_Estimation.draft import B09_Draft_Print_Drawings_Source as source
from B09_Estimation.draft.B09_Draft_Print_Drawings_Pdf import drawing_page
from common_util.common_util_auth import require_company, require_project_access, verify_session
from common_util.common_util_print_pdf import PrintError, merge_pdfs, render_htmls_to_pdfs
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/projects",
tags=["B09 Print Drawings (draft)"],
dependencies=[
Depends(verify_session),
Depends(require_company),
Depends(require_project_access),
],
)
class DrawingsPdfRequest(BaseModel):
ids: list[str] | None = None
def _error(status: int, message: str) -> JSONResponse:
return JSONResponse(status_code=status, content={"status": "error", "message": message})
@router.get("/{project_id}/print/drawings")
async def get_print_drawings(project_id: UUID) -> JSONResponse:
try:
items, _ = await source.drawing_items(project_id)
except Exception:
logger.exception("인쇄 도면 목록 실패: %s", project_id)
return _error(500, "도면 목록을 읽지 못했습니다.")
return JSONResponse(items)
#: PDF 만들기 시간 한도 — 기본 + 장마다(큰 도면 묶음)
TIMEOUT_BASE_S, TIMEOUT_PER_SHEET_S = 60, 10
def _pdf(drawings: list[dict], ids: list[str]) -> tuple[bytes, dict[str, int]]:
jobs: list[dict] = []
skipped: dict[str, int] = {}
for drawing_id, drawing in zip(ids, drawings, strict=True):
page, size, _, missing = drawing_page(drawing)
jobs.append({"html": page, "page_size": size})
if missing:
skipped[drawing_id] = missing
timeout = TIMEOUT_BASE_S + TIMEOUT_PER_SHEET_S * len(jobs)
return merge_pdfs(render_htmls_to_pdfs(jobs, timeout)), skipped
@router.post("/{project_id}/print/drawings-pdf")
async def post_print_drawings_pdf(
project_id: UUID, request: DrawingsPdfRequest | None = None
) -> Response:
try:
items, context = await source.drawing_items(project_id)
by_id = {item["id"]: item for item in items}
ids = request.ids if request and request.ids is not None else list(by_id)
unknown = [one for one in ids if one not in by_id]
if unknown:
return _error(400, f"목록에 없는 도면입니다: {', '.join(unknown)}")
if not ids:
return _error(404, "인쇄할 도면이 없습니다.")
chosen = [by_id[one] for one in ids]
drawings = await source.drawings_in_order(project_id, chosen, context)
body, skipped = await asyncio.to_thread(_pdf, drawings, ids)
except ValueError as exc:
return _error(400, str(exc))
except PrintError as exc:
return _error(503, str(exc))
except Exception:
logger.exception("인쇄 도면 PDF 실패: %s", project_id)
return _error(500, "도면 PDF 를 만들지 못했습니다.")
return Response(
body,
media_type="application/pdf",
headers={
"Content-Disposition": 'inline; filename="drawings.pdf"',
"X-Aislo-Pages": str(len(ids)),
"X-Aislo-Skipped-Images": json.dumps(skipped),
},
)