"""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()