From c9c416d6fd65e52ef4360b63a46d5184de755c78 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 2 Oct 2025 13:44:16 +0900 Subject: [PATCH] =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/리포트_페이지_관리_시스템_설계.md | 388 ++++++++++++ .../admin/report/designer/[reportId]/page.tsx | 8 +- .../report/designer/PageListPanel.tsx | 240 +++++++ .../report/designer/ReportDesignerCanvas.tsx | 18 +- .../designer/ReportDesignerRightPanel.tsx | 315 ++++++++- .../report/designer/ReportPreviewModal.tsx | 596 +++++++++--------- frontend/contexts/ReportDesignerContext.tsx | 420 ++++++++++-- frontend/types/report.ts | 43 +- 8 files changed, 1666 insertions(+), 362 deletions(-) create mode 100644 docs/리포트_페이지_관리_시스템_설계.md create mode 100644 frontend/components/report/designer/PageListPanel.tsx diff --git a/docs/리포트_페이지_관리_시스템_설계.md b/docs/리포트_페이지_관리_시스템_설계.md new file mode 100644 index 00000000..cad99adf --- /dev/null +++ b/docs/리포트_페이지_관리_시스템_설계.md @@ -0,0 +1,388 @@ +# 리포트 페이지 관리 시스템 설계 + +## 1. 개요 + +리포트 디자이너에 다중 페이지 관리 기능을 추가하여 여러 페이지에 걸친 복잡한 문서를 작성할 수 있도록 합니다. + +## 2. 주요 기능 + +### 2.1 페이지 관리 + +- 페이지 추가/삭제 +- 페이지 복사 +- 페이지 순서 변경 (드래그 앤 드롭) +- 페이지 이름 지정 + +### 2.2 페이지 네비게이션 + +- 좌측 페이지 썸네일 패널 +- 페이지 간 전환 (클릭) +- 이전/다음 페이지 이동 +- 페이지 번호 표시 + +### 2.3 페이지별 설정 + +- 페이지 크기 (A4, A3, Letter, 사용자 정의) +- 페이지 방향 (세로/가로) +- 여백 설정 +- 배경색 + +### 2.4 컴포넌트 관리 + +- 컴포넌트는 특정 페이지에 속함 +- 페이지 간 컴포넌트 복사/이동 +- 현재 페이지의 컴포넌트만 표시 + +## 3. 데이터베이스 스키마 + +### 3.1 기존 구조 활용 (변경 없음) + +**report_layout 테이블의 layout_config (JSONB) 활용** + +기존: + +```json +{ + "width": 210, + "height": 297, + "orientation": "portrait", + "components": [...] +} +``` + +변경 후: + +```json +{ + "pages": [ + { + "page_id": "page-uuid-1", + "page_name": "표지", + "page_order": 0, + "width": 210, + "height": 297, + "orientation": "portrait", + "margins": { + "top": 20, + "bottom": 20, + "left": 20, + "right": 20 + }, + "background_color": "#ffffff", + "components": [ + { + "id": "comp-1", + "type": "text", + "x": 100, + "y": 50, + ... + } + ] + }, + { + "page_id": "page-uuid-2", + "page_name": "본문", + "page_order": 1, + "width": 210, + "height": 297, + "orientation": "portrait", + "margins": { "top": 20, "bottom": 20, "left": 20, "right": 20 }, + "background_color": "#ffffff", + "components": [...] + } + ] +} +``` + +### 3.2 마이그레이션 전략 + +기존 단일 페이지 리포트 자동 변환: + +```typescript +// 기존 구조 감지 시 +if (layoutConfig.components && !layoutConfig.pages) { + // 자동으로 pages 구조로 변환 + layoutConfig = { + pages: [ + { + page_id: uuidv4(), + page_name: "페이지 1", + page_order: 0, + width: layoutConfig.width || 210, + height: layoutConfig.height || 297, + orientation: layoutConfig.orientation || "portrait", + margins: { top: 20, bottom: 20, left: 20, right: 20 }, + background_color: "#ffffff", + components: layoutConfig.components, + }, + ], + }; +} +``` + +## 4. 프론트엔드 구조 + +### 4.1 타입 정의 (types/report.ts) + +```typescript +export interface ReportPage { + page_id: string; + report_id: string; + page_order: number; + page_name: string; + + // 페이지 설정 + width: number; + height: number; + orientation: 'portrait' | 'landscape'; + + // 여백 + margin_top: number; + margin_bottom: number; + margin_left: number; + margin_right: number; + + // 배경 + background_color: string; + + created_at?: string; + updated_at?: string; +} + +export interface ComponentConfig { + id: string; + // page_id 불필요 (페이지의 components 배열에 포함됨) + type: 'text' | 'label' | 'image' | 'table' | ...; + x: number; + y: number; + width: number; + height: number; + // ... 기타 속성 +} + +export interface ReportLayoutConfig { + pages: ReportPage[]; +} +``` + +### 4.2 Context 구조 변경 + +```typescript +interface ReportDesignerContextType { + // 페이지 관리 + pages: ReportPage[]; + currentPageId: string | null; + currentPage: ReportPage | null; + + addPage: () => void; + deletePage: (pageId: string) => void; + duplicatePage: (pageId: string) => void; + reorderPages: (sourceIndex: number, targetIndex: number) => void; + selectPage: (pageId: string) => void; + updatePage: (pageId: string, updates: Partial) => void; + + // 컴포넌트 (현재 페이지만) + currentPageComponents: ComponentConfig[]; + + // ... 기존 기능들 +} +``` + +### 4.3 UI 구조 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ReportDesignerToolbar (저장, 미리보기, 페이지 추가 등) │ +├──────────┬────────────────────────────────────┬─────────────┤ +│ │ │ │ +│ PageList │ ReportDesignerCanvas │ Right │ +│ (좌측) │ (현재 페이지만 표시) │ Panel │ +│ │ │ (속성) │ +│ - Page 1 │ ┌──────────────────────────┐ │ │ +│ - Page 2 │ │ │ │ │ +│ * Page 3 │ │ [컴포넌트들] │ │ │ +│ (현재) │ │ │ │ │ +│ │ └──────────────────────────┘ │ │ +│ [+ 추가] │ │ │ +│ │ 이전 | 다음 (페이지 네비게이션) │ │ +└──────────┴────────────────────────────────────┴─────────────┘ +``` + +## 5. 컴포넌트 구조 + +### 5.1 새 컴포넌트 + +#### PageListPanel.tsx + +```typescript +- 좌측 페이지 목록 패널 +- 페이지 썸네일 표시 +- 드래그 앤 드롭으로 순서 변경 +- 페이지 추가/삭제/복사 버튼 +- 현재 페이지 하이라이트 +``` + +#### PageNavigator.tsx + +```typescript +- 캔버스 하단의 페이지 네비게이션 +- 이전/다음 버튼 +- 현재 페이지 번호 표시 +- 페이지 점프 (1/5 형식) +``` + +#### PageSettingsPanel.tsx + +```typescript +- 우측 패널 내 페이지 설정 섹션 +- 페이지 크기, 방향 +- 여백 설정 +- 배경색 +``` + +### 5.2 수정할 컴포넌트 + +#### ReportDesignerContext.tsx + +- pages 상태 추가 +- currentPageId 상태 추가 +- 페이지 관리 함수들 추가 +- components를 currentPageComponents로 필터링 + +#### ReportDesignerCanvas.tsx + +- currentPageComponents만 렌더링 +- 캔버스 크기를 currentPage 기준으로 설정 +- 컴포넌트 추가 시 page_id 포함 + +#### ReportDesignerToolbar.tsx + +- "페이지 추가" 버튼 추가 +- 저장 시 pages도 함께 저장 + +#### ReportPreviewModal.tsx + +- 모든 페이지 순서대로 미리보기 +- 페이지 구분선 표시 +- PDF 저장 시 모든 페이지 포함 + +## 6. API 엔드포인트 + +### 6.1 페이지 관리 + +```typescript +// 페이지 목록 조회 +GET /api/report/:reportId/pages +Response: { pages: ReportPage[] } + +// 페이지 생성 +POST /api/report/:reportId/pages +Body: { page_name, width, height, orientation, margins } +Response: { page: ReportPage } + +// 페이지 수정 +PUT /api/report/pages/:pageId +Body: Partial +Response: { page: ReportPage } + +// 페이지 삭제 +DELETE /api/report/pages/:pageId +Response: { success: boolean } + +// 페이지 순서 변경 +PUT /api/report/:reportId/pages/reorder +Body: { pageOrders: Array<{ page_id, page_order }> } +Response: { success: boolean } + +// 페이지 복사 +POST /api/report/pages/:pageId/duplicate +Response: { page: ReportPage } +``` + +### 6.2 레이아웃 (기존 수정) + +```typescript +// 레이아웃 저장 (페이지별) +PUT /api/report/:reportId/layout +Body: { + pages: ReportPage[], + components: ComponentConfig[] // page_id 포함 +} +``` + +## 7. 구현 단계 + +### Phase 1: DB 및 백엔드 (0.5일) + +1. ✅ DB 스키마 생성 +2. ✅ API 엔드포인트 구현 +3. ✅ 기존 리포트 마이그레이션 (단일 페이지 생성) + +### Phase 2: 타입 및 Context (0.5일) + +1. ✅ 타입 정의 업데이트 +2. ✅ Context에 페이지 상태/함수 추가 +3. ✅ API 연동 + +### Phase 3: UI 컴포넌트 (1일) + +1. ✅ PageListPanel 구현 +2. ✅ PageNavigator 구현 +3. ✅ PageSettingsPanel 구현 + +### Phase 4: 통합 및 수정 (1일) + +1. ✅ Canvas에서 현재 페이지만 표시 +2. ✅ 컴포넌트 추가/수정 시 page_id 처리 +3. ✅ 미리보기에서 모든 페이지 표시 +4. ✅ PDF/WORD 저장에서 모든 페이지 처리 + +### Phase 5: 테스트 및 최적화 (0.5일) + +1. ✅ 페이지 전환 성능 확인 +2. ✅ 썸네일 렌더링 최적화 +3. ✅ 버그 수정 + +**총 예상 기간: 3-4일** + +## 8. 주의사항 + +### 8.1 성능 최적화 + +- 페이지 썸네일은 저해상도로 렌더링 +- 현재 페이지 컴포넌트만 DOM에 유지 +- 페이지 전환 시 애니메이션 최소화 + +### 8.2 호환성 + +- 기존 리포트는 자동으로 단일 페이지로 마이그레이션 +- 템플릿도 페이지 구조 포함 + +### 8.3 사용자 경험 + +- 페이지 삭제 시 확인 다이얼로그 +- 컴포넌트가 있는 페이지 삭제 시 경고 +- 페이지 순서 변경 시 즉시 반영 + +## 9. 추후 확장 기능 + +### 9.1 페이지 템플릿 + +- 자주 사용하는 페이지 레이아웃 저장 +- 페이지 추가 시 템플릿 선택 + +### 9.2 마스터 페이지 + +- 모든 페이지에 공통으로 적용되는 헤더/푸터 +- 페이지 번호 자동 삽입 + +### 9.3 페이지 연결 + +- 테이블 데이터가 여러 페이지에 자동 분할 +- 페이지 오버플로우 처리 + +## 10. 참고 자료 + +- 오즈리포트 메뉴얼 +- Crystal Reports 페이지 관리 +- Adobe InDesign 페이지 시스템 diff --git a/frontend/app/(main)/admin/report/designer/[reportId]/page.tsx b/frontend/app/(main)/admin/report/designer/[reportId]/page.tsx index f4fce164..03d5bcd9 100644 --- a/frontend/app/(main)/admin/report/designer/[reportId]/page.tsx +++ b/frontend/app/(main)/admin/report/designer/[reportId]/page.tsx @@ -5,6 +5,7 @@ import { useParams, useRouter } from "next/navigation"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { ReportDesignerToolbar } from "@/components/report/designer/ReportDesignerToolbar"; +import { PageListPanel } from "@/components/report/designer/PageListPanel"; import { ReportDesignerLeftPanel } from "@/components/report/designer/ReportDesignerLeftPanel"; import { ReportDesignerCanvas } from "@/components/report/designer/ReportDesignerCanvas"; import { ReportDesignerRightPanel } from "@/components/report/designer/ReportDesignerRightPanel"; @@ -72,13 +73,16 @@ export default function ReportDesignerPage() { {/* 메인 영역 */}
- {/* 좌측 패널 */} + {/* 페이지 목록 패널 */} + + + {/* 좌측 패널 (템플릿, 컴포넌트) */} {/* 중앙 캔버스 */} - {/* 우측 패널 */} + {/* 우측 패널 (속성) */}
diff --git a/frontend/components/report/designer/PageListPanel.tsx b/frontend/components/report/designer/PageListPanel.tsx new file mode 100644 index 00000000..dbe2cb71 --- /dev/null +++ b/frontend/components/report/designer/PageListPanel.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useState } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useReportDesigner } from "@/contexts/ReportDesignerContext"; +import { Plus, Copy, Trash2, GripVertical, Edit2, Check, X } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function PageListPanel() { + const { + layoutConfig, + currentPageId, + addPage, + deletePage, + duplicatePage, + reorderPages, + selectPage, + updatePageSettings, + } = useReportDesigner(); + + const [editingPageId, setEditingPageId] = useState(null); + const [editingName, setEditingName] = useState(""); + const [draggedIndex, setDraggedIndex] = useState(null); + + const handleStartEdit = (pageId: string, currentName: string) => { + setEditingPageId(pageId); + setEditingName(currentName); + }; + + const handleSaveEdit = () => { + if (editingPageId && editingName.trim()) { + updatePageSettings(editingPageId, { page_name: editingName.trim() }); + } + setEditingPageId(null); + setEditingName(""); + }; + + const handleCancelEdit = () => { + setEditingPageId(null); + setEditingName(""); + }; + + const handleDragStart = (index: number) => { + setDraggedIndex(index); + }; + + const handleDragOver = (e: React.DragEvent, index: number) => { + e.preventDefault(); + if (draggedIndex === null || draggedIndex === index) return; + + // 실시간으로 순서 변경하지 않고, drop 시에만 변경 + }; + + const handleDrop = (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); + if (draggedIndex === null) return; + + const sourceIndex = draggedIndex; + if (sourceIndex !== targetIndex) { + reorderPages(sourceIndex, targetIndex); + } + + setDraggedIndex(null); + }; + + const handleDragEnd = () => { + setDraggedIndex(null); + }; + + return ( +
+ {/* 헤더 */} +
+

페이지 목록

+ +
+ + {/* 페이지 목록 */} + +
+ {layoutConfig.pages + .sort((a, b) => a.page_order - b.page_order) + .map((page, index) => ( + handleDragStart(index)} + onDragOver={(e) => handleDragOver(e, index)} + onDrop={(e) => handleDrop(e, index)} + onDragEnd={handleDragEnd} + className={`group relative cursor-pointer transition-all hover:shadow-md ${ + page.page_id === currentPageId + ? "border-primary bg-primary/5 ring-primary/20 ring-2" + : "border-border hover:border-primary/50" + } ${draggedIndex === index ? "opacity-50" : ""}`} + onClick={() => selectPage(page.page_id)} + > +
+ {/* 드래그 핸들 & 페이지 정보 */} +
+
+ +
+ +
+ {/* 페이지 이름 편집 */} + {editingPageId === page.page_id ? ( +
e.stopPropagation()}> + setEditingName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSaveEdit(); + if (e.key === "Escape") handleCancelEdit(); + }} + className="h-6 text-sm" + autoFocus + /> + + +
+ ) : ( +
+ {page.page_name} + +
+ )} + + {/* 페이지 정보 */} +
+ + {page.width} x {page.height}mm + + + {page.components.length}개 컴포넌트 +
+
+ + {/* 액션 메뉴 */} + + e.stopPropagation()}> + + + + { + e.stopPropagation(); + duplicatePage(page.page_id); + }} + > + + 복제 + + { + e.stopPropagation(); + deletePage(page.page_id); + }} + disabled={layoutConfig.pages.length <= 1} + className="text-destructive focus:text-destructive" + > + + 삭제 + + + +
+ + {/* 썸네일 (간단한 미리보기) */} +
+
+
+ {/* 간단한 컴포넌트 표시 */} + {page.components.slice(0, 10).map((comp) => ( +
+ ))} +
+
+
+
+ + ))} +
+ + + {/* 푸터 */} +
+ +
+
+ ); +} diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index dad87741..b33e9dd4 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -11,6 +11,8 @@ import { v4 as uuidv4 } from "uuid"; export function ReportDesignerCanvas() { const canvasRef = useRef(null); const { + currentPageId, + currentPage, components, addComponent, updateComponent, @@ -259,10 +261,24 @@ export function ReportDesignerCanvas() { redo, ]); + // 페이지가 없는 경우 + if (!currentPageId || !currentPage) { + return ( +
+
+

페이지가 없습니다

+

좌측에서 페이지를 추가하세요.

+
+
+ ); + } + return (
{/* 작업 영역 제목 */} -
작업 영역
+
+ {currentPage.page_name} ({currentPage.width} x {currentPage.height}mm) +
{/* 캔버스 스크롤 영역 */}
diff --git a/frontend/components/report/designer/ReportDesignerRightPanel.tsx b/frontend/components/report/designer/ReportDesignerRightPanel.tsx index b3fea2cb..6a17b20b 100644 --- a/frontend/components/report/designer/ReportDesignerRightPanel.tsx +++ b/frontend/components/report/designer/ReportDesignerRightPanel.tsx @@ -17,7 +17,16 @@ import { useToast } from "@/hooks/use-toast"; export function ReportDesignerRightPanel() { const context = useReportDesigner(); - const { selectedComponentId, components, updateComponent, removeComponent, queries } = context; + const { + selectedComponentId, + components, + updateComponent, + removeComponent, + queries, + currentPage, + currentPageId, + updatePageSettings, + } = context; const [activeTab, setActiveTab] = useState("properties"); const [uploadingImage, setUploadingImage] = useState(false); const fileInputRef = useRef(null); @@ -91,13 +100,17 @@ export function ReportDesignerRightPanel() {
- - - + + + + 페이지 + + + 속성 - - + + 쿼리 @@ -1114,6 +1127,296 @@ export function ReportDesignerRightPanel() { + {/* 페이지 설정 탭 */} + + +
+ {currentPage && currentPageId ? ( + <> + {/* 페이지 정보 */} + + + 페이지 정보 + + +
+ + + updatePageSettings(currentPageId, { + page_name: e.target.value, + }) + } + className="mt-1" + /> +
+
+
+ + {/* 페이지 크기 */} + + + 페이지 크기 + + +
+
+ + + updatePageSettings(currentPageId, { + width: Number(e.target.value), + }) + } + className="mt-1" + /> +
+
+ + + updatePageSettings(currentPageId, { + height: Number(e.target.value), + }) + } + className="mt-1" + /> +
+
+ +
+ + +
+ + {/* 프리셋 버튼 */} +
+ + +
+
+
+ + {/* 여백 설정 */} + + + 여백 (mm) + + +
+
+ + + updatePageSettings(currentPageId, { + margins: { + ...currentPage.margins, + top: Number(e.target.value), + }, + }) + } + className="mt-1" + /> +
+
+ + + updatePageSettings(currentPageId, { + margins: { + ...currentPage.margins, + bottom: Number(e.target.value), + }, + }) + } + className="mt-1" + /> +
+
+ + + updatePageSettings(currentPageId, { + margins: { + ...currentPage.margins, + left: Number(e.target.value), + }, + }) + } + className="mt-1" + /> +
+
+ + + updatePageSettings(currentPageId, { + margins: { + ...currentPage.margins, + right: Number(e.target.value), + }, + }) + } + className="mt-1" + /> +
+
+ + {/* 여백 프리셋 */} +
+ + + +
+
+
+ + {/* 배경색 */} + + + 배경 + + +
+ +
+ + updatePageSettings(currentPageId, { + background_color: e.target.value, + }) + } + className="h-10 w-20" + /> + + updatePageSettings(currentPageId, { + background_color: e.target.value, + }) + } + className="flex-1" + placeholder="#ffffff" + /> +
+
+ + {/* 배경색 프리셋 */} +
+ {["#ffffff", "#f3f4f6", "#e5e7eb", "#d1d5db"].map((color) => ( +
+
+
+ + ) : ( +
+

페이지를 선택하세요

+
+ )} +
+
+
+ {/* 쿼리 탭 */} diff --git a/frontend/components/report/designer/ReportPreviewModal.tsx b/frontend/components/report/designer/ReportPreviewModal.tsx index f058e513..57633397 100644 --- a/frontend/components/report/designer/ReportPreviewModal.tsx +++ b/frontend/components/report/designer/ReportPreviewModal.tsx @@ -22,7 +22,7 @@ interface ReportPreviewModalProps { } export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) { - const { components, canvasWidth, canvasHeight, getQueryResult, reportDetail } = useReportDesigner(); + const { layoutConfig, getQueryResult, reportDetail } = useReportDesigner(); const [isExporting, setIsExporting] = useState(false); const { toast } = useToast(); @@ -53,10 +53,14 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) printWindow.print(); }; - // HTML 생성 (인쇄/PDF용) - const generatePrintHTML = (): string => { - // 컴포넌트별 HTML 생성 - const componentsHTML = components + // 페이지별 컴포넌트 HTML 생성 + const generatePageHTML = ( + pageComponents: any[], + pageWidth: number, + pageHeight: number, + backgroundColor: string, + ): string => { + const componentsHTML = pageComponents .map((component) => { const queryResult = component.queryId ? getQueryResult(component.queryId) : null; let content = ""; @@ -152,7 +156,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) .map( (row) => ` - ${columns.map((col) => `${String(row[col.field] ?? "")}`).join("")} + ${columns.map((col: { field: string; align?: string }) => `${String(row[col.field] ?? "")}`).join("")} `, ) @@ -162,7 +166,7 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) - ${columns.map((col) => ``).join("")} + ${columns.map((col: { header: string; align?: string; width?: number }) => ``).join("")} @@ -178,6 +182,19 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) }) .join(""); + return ` +
+ ${componentsHTML} +
`; + }; + + // 모든 페이지 HTML 생성 (인쇄/PDF용) + const generatePrintHTML = (): string => { + const pagesHTML = layoutConfig.pages + .sort((a, b) => a.page_order - b.page_order) + .map((page) => generatePageHTML(page.components, page.width, page.height, page.background_color)) + .join('
'); + return ` @@ -191,7 +208,8 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) } @media print { body { margin: 0; padding: 0; } - .print-container { page-break-inside: avoid; } + .print-page { page-break-after: always; page-break-inside: avoid; } + .print-page:last-child { page-break-after: auto; } } body { font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; @@ -200,19 +218,10 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) -webkit-print-color-adjust: exact; print-color-adjust: exact; } - .print-container { - position: relative; - width: ${canvasWidth}mm; - min-height: ${canvasHeight}mm; - background-color: white; - margin: 0 auto; - } - + ${pagesHTML}
${col.header}${col.header}