"use client"; import React, { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Trash2, ChevronDown, Check } from "lucide-react"; import { RepeaterColumnConfig } from "./types"; import { cn } from "@/lib/utils"; interface RepeaterTableProps { columns: RepeaterColumnConfig[]; data: any[]; onDataChange: (newData: any[]) => void; onRowChange: (index: number, newRow: any) => void; onRowDelete: (index: number) => void; // 동적 데이터 소스 관련 activeDataSources?: Record; // 컬럼별 현재 활성화된 데이터 소스 ID onDataSourceChange?: (columnField: string, optionId: string) => void; // 데이터 소스 변경 콜백 } export function RepeaterTable({ columns, data, onDataChange, onRowChange, onRowDelete, activeDataSources = {}, onDataSourceChange, }: RepeaterTableProps) { const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string; } | null>(null); // 동적 데이터 소스 Popover 열림 상태 const [openPopover, setOpenPopover] = useState(null); // 컬럼 너비 상태 관리 const [columnWidths, setColumnWidths] = useState>(() => { const widths: Record = {}; columns.forEach((col) => { widths[col.field] = col.width ? parseInt(col.width) : 120; }); return widths; }); // 기본 너비 저장 (리셋용) const defaultWidths = React.useMemo(() => { const widths: Record = {}; columns.forEach((col) => { widths[col.field] = col.width ? parseInt(col.width) : 120; }); return widths; }, [columns]); // 리사이즈 상태 const [resizing, setResizing] = useState<{ field: string; startX: number; startWidth: number } | null>(null); // 리사이즈 핸들러 const handleMouseDown = (e: React.MouseEvent, field: string) => { e.preventDefault(); setResizing({ field, startX: e.clientX, startWidth: columnWidths[field] || 120, }); }; // 더블클릭으로 기본 너비로 리셋 const handleDoubleClick = (field: string) => { setColumnWidths((prev) => ({ ...prev, [field]: defaultWidths[field] || 120, })); }; useEffect(() => { if (!resizing) return; const handleMouseMove = (e: MouseEvent) => { if (!resizing) return; const diff = e.clientX - resizing.startX; const newWidth = Math.max(60, resizing.startWidth + diff); setColumnWidths((prev) => ({ ...prev, [resizing.field]: newWidth, })); }; const handleMouseUp = () => { setResizing(null); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [resizing, columns, data]); // 데이터 변경 감지 (필요시 활성화) // useEffect(() => { // console.log("📊 RepeaterTable 데이터 업데이트:", data.length, "개 행"); // }, [data]); const handleCellEdit = (rowIndex: number, field: string, value: any) => { const newRow = { ...data[rowIndex], [field]: value }; onRowChange(rowIndex, newRow); }; const renderCell = ( row: any, column: RepeaterColumnConfig, rowIndex: number ) => { const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.field === column.field; const value = row[column.field]; // 계산 필드는 편집 불가 if (column.calculated || !column.editable) { return (
{column.type === "number" ? typeof value === "number" ? value.toLocaleString() : value || "0" : value || "-"}
); } // 편집 가능한 필드 switch (column.type) { case "number": return ( handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0) } className="h-8 text-xs border-gray-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-none" /> ); case "date": // ISO 형식(2025-11-23T00:00:00.000Z)을 yyyy-mm-dd로 변환 const formatDateValue = (val: any): string => { if (!val) return ""; // 이미 yyyy-mm-dd 형식이면 그대로 반환 if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) { return val; } // ISO 형식이면 날짜 부분만 추출 if (typeof val === "string" && val.includes("T")) { return val.split("T")[0]; } // Date 객체이면 변환 if (val instanceof Date) { return val.toISOString().split("T")[0]; } return String(val); }; return ( handleCellEdit(rowIndex, column.field, e.target.value)} className="h-8 text-xs border-gray-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-none" /> ); case "select": return ( ); default: // text return ( handleCellEdit(rowIndex, column.field, e.target.value)} className="h-8 text-xs border-gray-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 rounded-none" /> ); } }; return (
{columns.map((col) => { const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0; const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId; const activeOption = hasDynamicSource ? col.dynamicDataSource!.options.find(opt => opt.id === activeOptionId) || col.dynamicDataSource!.options[0] : null; return ( ); })} {data.length === 0 ? ( ) : ( data.map((row, rowIndex) => ( {columns.map((col) => ( ))} )) )}
# handleDoubleClick(col.field)} title="더블클릭하여 기본 너비로 되돌리기" >
{hasDynamicSource ? ( setOpenPopover(open ? col.field : null)} >
데이터 소스 선택
{col.dynamicDataSource!.options.map((option) => ( ))}
) : ( <> {col.label} {col.required && *} )}
{/* 리사이즈 핸들 */}
handleMouseDown(e, col.field)} title="드래그하여 너비 조정" />
삭제
추가된 항목이 없습니다
{rowIndex + 1} {renderCell(row, col, rowIndex)}
); }