"use client"; /** * PivotGrid 설정 패널 * 화면 관리에서 PivotGrid 컴포넌트를 설정하는 UI */ import React, { useState, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { PivotGridComponentConfig, PivotFieldConfig, PivotAreaType, AggregationType, DateGroupInterval, 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 { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Plus, Trash2, GripVertical, Settings2, Rows, Columns, Database, Filter, ChevronUp, ChevronDown, } from "lucide-react"; import { apiClient } from "@/lib/api/client"; // ==================== 타입 ==================== interface TableInfo { table_name: string; table_comment?: string; } interface ColumnInfo { column_name: string; data_type: string; column_comment?: string; is_nullable: string; } interface PivotGridConfigPanelProps { config: PivotGridComponentConfig; onChange: (config: PivotGridComponentConfig) => void; } // ==================== 유틸리티 ==================== const AREA_LABELS: Record = { row: { label: "행 영역", icon: }, column: { label: "열 영역", icon: }, data: { label: "데이터 영역", icon: }, filter: { label: "필터 영역", icon: }, }; const AGGREGATION_OPTIONS: { value: AggregationType; label: string }[] = [ { value: "sum", label: "합계" }, { value: "count", label: "개수" }, { value: "avg", label: "평균" }, { value: "min", label: "최소" }, { value: "max", label: "최대" }, { value: "countDistinct", label: "고유값 개수" }, ]; const DATE_GROUP_OPTIONS: { value: DateGroupInterval; label: string }[] = [ { value: "year", label: "연도" }, { value: "quarter", label: "분기" }, { value: "month", label: "월" }, { value: "week", label: "주" }, { value: "day", label: "일" }, ]; const DATA_TYPE_OPTIONS: { value: FieldDataType; label: string }[] = [ { value: "string", label: "문자열" }, { value: "number", label: "숫자" }, { value: "date", label: "날짜" }, { value: "boolean", label: "부울" }, ]; // DB 타입을 FieldDataType으로 변환 function mapDbTypeToFieldType(dbType: string): FieldDataType { const type = dbType.toLowerCase(); if ( type.includes("int") || type.includes("numeric") || type.includes("decimal") || type.includes("float") || type.includes("double") || type.includes("real") ) { return "number"; } if ( type.includes("date") || type.includes("time") || type.includes("timestamp") ) { return "date"; } if (type.includes("bool")) { return "boolean"; } return "string"; } // ==================== 필드 설정 컴포넌트 ==================== interface FieldConfigItemProps { field: PivotFieldConfig; index: number; onChange: (field: PivotFieldConfig) => void; onRemove: () => void; onMoveUp: () => void; onMoveDown: () => void; isFirst: boolean; isLast: boolean; } const FieldConfigItem: React.FC = ({ field, index, onChange, onRemove, onMoveUp, onMoveDown, isFirst, isLast, }) => { return (
{/* 드래그 핸들 & 순서 버튼 */}
{/* 필드 설정 */}
{/* 필드명 & 라벨 */}
onChange({ ...field, field: e.target.value })} placeholder="column_name" className="h-8 text-xs" />
onChange({ ...field, caption: e.target.value })} placeholder="표시명" className="h-8 text-xs" />
{/* 데이터 타입 & 집계 함수 */}
{field.area === "data" && (
)} {field.dataType === "date" && (field.area === "row" || field.area === "column") && (
)}
{/* 삭제 버튼 */}
); }; // ==================== 영역별 필드 목록 ==================== interface AreaFieldListProps { area: PivotAreaType; fields: PivotFieldConfig[]; allColumns: ColumnInfo[]; onFieldsChange: (fields: PivotFieldConfig[]) => void; } const AreaFieldList: React.FC = ({ area, fields, allColumns, onFieldsChange, }) => { const areaFields = fields.filter((f) => f.area === area); const { label, icon } = AREA_LABELS[area]; const handleAddField = () => { const newField: PivotFieldConfig = { field: "", caption: "", area, areaIndex: areaFields.length, dataType: "string", visible: true, }; if (area === "data") { newField.summaryType = "sum"; } onFieldsChange([...fields, newField]); }; const handleAddFromColumn = (column: ColumnInfo) => { const dataType = mapDbTypeToFieldType(column.data_type); const newField: PivotFieldConfig = { field: column.column_name, caption: column.column_comment || column.column_name, area, areaIndex: areaFields.length, dataType, visible: true, }; if (area === "data") { newField.summaryType = "sum"; } onFieldsChange([...fields, newField]); }; const handleFieldChange = (index: number, updatedField: PivotFieldConfig) => { const newFields = [...fields]; const globalIndex = fields.findIndex( (f) => f.area === area && f.areaIndex === index ); if (globalIndex >= 0) { newFields[globalIndex] = updatedField; onFieldsChange(newFields); } }; const handleRemoveField = (index: number) => { const newFields = fields.filter( (f) => !(f.area === area && f.areaIndex === index) ); // 인덱스 재정렬 let idx = 0; newFields.forEach((f) => { if (f.area === area) { f.areaIndex = idx++; } }); onFieldsChange(newFields); }; const handleMoveField = (fromIndex: number, direction: "up" | "down") => { const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1; if (toIndex < 0 || toIndex >= areaFields.length) return; const newAreaFields = [...areaFields]; const [moved] = newAreaFields.splice(fromIndex, 1); newAreaFields.splice(toIndex, 0, moved); // 인덱스 재정렬 newAreaFields.forEach((f, idx) => { f.areaIndex = idx; }); // 전체 필드 업데이트 const newFields = fields.filter((f) => f.area !== area); onFieldsChange([...newFields, ...newAreaFields]); }; // 이미 추가된 컬럼 제외 const availableColumns = allColumns.filter( (col) => !fields.some((f) => f.field === col.column_name) ); return (
{icon} {label} {areaFields.length}
{/* 필드 목록 */} {areaFields .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)) .map((field, idx) => ( handleFieldChange(field.areaIndex || idx, f)} onRemove={() => handleRemoveField(field.areaIndex || idx)} onMoveUp={() => handleMoveField(idx, "up")} onMoveDown={() => handleMoveField(idx, "down")} isFirst={idx === 0} isLast={idx === areaFields.length - 1} /> ))} {/* 필드 추가 */}
); }; // ==================== 메인 컴포넌트 ==================== 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); // 테이블 목록 로드 useEffect(() => { const loadTables = async () => { setLoadingTables(true); try { const response = await apiClient.get("/api/table-management/list"); if (response.data.success) { setTables(response.data.data || []); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } finally { setLoadingTables(false); } }; loadTables(); }, []); // 테이블 선택 시 컬럼 로드 useEffect(() => { const loadColumns = async () => { if (!config.dataSource?.tableName) { setColumns([]); return; } setLoadingColumns(true); try { const response = await apiClient.get( `/api/table-management/columns/${config.dataSource.tableName}` ); if (response.data.success) { setColumns(response.data.data || []); } } catch (error) { console.error("컬럼 목록 로드 실패:", error); } finally { setLoadingColumns(false); } }; loadColumns(); }, [config.dataSource?.tableName]); // 설정 업데이트 헬퍼 const updateConfig = useCallback( (updates: Partial) => { onChange({ ...config, ...updates }); }, [config, onChange] ); return (
{/* 데이터 소스 설정 */}
{/* 필드 설정 */} {config.dataSource?.tableName && (
{columns.length}개 컬럼
{loadingColumns ? (
컬럼 로딩 중...
) : ( {(["row", "column", "data", "filter"] as PivotAreaType[]).map( (area) => ( updateConfig({ fields })} /> ) )} )}
)} {/* 표시 설정 */}
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, highlightTotals: v }, }) } />
{/* 기능 설정 */}
updateConfig({ allowExpandAll: v }) } />
updateConfig({ exportConfig: { ...config.exportConfig, excel: v }, }) } />
{/* 크기 설정 */}
updateConfig({ height: e.target.value })} placeholder="auto 또는 400px" className="h-8 text-xs" />
updateConfig({ maxHeight: e.target.value })} placeholder="600px" className="h-8 text-xs" />
); }; export default PivotGridConfigPanel;