"use client"; import type { TableRendererProps } from "./types"; import type { ComponentConfig, GridCell } from "@/types/report"; type TableColumn = NonNullable[number]; // ─── 헬퍼 함수 ──────────────────────────────────────────────────────────────── function applyNumberFormat(value: string, format?: string, suffix?: string): string { if (!format || format === "none") return value; const num = parseFloat(value.replace(/,/g, "")); if (isNaN(num)) return value; const formatted = num.toLocaleString("ko-KR"); return format === "currency" ? `${formatted}${suffix ?? "원"}` : formatted; } function getCellValue(col: TableColumn, row: Record): string { const raw = String(row[col.field] ?? ""); return applyNumberFormat(raw, col.numberFormat, col.currencySuffix); } function calcSummary(col: TableColumn, rows: Record[]): string { if (!col.summaryType || col.summaryType === "NONE") return ""; if (col.summaryType === "COUNT") { return applyNumberFormat(String(rows.length), col.numberFormat, col.currencySuffix); } const values = rows .map((row) => { const raw = String(row[col.field] ?? ""); return parseFloat(raw.replace(/,/g, "")); }) .filter((v) => !isNaN(v)); if (values.length === 0) return ""; const sum = values.reduce((a, b) => a + b, 0); const result = col.summaryType === "AVG" ? sum / values.length : sum; return applyNumberFormat( parseFloat(result.toFixed(4)).toString(), col.numberFormat, col.currencySuffix, ); } // ─── 그리드 셀 값 계산 ────────────────────────────────────────────────────── function getGridCellValue( cell: GridCell, row?: Record, ): string { if (cell.cellType === "static") return cell.value ?? ""; if (cell.cellType === "field" && cell.field && row) { const raw = String(row[cell.field] ?? ""); return applyNumberFormat(raw, cell.numberFormat, cell.currencySuffix); } return cell.value ?? ""; } // ─── 그리드 테이블 렌더러 ──────────────────────────────────────────────────── function GridTableRenderer({ component, getQueryResult }: TableRendererProps) { const cells = component.gridCells ?? []; const rowCount = component.gridRowCount ?? 0; const colCount = component.gridColCount ?? 0; const colWidths = component.gridColWidths ?? []; const rowHeights = component.gridRowHeights ?? []; const headerRows = component.gridHeaderRows ?? 1; const headerCols = component.gridHeaderCols ?? 1; if (cells.length === 0 || rowCount === 0 || colCount === 0) { return (
격자 양식을 구성하세요
); } const resultKey = component.visualQuery?.tableName ? `visual_${component.id}` : component.queryId; const queryResult = resultKey ? getQueryResult(resultKey) : null; const dataRow = queryResult?.rows?.[0] as Record | undefined; const totalConfigWidth = colWidths.reduce((a, b) => a + b, 0) || 1; const totalConfigHeight = rowHeights.reduce((a, b) => a + b, 0) || 1; const hdrBg = component.headerBackgroundColor || "#f3f4f6"; const hdrColor = component.headerTextColor || "#111827"; const findCell = (row: number, col: number) => cells.find((c) => c.row === row && c.col === col); const tableRows: React.ReactNode[] = []; for (let r = 0; r < rowCount; r++) { const tds: React.ReactNode[] = []; const rowHPct = ((rowHeights[r] ?? 32) / totalConfigHeight) * 100; for (let c = 0; c < colCount; c++) { const cell = findCell(r, c); if (!cell || cell.merged) continue; const rSpan = cell.rowSpan ?? 1; const cSpan = cell.colSpan ?? 1; const borderW = cell.borderStyle === "medium" ? 2 : cell.borderStyle === "thick" ? 3 : cell.borderStyle === "none" ? 0 : 1; const isHeader = r < headerRows || c < headerCols; const cellBg = cell.backgroundColor || (isHeader ? hdrBg : "white"); const cellColor = cell.textColor || (isHeader ? hdrColor : "#111827"); const displayValue = getGridCellValue(cell, dataRow); tds.push( 1 ? rSpan : undefined} colSpan={cSpan > 1 ? cSpan : undefined} style={{ backgroundColor: cellBg, border: `${borderW}px solid #d1d5db`, padding: "2px 4px", fontSize: cell.fontSize ?? 12, fontWeight: cell.fontWeight === "bold" ? 700 : (isHeader ? 600 : 400), color: cellColor, textAlign: cell.align || "center", verticalAlign: cell.verticalAlign || "middle", overflow: "hidden", whiteSpace: "pre-line", wordBreak: "break-word", }} > {displayValue} , ); } tableRows.push( {tds} , ); } return (
{colWidths.map((w, i) => { const pct = (w / totalConfigWidth) * 100; return ; })} {tableRows}
); } // ─── 기존 테이블 렌더러 ───────────────────────────────────────────────────── function ClassicTableRenderer({ component, getQueryResult }: TableRendererProps) { const resultKey = component.visualQuery?.tableName ? `visual_${component.id}` : component.queryId; const queryResult = resultKey ? getQueryResult(resultKey) : null; const hasData = queryResult && queryResult.rows.length > 0; const hasColumns = component.tableColumns && component.tableColumns.length > 0; if (!hasData && !hasColumns) { return (
테이블을 구성하세요
); } const allColumns = hasColumns ? component.tableColumns! : queryResult!.fields.map((field) => ({ field, header: field, width: undefined as number | undefined, align: "left" as const, visible: true, })); const visibleColumns = allColumns.filter((col) => col.visible !== false); const dataRows = hasData ? queryResult!.rows : []; const previewRowCount = component.tableRowCount ?? 3; const hasSummaryRow = component.showFooter && visibleColumns.some((col) => col.summaryType && col.summaryType !== "NONE"); const borderClass = component.showBorder !== false ? "border border-gray-300" : ""; const rowH = component.rowHeight ?? 28; // 열 너비: 설정된 비율을 유지하면서 컴포넌트 전체 너비에 맞게 스케일 const totalConfigWidth = visibleColumns.reduce((s, c) => s + (c.width || 120), 0); return (
{visibleColumns.map((col, idx) => { const ratio = (col.width || 120) / totalConfigWidth; return ; })} {visibleColumns.map((col, idx) => ( ))} {dataRows.length > 0 ? dataRows.map((row, rowIdx) => ( {visibleColumns.map((col, colIdx) => ( ))} )) : Array.from({ length: previewRowCount }).map((_, rowIdx) => ( {visibleColumns.map((col, colIdx) => ( ))} ))} {hasSummaryRow && dataRows.length > 0 && ( {visibleColumns.map((col, idx) => ( ))} )}
{col.header}
{getCellValue(col, row)}
{col.field ? `{${col.field}}` : "—"}
{calcSummary(col, dataRows)}
); } // ─── 메인 export ───────────────────────────────────────────────────────────── export function TableRenderer(props: TableRendererProps) { if (props.component.gridMode) { return ; } return ; }