ERP-node/frontend/components/screen/panels/TemplatesPanel.tsx

242 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import {
Table,
Search,
FileText,
Grid3x3,
Info,
FormInput,
Save,
X,
Trash2,
Edit,
Plus,
RotateCcw,
Send,
ExternalLink,
MousePointer,
Settings,
Upload,
} from "lucide-react";
// 템플릿 컴포넌트 타입 정의
export interface TemplateComponent {
id: string;
name: string;
description: string;
category: "table" | "button" | "form" | "layout" | "chart" | "status" | "file";
icon: React.ReactNode;
defaultSize: { width: number; height: number };
components: Array<{
type: "widget" | "container" | "datatable" | "file";
widgetType?: string;
label: string;
placeholder?: string;
position: { x: number; y: number };
size: { width: number; height: number };
style?: any;
required?: boolean;
readonly?: boolean;
}>;
}
// 미리 정의된 템플릿 컴포넌트들
const templateComponents: TemplateComponent[] = [
// 고급 데이터 테이블 템플릿
{
id: "advanced-data-table",
name: "고급 데이터 테이블",
description: "컬럼 설정, 필터링, 페이지네이션이 포함된 완전한 데이터 테이블",
category: "table",
icon: <Table className="h-4 w-4" />,
defaultSize: { width: 1000, height: 680 },
components: [
// 데이터 테이블 컴포넌트 (특별한 타입)
{
type: "datatable",
label: "데이터 테이블",
position: { x: 0, y: 0 },
size: { width: 1000, height: 680 },
style: {
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#ffffff",
padding: "16px",
},
},
],
},
// 범용 버튼 템플릿
{
id: "universal-button",
name: "버튼",
description: "다양한 기능을 설정할 수 있는 범용 버튼. 상세설정에서 기능을 선택하세요.",
category: "button",
icon: <MousePointer className="h-4 w-4" />,
defaultSize: { width: 80, height: 36 },
components: [
{
type: "widget",
widgetType: "button",
label: "버튼",
position: { x: 0, y: 0 },
size: { width: 80, height: 36 },
style: {
backgroundColor: "#3b82f6",
color: "#ffffff",
border: "none",
borderRadius: "6px",
fontSize: "14px",
fontWeight: "500",
},
},
],
},
// 파일 첨부 템플릿
{
id: "file-upload",
name: "파일 첨부",
description: "파일 업로드, 미리보기, 다운로드가 가능한 파일 첨부 컴포넌트",
category: "file",
icon: <Upload className="h-4 w-4" />,
defaultSize: { width: 600, height: 300 },
components: [
{
type: "file",
label: "파일 첨부",
position: { x: 0, y: 0 },
size: { width: 600, height: 300 },
style: {
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#ffffff",
padding: "16px",
},
},
],
},
];
interface TemplatesPanelProps {
onDragStart: (e: React.DragEvent, template: TemplateComponent) => void;
}
export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) => {
const [searchTerm, setSearchTerm] = React.useState("");
const [selectedCategory, setSelectedCategory] = React.useState<string>("all");
const categories = [
{ id: "all", name: "전체", icon: <Grid3x3 className="h-4 w-4" /> },
{ id: "table", name: "테이블", icon: <Table className="h-4 w-4" /> },
{ id: "button", name: "버튼", icon: <MousePointer className="h-4 w-4" /> },
{ id: "file", name: "파일", icon: <Upload className="h-4 w-4" /> },
];
const filteredTemplates = templateComponents.filter((template) => {
const matchesSearch =
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
template.description.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = selectedCategory === "all" || template.category === selectedCategory;
return matchesSearch && matchesCategory;
});
return (
<div className="flex h-full flex-col space-y-4 p-4">
{/* 검색 */}
<div className="space-y-3">
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="템플릿 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* 카테고리 필터 */}
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<Button
key={category.id}
variant={selectedCategory === category.id ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category.id)}
className="flex items-center space-x-1"
>
{category.icon}
<span>{category.name}</span>
</Button>
))}
</div>
</div>
<Separator />
{/* 템플릿 목록 */}
<div className="flex-1 space-y-2 overflow-y-auto">
{filteredTemplates.length === 0 ? (
<div className="flex h-32 items-center justify-center text-center text-gray-500">
<div>
<FileText className="mx-auto mb-2 h-8 w-8" />
<p className="text-sm"> </p>
</div>
</div>
) : (
filteredTemplates.map((template) => (
<div
key={template.id}
draggable
onDragStart={(e) => onDragStart(e, template)}
className="group cursor-move rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:border-blue-300 hover:shadow-md"
>
<div className="flex items-start space-x-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50 text-blue-600 group-hover:bg-blue-100">
{template.icon}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center space-x-2">
<h4 className="truncate font-medium text-gray-900">{template.name}</h4>
<Badge variant="secondary" className="text-xs">
{template.components.length}
</Badge>
</div>
<p className="mt-1 line-clamp-2 text-xs text-gray-500">{template.description}</p>
<div className="mt-2 flex items-center space-x-2 text-xs text-gray-400">
<span>
{template.defaultSize.width}×{template.defaultSize.height}
</span>
<span></span>
<span className="capitalize">{template.category}</span>
</div>
</div>
</div>
</div>
))
)}
</div>
{/* 도움말 */}
<div className="rounded-lg bg-blue-50 p-3">
<div className="flex items-start space-x-2">
<Info className="mt-0.5 h-4 w-4 flex-shrink-0 text-blue-600" />
<div className="text-xs text-blue-700">
<p className="mb-1 font-medium"> </p>
<p>릿 .</p>
</div>
</div>
</div>
</div>
);
};
export default TemplatesPanel;