- 점선 = 미리 자른 실선 조각(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>
108 lines
4.1 KiB
Python
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),
|
|
},
|
|
)
|