"use client"; /** * PivotGrid 설정 패널 - 간소화 버전 * * 피벗 테이블 설정 방법: * 1. 테이블 선택 * 2. 컬럼을 드래그하여 행/열/값 영역에 배치 */ import React, { useState, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { PivotGridComponentConfig, PivotFieldConfig, PivotAreaType, AggregationType, FieldDataType, } from "./types"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Rows, Columns, Calculator, X, Plus, GripVertical, Table2, BarChart3, Settings, ChevronDown, ChevronUp, Info, } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; // ==================== 타입 ==================== interface TableInfo { table_name: string; table_comment?: string; } interface ColumnInfo { column_name: string; data_type: string; column_comment?: string; } interface PivotGridConfigPanelProps { config: PivotGridComponentConfig; onChange: (config: PivotGridComponentConfig) => void; } // DB 타입을 FieldDataType으로 변환 function mapDbTypeToFieldType(dbType: string): FieldDataType { const type = dbType.toLowerCase(); if (type.includes("int") || type.includes("numeric") || type.includes("decimal") || type.includes("float")) { return "number"; } if (type.includes("date") || type.includes("time") || type.includes("timestamp")) { return "date"; } if (type.includes("bool")) { return "boolean"; } return "string"; } // ==================== 컬럼 칩 컴포넌트 ==================== interface ColumnChipProps { column: ColumnInfo; isUsed: boolean; onClick: () => void; } const ColumnChip: React.FC = ({ column, isUsed, onClick }) => { const dataType = mapDbTypeToFieldType(column.data_type); const typeColor = { number: "bg-blue-100 text-blue-700 border-blue-200", string: "bg-green-100 text-green-700 border-green-200", date: "bg-purple-100 text-purple-700 border-purple-200", boolean: "bg-orange-100 text-orange-700 border-orange-200", }[dataType]; return ( ); }; // ==================== 영역 드롭존 컴포넌트 ==================== interface AreaDropZoneProps { area: PivotAreaType; label: string; description: string; icon: React.ReactNode; fields: PivotFieldConfig[]; columns: ColumnInfo[]; onAddField: (column: ColumnInfo) => void; onRemoveField: (index: number) => void; onUpdateField: (index: number, updates: Partial) => void; color: string; } const AreaDropZone: React.FC = ({ area, label, description, icon, fields, columns, onAddField, onRemoveField, onUpdateField, color, }) => { const [isExpanded, setIsExpanded] = useState(true); // 사용 가능한 컬럼 (이미 추가된 컬럼 제외) const availableColumns = columns.filter( (col) => !fields.some((f) => f.field === col.column_name) ); return (
{/* 헤더 */}
setIsExpanded(!isExpanded)} >
{icon} {label} {fields.length}
{isExpanded ? : }
{/* 설명 */}

{description}

{isExpanded && (
{/* 추가된 필드 목록 */} {fields.length > 0 ? (
{fields.map((field, idx) => (
{field.caption || field.field} {/* 데이터 영역일 때 집계 함수 선택 */} {area === "data" && ( )}
))}
) : (
아래에서 컬럼을 선택하세요
)} {/* 컬럼 추가 드롭다운 */} {availableColumns.length > 0 && ( )}
)}
); }; // ==================== 메인 컴포넌트 ==================== export const PivotGridConfigPanel: React.FC = ({ config, onChange, }) => { const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); // 테이블 목록 로드 useEffect(() => { const loadTables = async () => { setLoadingTables(true); try { const tableList = await tableTypeApi.getTables(); const mappedTables: TableInfo[] = tableList.map((t: any) => ({ table_name: t.tableName, table_comment: t.tableLabel || t.displayName || t.tableName, })); setTables(mappedTables); } catch (error) { console.error("테이블 목록 로드 실패:", error); } finally { setLoadingTables(false); } }; loadTables(); }, []); // 테이블 선택 시 컬럼 로드 useEffect(() => { const loadColumns = async () => { if (!config.dataSource?.tableName) { setColumns([]); return; } setLoadingColumns(true); try { const columnList = await tableTypeApi.getColumns(config.dataSource.tableName); const mappedColumns: ColumnInfo[] = columnList.map((c: any) => ({ column_name: c.columnName || c.column_name, data_type: c.dataType || c.data_type || "text", column_comment: c.columnLabel || c.column_label || c.columnName || c.column_name, })); setColumns(mappedColumns); } catch (error) { console.error("컬럼 목록 로드 실패:", error); } finally { setLoadingColumns(false); } }; loadColumns(); }, [config.dataSource?.tableName]); // 설정 업데이트 헬퍼 const updateConfig = useCallback( (updates: Partial) => { onChange({ ...config, ...updates }); }, [config, onChange] ); // 필드 추가 const handleAddField = (area: PivotAreaType, column: ColumnInfo) => { const currentFields = config.fields || []; const areaFields = currentFields.filter(f => f.area === area); const newField: PivotFieldConfig = { field: column.column_name, caption: column.column_comment || column.column_name, area, areaIndex: areaFields.length, dataType: mapDbTypeToFieldType(column.data_type), visible: true, }; if (area === "data") { newField.summaryType = "sum"; } updateConfig({ fields: [...currentFields, newField] }); }; // 필드 제거 const handleRemoveField = (area: PivotAreaType, index: number) => { const currentFields = config.fields || []; const newFields = currentFields.filter( (f) => !(f.area === area && f.areaIndex === index) ); // 인덱스 재정렬 let idx = 0; newFields.forEach((f) => { if (f.area === area) { f.areaIndex = idx++; } }); updateConfig({ fields: newFields }); }; // 필드 업데이트 const handleUpdateField = (area: PivotAreaType, index: number, updates: Partial) => { const currentFields = config.fields || []; const newFields = currentFields.map((f) => { if (f.area === area && f.areaIndex === index) { return { ...f, ...updates }; } return f; }); updateConfig({ fields: newFields }); }; // 영역별 필드 가져오기 const getFieldsByArea = (area: PivotAreaType) => { return (config.fields || []) .filter(f => f.area === area) .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)); }; return (
{/* 사용 가이드 */}

피벗 테이블 설정 방법

  1. 데이터를 가져올 테이블을 선택하세요
  2. 행 그룹에 그룹화할 컬럼을 추가하세요 (예: 지역, 부서)
  3. 열 그룹에 가로로 펼칠 컬럼을 추가하세요 (예: 월, 분기)
  4. 에 집계할 숫자 컬럼을 추가하세요 (예: 매출, 수량)
{/* STEP 1: 테이블 선택 */}
{/* STEP 2: 필드 배치 */} {config.dataSource?.tableName && (
{loadingColumns && (컬럼 로딩 중...)}
{/* 사용 가능한 컬럼 목록 */} {columns.length > 0 && (
{columns.map((col) => { const isUsed = (config.fields || []).some(f => f.field === col.column_name); return ( {/* 클릭 시 아무것도 안함 - 드롭존에서 추가 */}} /> ); })}
)} {/* 영역별 드롭존 */}
} fields={getFieldsByArea("row")} columns={columns} onAddField={(col) => handleAddField("row", col)} onRemoveField={(idx) => handleRemoveField("row", idx)} onUpdateField={(idx, updates) => handleUpdateField("row", idx, updates)} color="border-emerald-200 bg-emerald-50/50" /> } fields={getFieldsByArea("column")} columns={columns} onAddField={(col) => handleAddField("column", col)} onRemoveField={(idx) => handleRemoveField("column", idx)} onUpdateField={(idx, updates) => handleUpdateField("column", idx, updates)} color="border-blue-200 bg-blue-50/50" /> } fields={getFieldsByArea("data")} columns={columns} onAddField={(col) => handleAddField("data", col)} onRemoveField={(idx) => handleRemoveField("data", idx)} onUpdateField={(idx, updates) => handleUpdateField("data", idx, updates)} color="border-amber-200 bg-amber-50/50" />
)} {/* 고급 설정 토글 */}
{/* 고급 설정 */} {showAdvanced && (
{/* 표시 설정 */}
updateConfig({ totals: { ...config.totals, showRowGrandTotals: v } }) } />
updateConfig({ totals: { ...config.totals, showColumnGrandTotals: v } }) } />
updateConfig({ totals: { ...config.totals, showRowTotals: v } }) } />
updateConfig({ totals: { ...config.totals, showColumnTotals: v } }) } />
updateConfig({ style: { ...config.style, alternateRowColors: v } }) } />
updateConfig({ style: { ...config.style, mergeCells: v } }) } />
updateConfig({ exportConfig: { ...config.exportConfig, excel: v } }) } />
updateConfig({ saveState: v }) } />
{/* 크기 설정 */}
updateConfig({ height: e.target.value })} placeholder="400px" className="h-8 text-xs" />
updateConfig({ maxHeight: e.target.value })} placeholder="600px" className="h-8 text-xs" />
{/* 조건부 서식 */}
{(config.style?.conditionalFormats || []).map((rule, index) => (
{rule.type === "colorScale" && (
{ const newFormats = [...(config.style?.conditionalFormats || [])]; newFormats[index] = { ...rule, colorScale: { ...rule.colorScale, minColor: e.target.value, maxColor: rule.colorScale?.maxColor || "#00ff00" } }; updateConfig({ style: { ...config.style, conditionalFormats: newFormats } }); }} className="w-6 h-6 rounded cursor-pointer" title="최소값 색상" /> { const newFormats = [...(config.style?.conditionalFormats || [])]; newFormats[index] = { ...rule, colorScale: { ...rule.colorScale, minColor: rule.colorScale?.minColor || "#ff0000", maxColor: e.target.value } }; updateConfig({ style: { ...config.style, conditionalFormats: newFormats } }); }} className="w-6 h-6 rounded cursor-pointer" title="최대값 색상" />
)} {rule.type === "dataBar" && ( { const newFormats = [...(config.style?.conditionalFormats || [])]; newFormats[index] = { ...rule, dataBar: { color: e.target.value } }; updateConfig({ style: { ...config.style, conditionalFormats: newFormats } }); }} className="w-6 h-6 rounded cursor-pointer" title="바 색상" /> )} {rule.type === "iconSet" && ( )}
))}
)}
); }; export default PivotGridConfigPanel;