ERP-node/frontend/components/screen/InteractiveDataTable.tsx

1178 lines
40 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Separator } from "@/components/ui/separator";
import { Search, ChevronLeft, ChevronRight, RotateCcw, Database, Loader2, Plus, Edit, Trash2 } from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
import { getCurrentUser, UserInfo } from "@/lib/api/client";
import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen";
import { cn } from "@/lib/utils";
interface InteractiveDataTableProps {
component: DataTableComponent;
className?: string;
style?: React.CSSProperties;
}
export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
component,
className = "",
style = {},
}) => {
const [data, setData] = useState<Record<string, any>[]>([]);
const [loading, setLoading] = useState(false);
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [total, setTotal] = useState(0);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [showAddModal, setShowAddModal] = useState(false);
const [addFormData, setAddFormData] = useState<Record<string, any>>({});
const [isAdding, setIsAdding] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [editFormData, setEditFormData] = useState<Record<string, any>>({});
const [editingRowData, setEditingRowData] = useState<Record<string, any> | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// 현재 사용자 정보
const [currentUser, setCurrentUser] = useState<UserInfo | null>(null);
// 검색 가능한 컬럼만 필터링
const visibleColumns = component.columns?.filter((col: DataTableColumn) => col.visible) || [];
const searchFilters = component.filters || [];
// 그리드 컬럼 계산
const totalGridColumns = visibleColumns.reduce((sum, col) => sum + (col.gridColumns || 2), 0);
// 페이지 크기 설정
const pageSize = component.pagination?.pageSize || 10;
// 데이터 로드 함수
const loadData = useCallback(
async (page: number = 1, searchParams: Record<string, any> = {}) => {
if (!component.tableName) return;
setLoading(true);
try {
console.log("🔍 테이블 데이터 조회:", {
tableName: component.tableName,
page,
pageSize,
searchParams,
});
const result = await tableTypeApi.getTableData(component.tableName, {
page,
size: pageSize,
search: searchParams,
});
console.log("✅ 테이블 데이터 조회 결과:", result);
setData(result.data);
setTotal(result.total);
setTotalPages(result.totalPages);
setCurrentPage(result.page);
} catch (error) {
console.error("❌ 테이블 데이터 조회 실패:", error);
setData([]);
setTotal(0);
setTotalPages(1);
} finally {
setLoading(false);
}
},
[component.tableName, pageSize],
);
// 현재 사용자 정보 로드
useEffect(() => {
const fetchCurrentUser = async () => {
try {
const response = await getCurrentUser();
if (response.success && response.data) {
setCurrentUser(response.data);
}
} catch (error) {
console.error("현재 사용자 정보 로드 실패:", error);
}
};
fetchCurrentUser();
}, []);
// 초기 데이터 로드
useEffect(() => {
loadData(1, searchValues);
}, [loadData]);
// 검색 실행
const handleSearch = useCallback(() => {
console.log("🔍 검색 실행:", searchValues);
loadData(1, searchValues);
}, [searchValues, loadData]);
// 검색값 변경
const handleSearchValueChange = useCallback((columnName: string, value: any) => {
setSearchValues((prev) => ({
...prev,
[columnName]: value,
}));
}, []);
// 페이지 변경
const handlePageChange = useCallback(
(page: number) => {
loadData(page, searchValues);
},
[loadData, searchValues],
);
// 행 선택 핸들러
const handleRowSelect = useCallback((rowIndex: number, isSelected: boolean) => {
setSelectedRows((prev) => {
const newSet = new Set(prev);
if (isSelected) {
newSet.add(rowIndex);
} else {
newSet.delete(rowIndex);
}
return newSet;
});
}, []);
// 전체 선택/해제 핸들러
const handleSelectAll = useCallback(
(isSelected: boolean) => {
if (isSelected) {
setSelectedRows(new Set(Array.from({ length: data.length }, (_, i) => i)));
} else {
setSelectedRows(new Set());
}
},
[data.length],
);
// 모달에 표시할 컬럼 계산
const getDisplayColumns = useCallback(() => {
const { hiddenFields, fieldOrder, advancedFieldConfigs } = component.addModalConfig || {};
// 숨겨진 필드와 고급 설정에서 숨겨진 필드 제외
let displayColumns = visibleColumns.filter((col) => {
// 기본 숨김 필드 체크
if (hiddenFields?.includes(col.columnName)) return false;
// 고급 설정에서 숨김 체크
const config = advancedFieldConfigs?.[col.columnName];
if (config?.inputType === "hidden") return false;
return true;
});
// 필드 순서 적용
if (fieldOrder && fieldOrder.length > 0) {
const orderedColumns: typeof displayColumns = [];
const remainingColumns = [...displayColumns];
// 지정된 순서대로 추가
fieldOrder.forEach((columnName) => {
const column = remainingColumns.find((col) => col.columnName === columnName);
if (column) {
orderedColumns.push(column);
const index = remainingColumns.indexOf(column);
remainingColumns.splice(index, 1);
}
});
// 나머지 컬럼들 추가
orderedColumns.push(...remainingColumns);
displayColumns = orderedColumns;
}
return displayColumns;
}, [visibleColumns, component.addModalConfig]);
// 자동 값 생성
const generateAutoValue = useCallback(
(autoValueType: string): string => {
const now = new Date();
switch (autoValueType) {
case "current_datetime":
return now.toISOString().slice(0, 19); // YYYY-MM-DDTHH:mm:ss
case "current_date":
return now.toISOString().slice(0, 10); // YYYY-MM-DD
case "current_time":
return now.toTimeString().slice(0, 8); // HH:mm:ss
case "current_user":
return currentUser?.userName || currentUser?.userId || "unknown_user";
case "uuid":
return crypto.randomUUID();
case "sequence":
return `SEQ_${Date.now()}`;
default:
return "";
}
},
[currentUser],
);
// 데이터 추가 핸들러
const handleAddData = useCallback(() => {
// 폼 데이터 초기화
const initialData: Record<string, any> = {};
const displayColumns = getDisplayColumns();
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
displayColumns.forEach((col) => {
const config = advancedConfigs[col.columnName];
if (config?.inputType === "auto") {
// 자동 값 설정
if (config.autoValueType === "custom") {
initialData[col.columnName] = config.customValue || "";
} else {
initialData[col.columnName] = generateAutoValue(config.autoValueType);
}
} else if (config?.defaultValue) {
// 기본값 설정
initialData[col.columnName] = config.defaultValue;
} else {
// 일반 빈 값
initialData[col.columnName] = "";
}
});
setAddFormData(initialData);
setShowAddModal(true);
}, [getDisplayColumns, generateAutoValue, component.addModalConfig]);
// 추가 폼 데이터 변경 핸들러
const handleAddFormChange = useCallback((columnName: string, value: any) => {
setAddFormData((prev) => ({
...prev,
[columnName]: value,
}));
}, []);
// 데이터 수정 핸들러
const handleEditData = useCallback(() => {
if (selectedRows.size !== 1) return;
const selectedIndex = Array.from(selectedRows)[0];
const selectedRowData = data[selectedIndex];
if (!selectedRowData) return;
// 수정할 데이터로 폼 초기화
const initialData: Record<string, any> = {};
const displayColumns = getDisplayColumns();
displayColumns.forEach((col) => {
initialData[col.columnName] = selectedRowData[col.columnName] || "";
});
setEditFormData(initialData);
setEditingRowData(selectedRowData);
setShowEditModal(true);
}, [selectedRows, data, getDisplayColumns]);
// 수정 폼 데이터 변경 핸들러
const handleEditFormChange = useCallback((columnName: string, value: any) => {
setEditFormData((prev) => ({
...prev,
[columnName]: value,
}));
}, []);
// 데이터 추가 제출 핸들러
const handleAddSubmit = useCallback(async () => {
try {
setIsAdding(true);
// 실제 API 호출로 데이터 추가
console.log("🔥 추가할 데이터:", addFormData);
await tableTypeApi.addTableData(component.tableName, addFormData);
// 모달 닫기 및 폼 초기화
setShowAddModal(false);
setAddFormData({});
// 첫 페이지로 이동하여 새 데이터 확인
loadData(1, searchValues);
} catch (error) {
console.error("데이터 추가 실패:", error);
alert("데이터 추가에 실패했습니다.");
} finally {
setIsAdding(false);
}
}, [addFormData, loadData, searchValues]);
// 데이터 수정 제출 핸들러
const handleEditSubmit = useCallback(async () => {
try {
setIsEditing(true);
// 실제 API 호출로 데이터 수정
console.log("🔥 수정할 데이터:", editFormData);
console.log("🔥 원본 데이터:", editingRowData);
if (editingRowData) {
await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData);
// 모달 닫기 및 폼 초기화
setShowEditModal(false);
setEditFormData({});
setEditingRowData(null);
setSelectedRows(new Set()); // 선택 해제
// 현재 페이지 데이터 새로고침
loadData(currentPage, searchValues);
}
} catch (error) {
console.error("데이터 수정 실패:", error);
alert("데이터 수정에 실패했습니다.");
} finally {
setIsEditing(false);
}
}, [editFormData, editingRowData, component.tableName, currentPage, searchValues, loadData]);
// 추가 모달 닫기 핸들러
const handleAddModalClose = useCallback(() => {
if (!isAdding) {
setShowAddModal(false);
setAddFormData({});
}
}, [isAdding]);
// 데이터 삭제 핸들러
const handleDeleteData = useCallback(() => {
if (selectedRows.size === 0) {
alert("삭제할 데이터를 선택해주세요.");
return;
}
setShowDeleteDialog(true);
}, [selectedRows.size]);
// 삭제 확인 핸들러
const handleDeleteConfirm = useCallback(async () => {
try {
setIsDeleting(true);
// 선택된 행의 실제 데이터 가져오기
const selectedData = Array.from(selectedRows).map((index) => data[index]);
// 실제 삭제 API 호출
console.log("🗑️ 삭제할 데이터:", selectedData);
await tableTypeApi.deleteTableData(component.tableName, selectedData);
// 선택 해제 및 다이얼로그 닫기
setSelectedRows(new Set());
setShowDeleteDialog(false);
// 데이터 새로고침
loadData(currentPage, searchValues);
} catch (error) {
console.error("데이터 삭제 실패:", error);
alert("데이터 삭제에 실패했습니다.");
} finally {
setIsDeleting(false);
}
}, [selectedRows, data, currentPage, searchValues, loadData]);
// 삭제 다이얼로그 닫기 핸들러
const handleDeleteDialogClose = useCallback(() => {
if (!isDeleting) {
setShowDeleteDialog(false);
}
}, [isDeleting]);
// 필수 필드 여부 확인
const isRequiredField = useCallback(
(columnName: string) => {
return component.addModalConfig?.requiredFields?.includes(columnName) || false;
},
[component.addModalConfig],
);
// 모달 크기 클래스 가져오기
const getModalSizeClass = useCallback(() => {
const width = component.addModalConfig?.width || "lg";
const sizeMap = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
"2xl": "max-w-2xl",
full: "max-w-full mx-4",
};
return sizeMap[width];
}, [component.addModalConfig]);
// 레이아웃 클래스 가져오기
const getLayoutClass = useCallback(() => {
const layout = component.addModalConfig?.layout || "two-column";
const gridColumns = component.addModalConfig?.gridColumns || 2;
switch (layout) {
case "single":
return "grid grid-cols-1 gap-4";
case "two-column":
return "grid grid-cols-2 gap-4";
case "grid":
return `grid grid-cols-${Math.min(gridColumns, 4)} gap-4`;
default:
return "grid grid-cols-2 gap-4";
}
}, [component.addModalConfig]);
// 수정 폼 입력 컴포넌트 렌더링
const renderEditFormInput = (column: DataTableColumn) => {
const value = editFormData[column.columnName] || "";
const isRequired = isRequiredField(column.columnName);
const advancedConfig = component.addModalConfig?.advancedFieldConfigs?.[column.columnName];
// 자동 생성 필드는 수정에서 읽기 전용으로 처리
if (advancedConfig?.inputType === "auto") {
return (
<div className="relative">
<Input
value={value}
readOnly
className="bg-gray-50 text-gray-700"
placeholder={`${column.label} (자동 생성됨)`}
/>
<p className="mt-1 text-xs text-gray-500"> .</p>
</div>
);
}
// 읽기 전용 필드
if (advancedConfig?.inputType === "readonly") {
return (
<div className="relative">
<Input
value={value}
readOnly
className="bg-gray-50 text-gray-700"
placeholder={advancedConfig?.placeholder || `${column.label} (읽기 전용)`}
/>
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
}
// 일반 입력 필드 렌더링
const commonProps = {
value,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => handleEditFormChange(column.columnName, e.target.value),
placeholder: advancedConfig?.placeholder || `${column.label} 입력...`,
required: isRequired,
className: isRequired && !value ? "border-orange-300 focus:border-orange-500" : "",
};
switch (column.widgetType) {
case "text":
case "email":
case "tel":
return (
<div>
<Input
type={column.widgetType === "email" ? "email" : column.widgetType === "tel" ? "tel" : "text"}
{...commonProps}
/>
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "number":
case "decimal":
return (
<div>
<Input type="number" step={column.widgetType === "decimal" ? "0.01" : "1"} {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "date":
return (
<div>
<Input type="date" {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "datetime":
return (
<div>
<Input type="datetime-local" {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "select":
case "dropdown":
// TODO: 동적 옵션 로드
return <Input {...commonProps} placeholder={`${column.label} 선택... (개발 중)`} readOnly />;
default:
return (
<div>
<Input {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
}
};
// 추가 폼 입력 컴포넌트 렌더링
const renderAddFormInput = (column: DataTableColumn) => {
const value = addFormData[column.columnName] || "";
const isRequired = isRequiredField(column.columnName);
const advancedConfig = component.addModalConfig?.advancedFieldConfigs?.[column.columnName];
// 읽기 전용 또는 자동 값인 경우
if (advancedConfig?.inputType === "readonly" || advancedConfig?.inputType === "auto") {
return (
<div className="relative">
<Input
value={value}
readOnly
className="bg-gray-50 text-gray-700"
placeholder={advancedConfig?.placeholder || `${column.label} (자동 생성)`}
/>
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
}
// 일반 입력 필드 렌더링
const commonProps = {
value,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => handleAddFormChange(column.columnName, e.target.value),
placeholder: advancedConfig?.placeholder || `${column.label} 입력...`,
required: isRequired,
className: isRequired && !value ? "border-orange-300 focus:border-orange-500" : "",
};
switch (column.widgetType) {
case "text":
case "email":
case "tel":
return (
<div>
<Input
type={column.widgetType === "email" ? "email" : column.widgetType === "tel" ? "tel" : "text"}
{...commonProps}
/>
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "number":
case "decimal":
return (
<div>
<Input type="number" step={column.widgetType === "decimal" ? "0.01" : "1"} {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "date":
return (
<div>
<Input type="date" {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "datetime":
return (
<div>
<Input type="datetime-local" {...commonProps} />
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
</div>
);
case "select":
case "dropdown":
// TODO: 동적 옵션 로드
return (
<Select value={value} onValueChange={(newValue) => handleAddFormChange(column.columnName, newValue)}>
<SelectTrigger>
<SelectValue placeholder={`${column.label} 선택...`} />
</SelectTrigger>
<SelectContent>
<SelectItem value=""> </SelectItem>
<SelectItem value="option1"> 1</SelectItem>
<SelectItem value="option2"> 2</SelectItem>
<SelectItem value="option3"> 3</SelectItem>
</SelectContent>
</Select>
);
case "boolean":
case "checkbox":
return (
<div className="flex items-center space-x-2">
<Checkbox
checked={value === true || value === "true"}
onCheckedChange={(checked) => handleAddFormChange(column.columnName, checked)}
/>
<Label>{column.label}</Label>
</div>
);
default:
return (
<Input
value={value}
onChange={(e) => handleAddFormChange(column.columnName, e.target.value)}
placeholder={`${column.label} 입력...`}
/>
);
}
};
// 검색 필터 렌더링
const renderSearchFilter = (filter: DataTableFilter) => {
const value = searchValues[filter.columnName] || "";
switch (filter.widgetType) {
case "text":
case "email":
case "tel":
return (
<Input
key={filter.columnName}
placeholder={`${filter.label} 검색...`}
value={value}
onChange={(e) => handleSearchValueChange(filter.columnName, e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
/>
);
case "number":
case "decimal":
return (
<Input
key={filter.columnName}
type="number"
placeholder={`${filter.label} 입력...`}
value={value}
onChange={(e) => handleSearchValueChange(filter.columnName, e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
/>
);
case "date":
return (
<Input
key={filter.columnName}
type="date"
value={value}
onChange={(e) => handleSearchValueChange(filter.columnName, e.target.value)}
/>
);
case "datetime":
return (
<Input
key={filter.columnName}
type="datetime-local"
value={value}
onChange={(e) => handleSearchValueChange(filter.columnName, e.target.value)}
/>
);
case "select":
// TODO: 선택 옵션은 추후 구현
return (
<Select
key={filter.columnName}
value={value}
onValueChange={(newValue) => handleSearchValueChange(filter.columnName, newValue)}
>
<SelectTrigger>
<SelectValue placeholder={`${filter.label} 선택...`} />
</SelectTrigger>
<SelectContent>
<SelectItem value=""></SelectItem>
{/* TODO: 동적 옵션 로드 */}
</SelectContent>
</Select>
);
default:
return (
<Input
key={filter.columnName}
placeholder={`${filter.label} 검색...`}
value={value}
onChange={(e) => handleSearchValueChange(filter.columnName, e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
/>
);
}
};
// 셀 값 포맷팅
const formatCellValue = (value: any, column: DataTableColumn) => {
if (value === null || value === undefined) return "";
switch (column.widgetType) {
case "date":
if (value) {
try {
const date = new Date(value);
return date.toLocaleDateString("ko-KR");
} catch {
return value;
}
}
break;
case "datetime":
if (value) {
try {
const date = new Date(value);
return date.toLocaleString("ko-KR");
} catch {
return value;
}
}
break;
case "number":
case "decimal":
if (typeof value === "number") {
return value.toLocaleString();
}
break;
default:
return String(value);
}
return String(value);
};
return (
<Card className={cn("flex h-full flex-col", className)} style={{ ...style, minHeight: "680px" }}>
{/* 헤더 */}
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Database className="text-muted-foreground h-4 w-4" />
<CardTitle className="text-lg">{component.title || component.label}</CardTitle>
{loading && (
<Badge variant="secondary" className="flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
...
</Badge>
)}
</div>
<div className="flex items-center space-x-2">
{/* 선택된 행 개수 표시 */}
{selectedRows.size > 0 && (
<Badge variant="secondary" className="text-xs">
{selectedRows.size}
</Badge>
)}
{searchFilters.length > 0 && (
<Badge variant="outline" className="text-xs">
<Search className="mr-1 h-3 w-3" />
{searchFilters.length}
</Badge>
)}
{/* CRUD 버튼들 */}
{component.enableAdd && (
<Button size="sm" onClick={handleAddData} disabled={loading} className="gap-2">
<Plus className="h-3 w-3" />
{component.addButtonText || "추가"}
</Button>
)}
{component.enableEdit && selectedRows.size === 1 && (
<Button size="sm" onClick={handleEditData} disabled={loading} className="gap-2" variant="outline">
<Edit className="h-3 w-3" />
{component.editButtonText || "수정"}
</Button>
)}
{component.enableDelete && selectedRows.size > 0 && (
<Button size="sm" variant="destructive" onClick={handleDeleteData} disabled={loading} className="gap-2">
<Trash2 className="h-3 w-3" />
{component.deleteButtonText || "삭제"}
</Button>
)}
{component.showSearchButton && (
<Button size="sm" onClick={handleSearch} disabled={loading} className="gap-2">
<Search className="h-3 w-3" />
{component.searchButtonText || "검색"}
</Button>
)}
<Button size="sm" variant="outline" onClick={() => loadData(1, {})} disabled={loading} className="gap-2">
<RotateCcw className="h-3 w-3" />
</Button>
</div>
</div>
{/* 검색 필터 */}
{searchFilters.length > 0 && (
<>
<Separator className="my-2" />
<div className="space-y-3">
<CardDescription className="flex items-center gap-2">
<Search className="h-3 w-3" />
</CardDescription>
<div
className="grid gap-3"
style={{
gridTemplateColumns: searchFilters
.map((filter: DataTableFilter) => `${filter.gridColumns || 3}fr`)
.join(" "),
}}
>
{searchFilters.map((filter: DataTableFilter) => (
<div key={filter.columnName} className="space-y-1">
<label className="text-muted-foreground text-xs font-medium">{filter.label}</label>
{renderSearchFilter(filter)}
</div>
))}
</div>
</div>
</>
)}
</CardHeader>
{/* 테이블 내용 */}
<CardContent className="flex-1 p-0">
<div className="flex h-full flex-col">
{visibleColumns.length > 0 ? (
<>
<Table>
<TableHeader>
<TableRow>
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
{component.enableDelete && (
<TableHead className="w-12 px-4">
<Checkbox
checked={selectedRows.size === data.length && data.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
)}
{visibleColumns.map((column: DataTableColumn) => (
<TableHead
key={column.id}
className="px-4 font-semibold"
style={{ width: `${((column.gridColumns || 2) / totalGridColumns) * 100}%` }}
>
{column.label}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
className="h-32 text-center"
>
<div className="text-muted-foreground flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
</TableCell>
</TableRow>
) : data.length > 0 ? (
data.map((row, rowIndex) => (
<TableRow key={rowIndex} className="hover:bg-muted/50">
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
{component.enableDelete && (
<TableCell className="w-12 px-4">
<Checkbox
checked={selectedRows.has(rowIndex)}
onCheckedChange={(checked) => handleRowSelect(rowIndex, checked as boolean)}
/>
</TableCell>
)}
{visibleColumns.map((column: DataTableColumn) => (
<TableCell key={column.id} className="px-4 font-mono text-sm">
{formatCellValue(row[column.columnName], column)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
className="h-32 text-center"
>
<div className="text-muted-foreground flex flex-col items-center gap-2">
<Database className="h-8 w-8" />
<p> </p>
<p className="text-xs"> </p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* 페이지네이션 */}
{component.pagination?.enabled && totalPages > 1 && (
<div className="bg-muted/20 mt-auto border-t">
<div className="flex items-center justify-between px-6 py-3">
{component.pagination.showPageInfo && (
<div className="text-muted-foreground text-sm">
<span className="font-medium">{total.toLocaleString()}</span> {" "}
<span className="font-medium">{((currentPage - 1) * pageSize + 1).toLocaleString()}</span>-
<span className="font-medium">{Math.min(currentPage * pageSize, total).toLocaleString()}</span>
</div>
)}
<div className="flex items-center space-x-2">
{component.pagination.showFirstLast && (
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1 || loading}
className="gap-1"
>
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1 || loading}
className="gap-1"
>
<ChevronLeft className="h-3 w-3" />
</Button>
<div className="flex items-center gap-1 text-sm font-medium">
<span>{currentPage}</span>
<span className="text-muted-foreground">/</span>
<span>{totalPages}</span>
</div>
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages || loading}
className="gap-1"
>
<ChevronRight className="h-3 w-3" />
</Button>
{component.pagination.showFirstLast && (
<Button
size="sm"
variant="outline"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages || loading}
className="gap-1"
>
</Button>
)}
</div>
</div>
</div>
)}
</>
) : (
<div className="flex flex-1 items-center justify-center">
<div className="text-muted-foreground flex flex-col items-center gap-2">
<Database className="h-8 w-8" />
<p className="text-sm"> </p>
<p className="text-xs"> </p>
</div>
</div>
)}
</div>
</CardContent>
{/* 데이터 추가 모달 */}
<Dialog open={showAddModal} onOpenChange={handleAddModalClose}>
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
<DialogHeader>
<DialogTitle>{component.addModalConfig?.title || "새 데이터 추가"}</DialogTitle>
<DialogDescription>
{component.addModalConfig?.description ||
`${component.title || component.label}에 새로운 데이터를 추가합니다.`}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className={getLayoutClass()}>
{getDisplayColumns().map((column) => (
<div key={column.id} className="space-y-2">
<Label htmlFor={column.columnName} className="text-sm font-medium">
{column.label}
{isRequiredField(column.columnName) && <span className="ml-1 text-orange-500">*</span>}
</Label>
<div>{renderAddFormInput(column)}</div>
</div>
))}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleAddModalClose} disabled={isAdding}>
{component.addModalConfig?.cancelButtonText || "취소"}
</Button>
<Button onClick={handleAddSubmit} disabled={isAdding}>
{isAdding ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Plus className="mr-2 h-4 w-4" />
{component.addModalConfig?.submitButtonText || "추가"}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 데이터 수정 모달 */}
<Dialog
open={showEditModal}
onOpenChange={(open) => {
if (!isEditing && !open) {
setShowEditModal(false);
setEditFormData({});
setEditingRowData(null);
}
}}
>
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> .</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className={getLayoutClass()}>
{getDisplayColumns().map((column) => (
<div key={column.id} className="space-y-2">
<Label htmlFor={`edit-${column.columnName}`} className="text-sm font-medium">
{column.label}
{isRequiredField(column.columnName) && <span className="ml-1 text-orange-500">*</span>}
</Label>
<div>{renderEditFormInput(column)}</div>
</div>
))}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setShowEditModal(false);
setEditFormData({});
setEditingRowData(null);
}}
disabled={isEditing}
>
</Button>
<Button onClick={handleEditSubmit} disabled={isEditing}>
{isEditing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Edit className="mr-2 h-4 w-4" />
{component.editButtonText || "수정"}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 데이터 삭제 확인 다이얼로그 */}
<Dialog open={showDeleteDialog} onOpenChange={handleDeleteDialogClose}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription>
<strong>{selectedRows.size}</strong> ?
<br />
<span className="text-red-600"> .</span>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleDeleteDialogClose} disabled={isDeleting}>
</Button>
<Button variant="destructive" onClick={handleDeleteConfirm} disabled={isDeleting}>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
};