ERP-node/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx

275 lines
9.9 KiB
TypeScript

"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<string, string>; // 컬럼별 현재 활성화된 데이터 소스 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<string | null>(null);
// 데이터 변경 감지 (필요시 활성화)
// 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 (
<div className="px-2 py-1">
{column.type === "number"
? typeof value === "number"
? value.toLocaleString()
: value || "0"
: value || "-"}
</div>
);
}
// 편집 가능한 필드
switch (column.type) {
case "number":
return (
<Input
type="number"
value={value || ""}
onChange={(e) =>
handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0)
}
className="h-7 text-xs"
/>
);
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 (
<Input
type="date"
value={formatDateValue(value)}
onChange={(e) => handleCellEdit(rowIndex, column.field, e.target.value)}
className="h-7 text-xs"
/>
);
case "select":
return (
<Select
value={value || ""}
onValueChange={(newValue) =>
handleCellEdit(rowIndex, column.field, newValue)
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{column.selectOptions?.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
default: // text
return (
<Input
type="text"
value={value || ""}
onChange={(e) => handleCellEdit(rowIndex, column.field, e.target.value)}
className="h-7 text-xs"
/>
);
}
};
return (
<div className="border rounded-md overflow-hidden bg-background">
<div className="overflow-x-auto max-h-[240px] overflow-y-auto">
<table className="w-full text-xs sm:text-sm">
<thead className="bg-muted sticky top-0 z-10">
<tr>
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-12">
#
</th>
{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 (
<th
key={col.field}
className="px-4 py-2 text-left font-medium text-muted-foreground"
style={{ width: col.width }}
>
{hasDynamicSource ? (
<Popover
open={openPopover === col.field}
onOpenChange={(open) => setOpenPopover(open ? col.field : null)}
>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"inline-flex items-center gap-1 hover:text-primary transition-colors",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 rounded px-1 -mx-1"
)}
>
<span>{col.label}</span>
<ChevronDown className="h-3 w-3 opacity-60" />
</button>
</PopoverTrigger>
<PopoverContent
className="w-auto min-w-[160px] p-1"
align="start"
sideOffset={4}
>
<div className="text-[10px] text-muted-foreground px-2 py-1 border-b mb-1">
</div>
{col.dynamicDataSource!.options.map((option) => (
<button
key={option.id}
type="button"
onClick={() => {
onDataSourceChange?.(col.field, option.id);
setOpenPopover(null);
}}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-sm",
"hover:bg-accent hover:text-accent-foreground transition-colors",
"focus:outline-none focus-visible:bg-accent",
activeOption?.id === option.id && "bg-accent/50"
)}
>
<Check
className={cn(
"h-3 w-3",
activeOption?.id === option.id ? "opacity-100" : "opacity-0"
)}
/>
<span>{option.label}</span>
</button>
))}
</PopoverContent>
</Popover>
) : (
<>
{col.label}
{col.required && <span className="text-destructive ml-1">*</span>}
</>
)}
</th>
);
})}
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-20">
</th>
</tr>
</thead>
<tbody className="bg-background">
{data.length === 0 ? (
<tr>
<td
colSpan={columns.length + 2}
className="px-4 py-8 text-center text-muted-foreground"
>
</td>
</tr>
) : (
data.map((row, rowIndex) => (
<tr key={rowIndex} className="border-t hover:bg-accent/50">
<td className="px-4 py-2 text-center text-muted-foreground">
{rowIndex + 1}
</td>
{columns.map((col) => (
<td key={col.field} className="px-2 py-1">
{renderCell(row, col, rowIndex)}
</td>
))}
<td className="px-4 py-2 text-center">
<Button
variant="ghost"
size="sm"
onClick={() => onRowDelete(rowIndex)}
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}