555 lines
16 KiB
TypeScript
555 lines
16 KiB
TypeScript
"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,
|
||
Square,
|
||
CreditCard,
|
||
Layout,
|
||
Columns,
|
||
Rows,
|
||
SidebarOpen,
|
||
Folder,
|
||
ChevronDown,
|
||
} from "lucide-react";
|
||
|
||
// 템플릿 컴포넌트 타입 정의
|
||
export interface TemplateComponent {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
category: "table" | "button" | "form" | "layout" | "chart" | "status" | "file" | "area";
|
||
icon: React.ReactNode;
|
||
defaultSize: { width: number; height: number };
|
||
components: Array<{
|
||
type: "widget" | "container" | "datatable" | "file" | "area";
|
||
widgetType?: string;
|
||
label: string;
|
||
placeholder?: string;
|
||
position: { x: number; y: number };
|
||
size: { width: number; height: number };
|
||
style?: any;
|
||
required?: boolean;
|
||
readonly?: boolean;
|
||
parentId?: string;
|
||
title?: string;
|
||
// 영역 컴포넌트 전용 속성
|
||
layoutType?: string;
|
||
description?: string;
|
||
layoutConfig?: any;
|
||
areaStyle?: any;
|
||
}>;
|
||
}
|
||
|
||
// 미리 정의된 템플릿 컴포넌트들
|
||
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",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// === 영역 템플릿들 ===
|
||
|
||
// 기본 박스 영역
|
||
{
|
||
id: "area-box",
|
||
name: "기본 박스 영역",
|
||
description: "컴포넌트들을 그룹화할 수 있는 기본 박스 형태의 영역",
|
||
category: "area",
|
||
icon: <Square className="h-4 w-4" />,
|
||
defaultSize: { width: 400, height: 300 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "박스 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 400, height: 300 },
|
||
layoutType: "box",
|
||
title: "박스 영역",
|
||
description: "컴포넌트들을 그룹화할 수 있는 기본 박스",
|
||
layoutConfig: {},
|
||
areaStyle: {
|
||
backgroundColor: "#f9fafb",
|
||
borderWidth: 1,
|
||
borderStyle: "solid",
|
||
borderColor: "#d1d5db",
|
||
borderRadius: 8,
|
||
padding: 16,
|
||
margin: 0,
|
||
shadow: "none",
|
||
},
|
||
style: {
|
||
border: "1px solid #d1d5db",
|
||
borderRadius: "8px",
|
||
backgroundColor: "#f9fafb",
|
||
padding: "16px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 카드 영역
|
||
{
|
||
id: "area-card",
|
||
name: "카드 영역",
|
||
description: "그림자와 둥근 모서리가 있는 카드 형태의 영역",
|
||
category: "area",
|
||
icon: <CreditCard className="h-4 w-4" />,
|
||
defaultSize: { width: 400, height: 300 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "카드 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 400, height: 300 },
|
||
layoutType: "card",
|
||
title: "카드 영역",
|
||
description: "그림자와 둥근 모서리가 있는 카드 형태",
|
||
layoutConfig: {},
|
||
areaStyle: {
|
||
backgroundColor: "#ffffff",
|
||
borderWidth: 0,
|
||
borderStyle: "none",
|
||
borderColor: "#e5e7eb",
|
||
borderRadius: 12,
|
||
padding: 20,
|
||
margin: 0,
|
||
shadow: "md",
|
||
},
|
||
style: {
|
||
backgroundColor: "#ffffff",
|
||
borderRadius: "12px",
|
||
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
||
padding: "20px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 패널 영역 (헤더 포함)
|
||
{
|
||
id: "area-panel",
|
||
name: "패널 영역",
|
||
description: "제목 헤더가 포함된 패널 형태의 영역",
|
||
category: "area",
|
||
icon: <Layout className="h-4 w-4" />,
|
||
defaultSize: { width: 500, height: 400 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "패널 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 500, height: 400 },
|
||
layoutType: "panel",
|
||
title: "패널 제목",
|
||
style: {
|
||
backgroundColor: "#ffffff",
|
||
border: "1px solid #e5e7eb",
|
||
borderRadius: "8px",
|
||
headerBackgroundColor: "#f3f4f6",
|
||
headerHeight: 48,
|
||
headerPadding: 16,
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 그리드 영역
|
||
{
|
||
id: "area-grid",
|
||
name: "그리드 영역",
|
||
description: "내부 컴포넌트들을 격자 형태로 배치하는 영역",
|
||
category: "area",
|
||
icon: <Grid3x3 className="h-4 w-4" />,
|
||
defaultSize: { width: 600, height: 400 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "그리드 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 600, height: 400 },
|
||
layoutType: "grid",
|
||
title: "그리드 영역",
|
||
description: "격자 형태로 컴포넌트 배치",
|
||
layoutConfig: {
|
||
gridColumns: 3,
|
||
gridRows: 2,
|
||
gridGap: 16,
|
||
},
|
||
areaStyle: {
|
||
backgroundColor: "#ffffff",
|
||
borderWidth: 1,
|
||
borderStyle: "solid",
|
||
borderColor: "#d1d5db",
|
||
borderRadius: 8,
|
||
padding: 16,
|
||
margin: 0,
|
||
shadow: "none",
|
||
showGridLines: true,
|
||
gridLineColor: "#e5e7eb",
|
||
},
|
||
style: {
|
||
backgroundColor: "#ffffff",
|
||
border: "1px solid #d1d5db",
|
||
borderRadius: "8px",
|
||
padding: "16px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 가로 플렉스 영역
|
||
{
|
||
id: "area-flex-row",
|
||
name: "가로 배치 영역",
|
||
description: "내부 컴포넌트들을 가로로 나란히 배치하는 영역",
|
||
category: "area",
|
||
icon: <Columns className="h-4 w-4" />,
|
||
defaultSize: { width: 600, height: 200 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "가로 배치 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 600, height: 200 },
|
||
layoutType: "flex-row",
|
||
layoutConfig: {
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
gap: 16,
|
||
},
|
||
style: {
|
||
backgroundColor: "#f8fafc",
|
||
border: "1px solid #cbd5e1",
|
||
borderRadius: "8px",
|
||
padding: "16px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 세로 플렉스 영역
|
||
{
|
||
id: "area-flex-column",
|
||
name: "세로 배치 영역",
|
||
description: "내부 컴포넌트들을 세로로 순차 배치하는 영역",
|
||
category: "area",
|
||
icon: <Rows className="h-4 w-4" />,
|
||
defaultSize: { width: 300, height: 500 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "세로 배치 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 300, height: 500 },
|
||
layoutType: "flex-column",
|
||
layoutConfig: {
|
||
flexDirection: "column",
|
||
justifyContent: "flex-start",
|
||
alignItems: "stretch",
|
||
gap: 12,
|
||
},
|
||
style: {
|
||
backgroundColor: "#f1f5f9",
|
||
border: "1px solid #94a3b8",
|
||
borderRadius: "8px",
|
||
padding: "16px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 사이드바 영역
|
||
{
|
||
id: "area-sidebar",
|
||
name: "사이드바 영역",
|
||
description: "사이드바와 메인 컨텐츠 영역으로 구분된 레이아웃",
|
||
category: "area",
|
||
icon: <SidebarOpen className="h-4 w-4" />,
|
||
defaultSize: { width: 700, height: 400 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "사이드바 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 700, height: 400 },
|
||
layoutType: "sidebar",
|
||
layoutConfig: {
|
||
sidebarPosition: "left",
|
||
sidebarWidth: 200,
|
||
collapsible: true,
|
||
},
|
||
style: {
|
||
backgroundColor: "#ffffff",
|
||
border: "1px solid #e2e8f0",
|
||
borderRadius: "8px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 탭 영역
|
||
{
|
||
id: "area-tabs",
|
||
name: "탭 영역",
|
||
description: "탭으로 구분된 여러 컨텐츠 영역을 제공하는 레이아웃",
|
||
category: "area",
|
||
icon: <Folder className="h-4 w-4" />,
|
||
defaultSize: { width: 600, height: 400 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "탭 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 600, height: 400 },
|
||
layoutType: "tabs",
|
||
layoutConfig: {
|
||
tabPosition: "top",
|
||
defaultActiveTab: "tab1",
|
||
},
|
||
style: {
|
||
backgroundColor: "#ffffff",
|
||
border: "1px solid #d1d5db",
|
||
borderRadius: "8px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
|
||
// 아코디언 영역
|
||
{
|
||
id: "area-accordion",
|
||
name: "아코디언 영역",
|
||
description: "접고 펼칠 수 있는 섹션들로 구성된 영역",
|
||
category: "area",
|
||
icon: <ChevronDown className="h-4 w-4" />,
|
||
defaultSize: { width: 500, height: 600 },
|
||
components: [
|
||
{
|
||
type: "area",
|
||
label: "아코디언 영역",
|
||
position: { x: 0, y: 0 },
|
||
size: { width: 500, height: 600 },
|
||
layoutType: "accordion",
|
||
layoutConfig: {
|
||
allowMultiple: false,
|
||
defaultExpanded: ["section1"],
|
||
},
|
||
style: {
|
||
backgroundColor: "#ffffff",
|
||
border: "1px solid #e5e7eb",
|
||
borderRadius: "8px",
|
||
},
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
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: "area", name: "영역", icon: <Layout 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;
|