"""인쇄 미리보기 도면(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), }, )