480 lines
16 KiB
TypeScript
480 lines
16 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import React from "react";
|
|||
|
|
import { Table, Filter, Search, Download, RefreshCw, Plus, Edit, Trash2 } from "lucide-react";
|
|||
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||
|
|
import { Button } from "@/components/ui/button";
|
|||
|
|
import { Input } from "@/components/ui/input";
|
|||
|
|
import { Badge } from "@/components/ui/badge";
|
|||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|||
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 데이터 테이블 템플릿 컴포넌트
|
|||
|
|
* 기존 하드코딩된 데이터 테이블 기능을 독립적인 컴포넌트로 분리
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
export interface DataTableTemplateProps {
|
|||
|
|
/**
|
|||
|
|
* 테이블 제목
|
|||
|
|
*/
|
|||
|
|
title?: string;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 테이블 설명
|
|||
|
|
*/
|
|||
|
|
description?: string;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 컬럼 정의
|
|||
|
|
*/
|
|||
|
|
columns?: Array<{
|
|||
|
|
id: string;
|
|||
|
|
label: string;
|
|||
|
|
type: string;
|
|||
|
|
visible: boolean;
|
|||
|
|
sortable: boolean;
|
|||
|
|
filterable: boolean;
|
|||
|
|
width?: number;
|
|||
|
|
}>;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 필터 설정
|
|||
|
|
*/
|
|||
|
|
filters?: Array<{
|
|||
|
|
id: string;
|
|||
|
|
label: string;
|
|||
|
|
type: "text" | "select" | "date" | "number";
|
|||
|
|
options?: Array<{ label: string; value: string }>;
|
|||
|
|
}>;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 페이징 설정
|
|||
|
|
*/
|
|||
|
|
pagination?: {
|
|||
|
|
enabled: boolean;
|
|||
|
|
pageSize: number;
|
|||
|
|
pageSizeOptions: number[];
|
|||
|
|
showPageSizeSelector: boolean;
|
|||
|
|
showPageInfo: boolean;
|
|||
|
|
showFirstLast: boolean;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 액션 버튼 설정
|
|||
|
|
*/
|
|||
|
|
actions?: {
|
|||
|
|
showSearchButton: boolean;
|
|||
|
|
searchButtonText: string;
|
|||
|
|
enableExport: boolean;
|
|||
|
|
enableRefresh: boolean;
|
|||
|
|
enableAdd: boolean;
|
|||
|
|
enableEdit: boolean;
|
|||
|
|
enableDelete: boolean;
|
|||
|
|
addButtonText: string;
|
|||
|
|
editButtonText: string;
|
|||
|
|
deleteButtonText: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 스타일 설정
|
|||
|
|
*/
|
|||
|
|
style?: React.CSSProperties;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 클래스명
|
|||
|
|
*/
|
|||
|
|
className?: string;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 미리보기 모드 (실제 데이터 없이 구조만 표시)
|
|||
|
|
*/
|
|||
|
|
isPreview?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const DataTableTemplate: React.FC<DataTableTemplateProps> = ({
|
|||
|
|
title = "데이터 테이블",
|
|||
|
|
description = "데이터를 표시하고 관리하는 테이블",
|
|||
|
|
columns = [],
|
|||
|
|
filters = [],
|
|||
|
|
pagination = {
|
|||
|
|
enabled: true,
|
|||
|
|
pageSize: 10,
|
|||
|
|
pageSizeOptions: [5, 10, 20, 50],
|
|||
|
|
showPageSizeSelector: true,
|
|||
|
|
showPageInfo: true,
|
|||
|
|
showFirstLast: true,
|
|||
|
|
},
|
|||
|
|
actions = {
|
|||
|
|
showSearchButton: true,
|
|||
|
|
searchButtonText: "검색",
|
|||
|
|
enableExport: true,
|
|||
|
|
enableRefresh: true,
|
|||
|
|
enableAdd: true,
|
|||
|
|
enableEdit: true,
|
|||
|
|
enableDelete: true,
|
|||
|
|
addButtonText: "추가",
|
|||
|
|
editButtonText: "수정",
|
|||
|
|
deleteButtonText: "삭제",
|
|||
|
|
},
|
|||
|
|
style,
|
|||
|
|
className = "",
|
|||
|
|
isPreview = true,
|
|||
|
|
}) => {
|
|||
|
|
// 미리보기용 기본 컬럼 데이터
|
|||
|
|
const defaultColumns = React.useMemo(() => {
|
|||
|
|
if (columns.length > 0) return columns;
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
{ id: "id", label: "ID", type: "number", visible: true, sortable: true, filterable: false, width: 80 },
|
|||
|
|
{ id: "name", label: "이름", type: "text", visible: true, sortable: true, filterable: true, width: 150 },
|
|||
|
|
{ id: "email", label: "이메일", type: "email", visible: true, sortable: true, filterable: true, width: 200 },
|
|||
|
|
{ id: "status", label: "상태", type: "select", visible: true, sortable: true, filterable: true, width: 100 },
|
|||
|
|
{
|
|||
|
|
id: "created_date",
|
|||
|
|
label: "생성일",
|
|||
|
|
type: "date",
|
|||
|
|
visible: true,
|
|||
|
|
sortable: true,
|
|||
|
|
filterable: true,
|
|||
|
|
width: 120,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
}, [columns]);
|
|||
|
|
|
|||
|
|
// 미리보기용 샘플 데이터
|
|||
|
|
const sampleData = React.useMemo(() => {
|
|||
|
|
if (!isPreview) return [];
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
{ id: 1, name: "홍길동", email: "hong@example.com", status: "활성", created_date: "2024-01-15" },
|
|||
|
|
{ id: 2, name: "김철수", email: "kim@example.com", status: "비활성", created_date: "2024-01-14" },
|
|||
|
|
{ id: 3, name: "이영희", email: "lee@example.com", status: "활성", created_date: "2024-01-13" },
|
|||
|
|
];
|
|||
|
|
}, [isPreview]);
|
|||
|
|
|
|||
|
|
const visibleColumns = defaultColumns.filter((col) => col.visible);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Card className={`h-full w-full ${className}`} style={style}>
|
|||
|
|
{/* 헤더 영역 */}
|
|||
|
|
<CardHeader className="pb-4">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<CardTitle className="flex items-center space-x-2">
|
|||
|
|
<Table className="h-5 w-5" />
|
|||
|
|
<span>{title}</span>
|
|||
|
|
</CardTitle>
|
|||
|
|
{description && <p className="text-muted-foreground mt-1 text-sm">{description}</p>}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 액션 버튼들 */}
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
{actions.enableRefresh && (
|
|||
|
|
<Button variant="outline" size="sm">
|
|||
|
|
<RefreshCw className="h-4 w-4" />
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{actions.enableExport && (
|
|||
|
|
<Button variant="outline" size="sm">
|
|||
|
|
<Download className="mr-1 h-4 w-4" />
|
|||
|
|
내보내기
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{actions.enableAdd && (
|
|||
|
|
<Button size="sm">
|
|||
|
|
<Plus className="mr-1 h-4 w-4" />
|
|||
|
|
{actions.addButtonText}
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardHeader>
|
|||
|
|
|
|||
|
|
<CardContent className="space-y-4">
|
|||
|
|
{/* 검색 및 필터 영역 */}
|
|||
|
|
<div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
|
|||
|
|
{/* 검색 입력 */}
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<div className="relative min-w-[200px] flex-1">
|
|||
|
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|||
|
|
<Input placeholder="검색어를 입력하세요..." className="pl-10" disabled={isPreview} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{actions.showSearchButton && (
|
|||
|
|
<Button variant="outline" disabled={isPreview}>
|
|||
|
|
{actions.searchButtonText}
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 필터 영역 */}
|
|||
|
|
{filters.length > 0 && (
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<Filter className="text-muted-foreground h-4 w-4" />
|
|||
|
|
{filters.slice(0, 3).map((filter) => (
|
|||
|
|
<Select key={filter.id} disabled={isPreview}>
|
|||
|
|
<SelectTrigger className="w-[140px]">
|
|||
|
|
<SelectValue placeholder={filter.label} />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
{filter.options?.map((option) => (
|
|||
|
|
<SelectItem key={option.value} value={option.value}>
|
|||
|
|
{option.label}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 데이터 테이블 */}
|
|||
|
|
<div className="rounded-md border">
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="w-full">
|
|||
|
|
{/* 테이블 헤더 */}
|
|||
|
|
<thead className="bg-muted/50 border-b">
|
|||
|
|
<tr>
|
|||
|
|
{/* 선택 체크박스 */}
|
|||
|
|
<th className="w-12 p-3">
|
|||
|
|
<Checkbox disabled={isPreview} />
|
|||
|
|
</th>
|
|||
|
|
|
|||
|
|
{/* 컬럼 헤더 */}
|
|||
|
|
{visibleColumns.map((column) => (
|
|||
|
|
<th key={column.id} className="p-3 text-left font-medium" style={{ width: column.width }}>
|
|||
|
|
<div className="flex items-center space-x-1">
|
|||
|
|
<span>{column.label}</span>
|
|||
|
|
{column.sortable && (
|
|||
|
|
<div className="flex flex-col">
|
|||
|
|
<div className="bg-muted-foreground h-1 w-1"></div>
|
|||
|
|
<div className="bg-muted-foreground mt-0.5 h-1 w-1"></div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</th>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
{/* 액션 컬럼 */}
|
|||
|
|
{(actions.enableEdit || actions.enableDelete) && <th className="w-24 p-3 text-center">액션</th>}
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
|
|||
|
|
{/* 테이블 바디 */}
|
|||
|
|
<tbody>
|
|||
|
|
{isPreview ? (
|
|||
|
|
// 미리보기 데이터
|
|||
|
|
sampleData.map((row, index) => (
|
|||
|
|
<tr key={index} className="hover:bg-muted/30 border-b">
|
|||
|
|
<td className="p-3">
|
|||
|
|
<Checkbox disabled />
|
|||
|
|
</td>
|
|||
|
|
{visibleColumns.map((column) => (
|
|||
|
|
<td key={column.id} className="p-3">
|
|||
|
|
{column.type === "select" && column.id === "status" ? (
|
|||
|
|
<Badge variant={row[column.id] === "활성" ? "default" : "secondary"}>
|
|||
|
|
{row[column.id]}
|
|||
|
|
</Badge>
|
|||
|
|
) : (
|
|||
|
|
<span className="text-sm">{row[column.id]}</span>
|
|||
|
|
)}
|
|||
|
|
</td>
|
|||
|
|
))}
|
|||
|
|
{(actions.enableEdit || actions.enableDelete) && (
|
|||
|
|
<td className="p-3">
|
|||
|
|
<div className="flex items-center justify-center space-x-1">
|
|||
|
|
{actions.enableEdit && (
|
|||
|
|
<Button variant="ghost" size="sm" disabled>
|
|||
|
|
<Edit className="h-3 w-3" />
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
{actions.enableDelete && (
|
|||
|
|
<Button variant="ghost" size="sm" disabled>
|
|||
|
|
<Trash2 className="h-3 w-3 text-red-500" />
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
)}
|
|||
|
|
</tr>
|
|||
|
|
))
|
|||
|
|
) : (
|
|||
|
|
// 실제 데이터가 없는 경우 플레이스홀더
|
|||
|
|
<tr>
|
|||
|
|
<td colSpan={visibleColumns.length + 2} className="text-muted-foreground p-8 text-center">
|
|||
|
|
데이터가 없습니다.
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
)}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 페이징 영역 */}
|
|||
|
|
{pagination.enabled && (
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="text-muted-foreground flex items-center space-x-2 text-sm">
|
|||
|
|
{pagination.showPageInfo && <span>{isPreview ? "1-3 of 3" : "0-0 of 0"} 항목 표시</span>}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
{/* 페이지 크기 선택 */}
|
|||
|
|
{pagination.showPageSizeSelector && (
|
|||
|
|
<div className="flex items-center space-x-2">
|
|||
|
|
<span className="text-muted-foreground text-sm">표시:</span>
|
|||
|
|
<Select disabled={isPreview}>
|
|||
|
|
<SelectTrigger className="w-16">
|
|||
|
|
<SelectValue placeholder={pagination.pageSize.toString()} />
|
|||
|
|
</SelectTrigger>
|
|||
|
|
<SelectContent>
|
|||
|
|
{pagination.pageSizeOptions.map((size) => (
|
|||
|
|
<SelectItem key={size} value={size.toString()}>
|
|||
|
|
{size}
|
|||
|
|
</SelectItem>
|
|||
|
|
))}
|
|||
|
|
</SelectContent>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 페이지 네비게이션 */}
|
|||
|
|
<div className="flex items-center space-x-1">
|
|||
|
|
{pagination.showFirstLast && (
|
|||
|
|
<Button variant="outline" size="sm" disabled>
|
|||
|
|
«
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
<Button variant="outline" size="sm" disabled>
|
|||
|
|
‹
|
|||
|
|
</Button>
|
|||
|
|
<Button variant="outline" size="sm" className="bg-primary text-primary-foreground">
|
|||
|
|
1
|
|||
|
|
</Button>
|
|||
|
|
<Button variant="outline" size="sm" disabled>
|
|||
|
|
›
|
|||
|
|
</Button>
|
|||
|
|
{pagination.showFirstLast && (
|
|||
|
|
<Button variant="outline" size="sm" disabled>
|
|||
|
|
»
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 데이터 테이블 템플릿 기본 설정
|
|||
|
|
*/
|
|||
|
|
export const getDefaultDataTableConfig = () => ({
|
|||
|
|
template_code: "advanced-data-table-v2",
|
|||
|
|
template_name: "고급 데이터 테이블 v2",
|
|||
|
|
template_name_eng: "Advanced Data Table v2",
|
|||
|
|
description: "검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트",
|
|||
|
|
category: "table",
|
|||
|
|
icon_name: "table",
|
|||
|
|
default_size: {
|
|||
|
|
width: 1000,
|
|||
|
|
height: 680,
|
|||
|
|
},
|
|||
|
|
layout_config: {
|
|||
|
|
components: [
|
|||
|
|
{
|
|||
|
|
type: "datatable",
|
|||
|
|
label: "고급 데이터 테이블",
|
|||
|
|
position: { x: 0, y: 0 },
|
|||
|
|
size: { width: 1000, height: 680 },
|
|||
|
|
style: {
|
|||
|
|
border: "1px solid #e5e7eb",
|
|||
|
|
borderRadius: "8px",
|
|||
|
|
backgroundColor: "#ffffff",
|
|||
|
|
padding: "0",
|
|||
|
|
},
|
|||
|
|
// 데이터 테이블 전용 설정
|
|||
|
|
columns: [
|
|||
|
|
{ id: "id", label: "ID", type: "number", visible: true, sortable: true, filterable: false, width: 80 },
|
|||
|
|
{ id: "name", label: "이름", type: "text", visible: true, sortable: true, filterable: true, width: 150 },
|
|||
|
|
{ id: "email", label: "이메일", type: "email", visible: true, sortable: true, filterable: true, width: 200 },
|
|||
|
|
{ id: "status", label: "상태", type: "select", visible: true, sortable: true, filterable: true, width: 100 },
|
|||
|
|
{
|
|||
|
|
id: "created_date",
|
|||
|
|
label: "생성일",
|
|||
|
|
type: "date",
|
|||
|
|
visible: true,
|
|||
|
|
sortable: true,
|
|||
|
|
filterable: true,
|
|||
|
|
width: 120,
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
filters: [
|
|||
|
|
{
|
|||
|
|
id: "status",
|
|||
|
|
label: "상태",
|
|||
|
|
type: "select",
|
|||
|
|
options: [
|
|||
|
|
{ label: "전체", value: "" },
|
|||
|
|
{ label: "활성", value: "active" },
|
|||
|
|
{ label: "비활성", value: "inactive" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{ id: "name", label: "이름", type: "text" },
|
|||
|
|
{ id: "email", label: "이메일", type: "text" },
|
|||
|
|
],
|
|||
|
|
pagination: {
|
|||
|
|
enabled: true,
|
|||
|
|
pageSize: 10,
|
|||
|
|
pageSizeOptions: [5, 10, 20, 50, 100],
|
|||
|
|
showPageSizeSelector: true,
|
|||
|
|
showPageInfo: true,
|
|||
|
|
showFirstLast: true,
|
|||
|
|
},
|
|||
|
|
actions: {
|
|||
|
|
showSearchButton: true,
|
|||
|
|
searchButtonText: "검색",
|
|||
|
|
enableExport: true,
|
|||
|
|
enableRefresh: true,
|
|||
|
|
enableAdd: true,
|
|||
|
|
enableEdit: true,
|
|||
|
|
enableDelete: true,
|
|||
|
|
addButtonText: "추가",
|
|||
|
|
editButtonText: "수정",
|
|||
|
|
deleteButtonText: "삭제",
|
|||
|
|
},
|
|||
|
|
// 모달 설정
|
|||
|
|
addModalConfig: {
|
|||
|
|
title: "새 데이터 추가",
|
|||
|
|
description: "테이블에 새로운 데이터를 추가합니다.",
|
|||
|
|
width: "lg",
|
|||
|
|
layout: "two-column",
|
|||
|
|
gridColumns: 2,
|
|||
|
|
fieldOrder: ["name", "email", "status"],
|
|||
|
|
requiredFields: ["name", "email"],
|
|||
|
|
hiddenFields: ["id", "created_date"],
|
|||
|
|
advancedFieldConfigs: {
|
|||
|
|
status: {
|
|||
|
|
type: "select",
|
|||
|
|
options: [
|
|||
|
|
{ label: "활성", value: "active" },
|
|||
|
|
{ label: "비활성", value: "inactive" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
submitButtonText: "추가",
|
|||
|
|
cancelButtonText: "취소",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export default DataTableTemplate;
|