Files
Aislo/resources/tester/test_302_2_print_drawings.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

304 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""PLAN 302-2 · 535-2 인쇄 미리보기 도면 PDF — 목록 차례 · 쪽 수 · 쪽 크기 · 한글 · 숨긴 층 · 권한 ·
점선 조각 · 그림 · 빈칸 그대로(PDF 읽기 = pypdf).
임시 폴더 견본만 씀(공용 storage 안 씀) — B07 목록 · 단건 · 표제란 값은 바꿔 끼움.
"""
from __future__ import annotations
import io
import json
from pathlib import Path
from uuid import uuid4
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pypdf import PdfReader
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,
dash_pieces,
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"]) == {}
if response.status_code == 503:
pytest.skip(response.json()["message"]) # 서버에 Edge · Chrome 없음
pages = PdfReader(io.BytesIO(response.content)).pages
assert len(pages) == 5
for page in pages:
assert abs(float(page.mediabox.width) / PT_PER_MM - 840) < 0.5
assert abs(float(page.mediabox.height) / PT_PER_MM - 594) < 0.5
texts = [page.extract_text() for page in pages]
first = texts[0]
assert "가나다임도개설공사" in first and "라마바설계" in first # 표제란 한글 그대로
assert "보이는글" in first
assert "숨은글" not in first and "얼린글" not in first
# 구조물 장 = 얹은 도각 + 장 도번 · 표제란 = 프로젝트 값
assert "구조물그림" in texts[2] and "SD-000001" in texts[2]
assert "SD-000002-2" in texts[4] and "가나다임도개설공사" in texts[4]
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})
if response.status_code == 503:
pytest.skip(response.json()["message"])
pages = PdfReader(io.BytesIO(response.content)).pages
assert len(pages) == 2 and "구조물그림" in pages[0].extract_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
#: 1×1 PNG
PNG = (
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAE"
"hQGAhKmMIQAAAABJRU5ErkJggg=="
)
def test_그림_넣기_못_넣은_수():
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," + PNG),
image("/api/assets/없는그림.png"),
image("data:image/png;base64,@@깨진@@"),
],
"layers": [],
}
page, size, paper, skipped = drawing_page(drawing)
assert skipped == 2 and page.count("<image ") == 1
def test_점선_조각_빈칸_그대로():
"""점선 = 미리 자른 실선 조각(Chromium 이 윤곽으로 안 풂) · 무늬 = 꺾은선 처음부터 이어 셈."""
pieces = dash_pieces([(0, 0), (10, 0), (10, 10)], (6, 3))
assert pieces == [
[(0, 0), (6.0, 0.0)],
[(9.0, 0.0), (10, 0), (10.0, 5.0)],
[(10.0, 8.0), (10, 10)],
]
assert dash_pieces([(0, 0), (1, 0)], (0, 0)) == [[(0, 0), (1, 0)]] # 무늬 없음 = 실선
line = {"type": "Line", "layerId": "a", "lineDash": [6, 3]}
line["shapeData"] = {"startPoint": {"x": 0, "y": 0}, "endPoint": {"x": 100, "y": 0}}
text = _text("축 척", 50, 50, "a")
page, size, paper, _ = drawing_page({"entities": [line, text], "layers": []})
assert "stroke-dasharray" not in page and page.count("M") > 5
assert "축 척" in page and "white-space: pre" in page
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()