From 7d801c0a2bb3b6ae055144ea048fd93751489e79 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 1 Oct 2025 18:04:38 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B0=94=EC=9D=B8=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/designer/CanvasComponent.tsx | 90 ++++--- .../report/designer/ReportDesignerCanvas.tsx | 9 + .../designer/ReportDesignerRightPanel.tsx | 227 +++++++++++++++++- .../report/designer/ReportPreviewModal.tsx | 84 +++++-- frontend/contexts/ReportDesignerContext.tsx | 44 +--- frontend/types/report.ts | 11 + 6 files changed, 363 insertions(+), 102 deletions(-) diff --git a/frontend/components/report/designer/CanvasComponent.tsx b/frontend/components/report/designer/CanvasComponent.tsx index ce751eec..1b32fe33 100644 --- a/frontend/components/report/designer/CanvasComponent.tsx +++ b/frontend/components/report/designer/CanvasComponent.tsx @@ -256,39 +256,70 @@ export function CanvasComponent({ component }: CanvasComponentProps) { const queryResult = getQueryResult(component.queryId); if (queryResult && queryResult.rows.length > 0) { + // tableColumns가 없으면 자동 생성 + const columns = + component.tableColumns && component.tableColumns.length > 0 + ? component.tableColumns + : queryResult.fields.map((field) => ({ + field, + header: field, + width: undefined, + align: "left" as const, + })); + return (
- 테이블 (디테일 데이터) - ● 연결됨 + 테이블 + ● 연결됨 ({queryResult.rows.length}행)
- +
- - {queryResult.fields.map((field) => ( - + {columns.map((col) => ( + ))} - {queryResult.rows.slice(0, 3).map((row, idx) => ( + {queryResult.rows.map((row, idx) => ( - {queryResult.fields.map((field) => ( - ))} ))} - {queryResult.rows.length > 3 && ( - - - - )}
- {field} +
+ {col.header}
- {String(row[field] ?? "")} + {columns.map((col) => ( + + {String(row[col.field] ?? "")}
- ... 외 {queryResult.rows.length - 3}건 -
@@ -298,24 +329,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) { // 기본 테이블 (데이터 없을 때) return ( -
-
테이블 (디테일 데이터)
- - - - - - - - - - - - - - - -
품목명수량단가
품목11050,000
+
+
테이블
+
+ 쿼리를 연결하세요 +
); diff --git a/frontend/components/report/designer/ReportDesignerCanvas.tsx b/frontend/components/report/designer/ReportDesignerCanvas.tsx index 1e690148..9f37e965 100644 --- a/frontend/components/report/designer/ReportDesignerCanvas.tsx +++ b/frontend/components/report/designer/ReportDesignerCanvas.tsx @@ -119,6 +119,15 @@ export function ReportDesignerCanvas() { borderWidth: 1, borderColor: "#cccccc", }), + // 테이블 전용 + ...(item.componentType === "table" && { + queryId: undefined, + tableColumns: [], + headerBackgroundColor: "#f3f4f6", + headerTextColor: "#111827", + showBorder: true, + rowHeight: 32, + }), }; addComponent(newComponent); diff --git a/frontend/components/report/designer/ReportDesignerRightPanel.tsx b/frontend/components/report/designer/ReportDesignerRightPanel.tsx index 2aa0ecc9..63304bca 100644 --- a/frontend/components/report/designer/ReportDesignerRightPanel.tsx +++ b/frontend/components/report/designer/ReportDesignerRightPanel.tsx @@ -361,6 +361,105 @@ export function ReportDesignerRightPanel() {
+ {/* 테이블 스타일 */} + {selectedComponent.type === "table" && ( + + + 테이블 스타일 + + + {/* 헤더 배경색 */} +
+ +
+ + updateComponent(selectedComponent.id, { + headerBackgroundColor: e.target.value, + }) + } + className="h-8 w-16" + /> + + updateComponent(selectedComponent.id, { + headerBackgroundColor: e.target.value, + }) + } + className="h-8 flex-1 font-mono text-xs" + /> +
+
+ + {/* 헤더 텍스트 색상 */} +
+ +
+ + updateComponent(selectedComponent.id, { + headerTextColor: e.target.value, + }) + } + className="h-8 w-16" + /> + + updateComponent(selectedComponent.id, { + headerTextColor: e.target.value, + }) + } + className="h-8 flex-1 font-mono text-xs" + /> +
+
+ + {/* 테두리 표시 */} +
+ + updateComponent(selectedComponent.id, { + showBorder: e.target.checked, + }) + } + className="h-4 w-4" + /> + +
+ + {/* 행 높이 */} +
+ + + updateComponent(selectedComponent.id, { + rowHeight: parseInt(e.target.value), + }) + } + className="h-8" + /> +
+
+
+ )} + {/* 이미지 속성 */} {selectedComponent.type === "image" && ( @@ -782,11 +881,131 @@ export function ReportDesignerRightPanel() { )} - {/* 테이블 안내 메시지 */} + {/* 테이블 컬럼 설정 */} {selectedComponent.queryId && selectedComponent.type === "table" && ( -
- 테이블은 선택한 쿼리의 모든 필드를 자동으로 표시합니다. -
+ + + 컬럼 설정 + + + + + {selectedComponent.tableColumns && selectedComponent.tableColumns.length > 0 && ( +
+ {selectedComponent.tableColumns.map((col, idx) => ( +
+
+ 컬럼 {idx + 1} + +
+
+
+ + { + const newColumns = [...selectedComponent.tableColumns!]; + newColumns[idx].field = e.target.value; + updateComponent(selectedComponent.id, { + tableColumns: newColumns, + }); + }} + className="h-7 text-xs" + /> +
+
+ + { + const newColumns = [...selectedComponent.tableColumns!]; + newColumns[idx].header = e.target.value; + updateComponent(selectedComponent.id, { + tableColumns: newColumns, + }); + }} + className="h-7 text-xs" + /> +
+
+ + { + const newColumns = [...selectedComponent.tableColumns!]; + newColumns[idx].width = e.target.value + ? parseInt(e.target.value) + : undefined; + updateComponent(selectedComponent.id, { + tableColumns: newColumns, + }); + }} + placeholder="자동" + className="h-7 text-xs" + /> +
+
+ + +
+
+
+ ))} +
+ )} +
+
)} {/* 기본값 (텍스트/라벨만) */} diff --git a/frontend/components/report/designer/ReportPreviewModal.tsx b/frontend/components/report/designer/ReportPreviewModal.tsx index b15e60ca..8f39cc4f 100644 --- a/frontend/components/report/designer/ReportPreviewModal.tsx +++ b/frontend/components/report/designer/ReportPreviewModal.tsx @@ -297,28 +297,70 @@ export function ReportPreviewModal({ isOpen, onClose }: ReportPreviewModalProps) )} {component.type === "table" && queryResult && queryResult.rows.length > 0 ? ( - - - - {queryResult.fields.map((field) => ( - - ))} - - - - {queryResult.rows.map((row, idx) => ( - - {queryResult.fields.map((field) => ( - + (() => { + // tableColumns가 없으면 자동 생성 + const columns = + component.tableColumns && component.tableColumns.length > 0 + ? component.tableColumns + : queryResult.fields.map((field) => ({ + field, + header: field, + align: "left" as const, + })); + + return ( +
- {field} -
- {String(row[field] ?? "")} -
+ + + {columns.map((col) => ( + + ))} + + + + {queryResult.rows.map((row, idx) => ( + + {columns.map((col) => ( + + ))} + ))} - - ))} - -
+ {col.header} +
+ {String(row[col.field] ?? "")} +
+ + + ); + })() ) : component.type === "table" ? (
쿼리를 실행해주세요
) : null} diff --git a/frontend/contexts/ReportDesignerContext.tsx b/frontend/contexts/ReportDesignerContext.tsx index 7b577e84..7d0af59a 100644 --- a/frontend/contexts/ReportDesignerContext.tsx +++ b/frontend/contexts/ReportDesignerContext.tsx @@ -97,22 +97,7 @@ function getTemplateLayout(templateId: string): TemplateLayout | null { zIndex: 1, }, ], - queries: [ - { - id: `query-${Date.now()}-1`, - name: "발주 헤더", - type: "MASTER", - sqlQuery: "SELECT order_no, order_date, supplier_name FROM orders WHERE order_no = $1", - parameters: ["$1"], - }, - { - id: `query-${Date.now()}-2`, - name: "발주 품목", - type: "DETAIL", - sqlQuery: "SELECT item_name, quantity, unit_price FROM order_items WHERE order_no = $1", - parameters: ["$1"], - }, - ], + queries: [], }; case "invoice": @@ -191,22 +176,7 @@ function getTemplateLayout(templateId: string): TemplateLayout | null { defaultValue: "합계: 0원", }, ], - queries: [ - { - id: `query-${Date.now()}-1`, - name: "청구 헤더", - type: "MASTER", - sqlQuery: "SELECT invoice_no, invoice_date, customer_name FROM invoices WHERE invoice_no = $1", - parameters: ["$1"], - }, - { - id: `query-${Date.now()}-2`, - name: "청구 항목", - type: "DETAIL", - sqlQuery: "SELECT description, quantity, unit_price, amount FROM invoice_items WHERE invoice_no = $1", - parameters: ["$1"], - }, - ], + queries: [], }; case "basic": @@ -243,15 +213,7 @@ function getTemplateLayout(templateId: string): TemplateLayout | null { defaultValue: "내용을 입력하세요", }, ], - queries: [ - { - id: `query-${Date.now()}-1`, - name: "기본 쿼리", - type: "MASTER", - sqlQuery: "SELECT * FROM table_name WHERE id = $1", - parameters: ["$1"], - }, - ], + queries: [], }; default: diff --git a/frontend/types/report.ts b/frontend/types/report.ts index 116ab9a9..e9f7b940 100644 --- a/frontend/types/report.ts +++ b/frontend/types/report.ts @@ -123,6 +123,17 @@ export interface ComponentConfig { labelPosition?: "top" | "left" | "bottom" | "right"; // 레이블 위치 showUnderline?: boolean; // 서명란 밑줄 표시 여부 personName?: string; // 도장란 이름 (예: "홍길동") + // 테이블 전용 + tableColumns?: Array<{ + field: string; // 필드명 + header: string; // 헤더 표시명 + width?: number; // 컬럼 너비 (px) + align?: "left" | "center" | "right"; // 정렬 + }>; + headerBackgroundColor?: string; // 헤더 배경색 + headerTextColor?: string; // 헤더 텍스트 색상 + showBorder?: boolean; // 테두리 표시 + rowHeight?: number; // 행 높이 (px) } // 리포트 상세