Files
Aislo/resources/tester/test_302_2_print_drawings.py
T
eomsangdonandClaude Opus 5.5 f4889352f9 feat(b09-print): 인쇄 미리보기 도면 PDF — B07 도면 + 구조물 조합 도면 · 도각 크기 쪽 · 숨긴 층 뺌 · 벡터 · 한글 (302-2)
- 길 = GET /api/projects/{id}/print/drawings · POST /api/projects/{id}/print/drawings-pdf {ids?} (draft 라우터 · 로그인 · 회사 · 프로젝트 권한 스스로)
- 차례 = B07 도면번호 → 구조물 조합 도면(도번 차례 · 도번마다 한 번 · 더한장)
- B07 = B07 단건 조회 그대로(확정본 · 미확정 조립) · 미확정 횡단 장 구조물 = 화면 TS 를 서버 Node 로(번들 tmp · package.json 안 건드림)
- 쪽 = 도각 양식 실치수(A1 840×594) · 색 = openwebcad 밝은 바탕 규칙 · 글 = 맑은 고딕 심음(subset) · 그림 = data URL 넣음 · 못 넣은 수 = 응답 머리
- 화면 쪽 = fetchDrawingList · fetchDrawingsPdf(약속 꼴) · skippedImagesOf
- requirements = PyMuPDF · ezdxf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-13 08:01:47 +09:00

274 lines
10 KiB
Python

"""PLAN 302-2 인쇄 미리보기 도면 PDF — 목록 차례 · 쪽 수 · 쪽 크기 · 한글 · 숨긴 층 · 권한.
임시 폴더 견본만 씀(공용 storage 안 씀) — B07 목록 · 단건 · 표제란 값은 바꿔 끼움.
"""
from __future__ import annotations
import json
from pathlib import Path
from uuid import uuid4
import pymupdf
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
frame_entities,
usable_bbox,
use_project_templates,
use_title_fields,
)
from B09_Estimation.draft import B09_Draft_Print_Drawings_Router as router_module
from B09_Estimation.draft import B09_Draft_Print_Drawings_Source as source
from B09_Estimation.draft.B09_Draft_Print_Drawings_Pdf import (
PT_PER_MM,
add_drawing_page,
paint_color,
)
from common_util import common_util_auth as auth
TITLE = {"공사명": "가나다임도개설공사", "용역회사": "라마바설계"}
def _text(label: str, x: float, y: float, layer: str) -> dict:
return {
"id": str(uuid4()),
"type": "Text",
"lineColor": "#ffffff",
"lineWidth": 1,
"layerId": layer,
"shapeData": {
"basePoint": {"x": x, "y": y},
"label": label,
"options": {"fontSize": 10, "textAlign": "left", "textColor": "#ffffff"},
},
}
def _b07_drawing(drawing_id: str) -> dict:
"""도각(시스템 A1 · 표제란 = TITLE) + 보이는 글 + 숨긴 층 · 동결 층 글."""
use_project_templates(None)
use_title_fields(TITLE)
entities = frame_entities(drawing_id, usable_bbox(), fit=False, fields={"도면명": "종단면도"})
entities += [
_text("보이는글", 200, 300, "draw"),
_text("숨은글", 200, 320, "hidden"),
_text("얼린글", 200, 340, "frozen"),
]
layers = [
{"id": "draw", "name": "그림", "isVisible": True, "isLocked": False},
{"id": "hidden", "name": "숨김", "isVisible": False, "isLocked": False},
{"id": "frozen", "name": "동결", "isVisible": True, "isLocked": False, "isFrozen": True},
]
return {"format": "openwebcad", "entities": entities, "layers": layers}
def _structure_doc(number: str, more: list[str] = ()) -> dict:
sheet = {
"format": "openwebcad",
"entities": [_text("구조물그림", 300, 300, "기본")],
"layers": [],
}
return {
"도번": number,
"도면": sheet,
"더한장": [{"장id": f"s{i}", "이름": name, "도면": sheet} for i, name in enumerate(more)],
}
@pytest.fixture
def sample(tmp_path, monkeypatch):
"""B07 둘(종단 · 표지) + 구조물 사본 셋(SD-2 둘 = 한 번 · 더한장 하나 · SD-1 하나)."""
from M02_MasterTemplete import M02_Item_Snapshot as snapshot
base = tmp_path / "structures"
copies = {
"pipe@10__a": ("A", _structure_doc("SD-000002", ["상세"])),
"pipe@20__b": ("B", _structure_doc("SD-000002", ["상세"])),
"pipe@30__c": ("C", _structure_doc("SD-000001")),
}
linked = {}
for owner, (folder, doc) in copies.items():
(base / folder).mkdir(parents=True)
(base / folder / "도면.json").write_text(json.dumps(doc, ensure_ascii=False), "utf-8")
linked[owner] = {"항목": {"name": f"구조물{folder}"}, "폴더": folder, "부분": ["도면"]}
monkeypatch.setattr(snapshot, "linked_snapshots", lambda root: linked)
monkeypatch.setattr(snapshot, "structures_dir", lambda root: base)
async def root(_pid):
return tmp_path
async def fields(_pid):
return dict(TITLE)
async def b07_list(_pid):
return 7, [
{
"id": "longitudinal_1",
"kind": "longitudinal",
"label": "종단도(1)",
"confirmed": True,
},
{"id": "cover", "kind": "cover", "label": "표지", "confirmed": False},
]
async def b07_drawing(_pid, drawing_id):
return {"drawing": _b07_drawing(drawing_id), "confirmed": True}
monkeypatch.setattr(source, "project_root", root)
monkeypatch.setattr(source, "title_fields", fields)
monkeypatch.setattr(source, "b07_list", b07_list)
monkeypatch.setattr(source, "b07_drawing", b07_drawing)
return tmp_path
def _client(session: dict | None = None, project_company: int | None = None) -> TestClient:
app = FastAPI()
app.include_router(router_module.router)
if session is not None:
app.dependency_overrides[auth.verify_session] = lambda: session
if project_company is None:
app.dependency_overrides[auth.require_project_access] = lambda: session
return TestClient(app)
SESSION = {"user_id": 1, "company_id": 1, "role": "USER", "is_master": False}
def test_paint_color_theme_짝():
"""openwebcad theme.ts paintColor(밝은 바탕) 손셈.
자동색 검정 · 밝은 노랑 휘도 0.3 으로 · 어두운 색 그대로.
"""
assert paint_color("#ffffff") == paint_color("#FFF") == paint_color("#f5f7fa") == (0, 0, 0)
assert paint_color("#f6d55c") == (89 / 255, 77 / 255, 33 / 255)
assert paint_color("#336699") == (0x33 / 255, 0x66 / 255, 0x99 / 255)
def test_목록_차례_B07_번호_다음_도번_한_번(sample):
pid = uuid4()
body = _client(SESSION).get(f"/api/projects/{pid}/print/drawings").json()
assert [item["id"] for item in body] == [
"b07:longitudinal_1",
"b07:cover",
"structure:SD-000001",
"structure:SD-000002",
"structure:SD-000002-2",
]
assert [item["number"] for item in body][:2] == ["1", "2"]
assert body[3]["label"] == "구조물A" and body[4]["label"] == "구조물A · 상세"
assert {item["paper"] for item in body} == {"A1"}
assert {item["source"] for item in body} == {"b07", "structure"}
def test_PDF_쪽수_크기_한글_숨긴층(sample):
pid = uuid4()
response = _client(SESSION).post(f"/api/projects/{pid}/print/drawings-pdf", json={})
assert response.status_code == 200, response.text
assert response.headers["content-type"] == "application/pdf"
assert response.headers["x-aislo-pages"] == "5"
assert json.loads(response.headers["x-aislo-skipped-images"]) == {}
doc = pymupdf.open(stream=response.content, filetype="pdf")
assert doc.page_count == 5
for page in doc:
assert abs(page.rect.width / PT_PER_MM - 840) < 0.5
assert abs(page.rect.height / PT_PER_MM - 594) < 0.5
first = doc[0].get_text()
assert "가나다임도개설공사" in first and "라마바설계" in first # 표제란 한글 그대로
assert "보이는글" in first
assert "숨은글" not in first and "얼린글" not in first
# 구조물 장 = 얹은 도각 + 장 도번 · 표제란 = 프로젝트 값
assert "구조물그림" in doc[2].get_text() and "SD-000001" in doc[2].get_text()
assert "SD-000002-2" in doc[4].get_text() and "가나다임도개설공사" in doc[4].get_text()
def test_PDF_고른_차례_모르는_id(sample):
pid = uuid4()
client = _client(SESSION)
picked = ["structure:SD-000001", "b07:cover"]
response = client.post(f"/api/projects/{pid}/print/drawings-pdf", json={"ids": picked})
doc = pymupdf.open(stream=response.content, filetype="pdf")
assert doc.page_count == 2 and "구조물그림" in doc[0].get_text()
bad = client.post(f"/api/projects/{pid}/print/drawings-pdf", json={"ids": ["b07:없음"]})
assert bad.status_code == 400
class _FakePool:
"""require_project_access 가 읽는 프로젝트 회사 한 줄."""
def __init__(self, company_id: int):
self.row = (company_id,)
def acquire(self):
pool = self
class _Cursor:
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
async def execute(self, *_):
return None
async def fetchone(self):
return pool.row
class _Connection:
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
def cursor(self):
return _Cursor()
return _Connection()
def test_로그인_회사_권한(sample, monkeypatch):
pid = uuid4()
path = f"/api/projects/{pid}/print/drawings"
assert _client().get(path).status_code == 401 # 쿠키 없음
no_company = {**SESSION, "company_id": None}
assert _client(no_company, project_company=0).get(path).status_code == 403
monkeypatch.setattr(auth, "get_db_pool", lambda: _FakePool(2))
other = _client(SESSION, project_company=2)
assert other.get(path).status_code == 403 # 남의 회사 프로젝트
assert other.post(f"/api/projects/{pid}/print/drawings-pdf", json={}).status_code == 403
monkeypatch.setattr(auth, "get_db_pool", lambda: _FakePool(1))
assert _client(SESSION, project_company=1).get(path).status_code == 200
def test_그림_넣기_못_넣은_수():
png = pymupdf.Pixmap(pymupdf.csRGB, pymupdf.IRect(0, 0, 4, 4), 0).tobytes("png")
import base64
def image(data: str) -> dict:
points = [{"x": 10, "y": 10}, {"x": 50, "y": 10}, {"x": 50, "y": 50}, {"x": 10, "y": 50}]
return {"type": "Image", "layerId": "a", "shapeData": {"points": points, "imageData": data}}
drawing = {
"entities": [
image("data:image/png;base64," + base64.b64encode(png).decode()),
image("/api/assets/없는그림.png"),
],
"layers": [],
}
doc = pymupdf.open()
paper, skipped = add_drawing_page(doc, drawing)
assert skipped == 1 and len(doc[0].get_images()) == 1
def test_미확정_횡단_구조물_Node_번들이_돎():
"""서버 Node(화면 TS 그대로) — 번들 빌드 · 실행 · 배치 없는 장은 그대로."""
entity = _text("서버그림", 0, 0, "draw")
drawings = {"b07:cross_x": {"entities": [entity], "cross_placements": []}}
assert source.place_structures({"cross_sections": []}, drawings) is True
assert drawings["b07:cross_x"]["entities"] == [entity]
assert Path(source.BUNDLE).is_file()