138 lines
3.8 KiB
TypeScript
138 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useDrag } from "react-dnd";
|
|
import {
|
|
Type,
|
|
Table,
|
|
Image,
|
|
Minus,
|
|
PenLine,
|
|
Stamp as StampIcon,
|
|
Hash,
|
|
CreditCard,
|
|
Calculator,
|
|
Barcode,
|
|
CheckSquare,
|
|
ChevronRight,
|
|
LayoutGrid,
|
|
FileText,
|
|
Database,
|
|
} from "lucide-react";
|
|
|
|
interface ComponentItem {
|
|
type: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
interface ComponentCategory {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
items: ComponentItem[];
|
|
}
|
|
|
|
const CATEGORIES: ComponentCategory[] = [
|
|
{
|
|
id: "basic",
|
|
label: "기본 요소",
|
|
icon: <LayoutGrid className="h-5 w-5" />,
|
|
items: [
|
|
{ type: "text", label: "텍스트", icon: <Type className="h-5 w-5" /> },
|
|
{ type: "image", label: "이미지", icon: <Image className="h-5 w-5" /> },
|
|
{ type: "divider", label: "구분선", icon: <Minus className="h-5 w-5" /> },
|
|
],
|
|
},
|
|
{
|
|
id: "data",
|
|
label: "데이터 표시",
|
|
icon: <Database className="h-5 w-5" />,
|
|
items: [
|
|
{ type: "table", label: "테이블", icon: <Table className="h-5 w-5" /> },
|
|
{ type: "card", label: "정보카드", icon: <CreditCard className="h-5 w-5" /> },
|
|
{ type: "calculation", label: "계산", icon: <Calculator className="h-5 w-5" /> },
|
|
],
|
|
},
|
|
{
|
|
id: "form",
|
|
label: "입력/인증",
|
|
icon: <FileText className="h-5 w-5" />,
|
|
items: [
|
|
{ type: "checkbox", label: "체크박스", icon: <CheckSquare className="h-5 w-5" /> },
|
|
{ type: "signature", label: "서명란", icon: <PenLine className="h-5 w-5" /> },
|
|
{ type: "stamp", label: "도장란", icon: <StampIcon className="h-5 w-5" /> },
|
|
{ type: "barcode", label: "바코드/QR", icon: <Barcode className="h-5 w-5" /> },
|
|
],
|
|
},
|
|
];
|
|
|
|
function DraggableComponentItem({ type, label, icon }: ComponentItem) {
|
|
const [{ isDragging }, drag] = useDrag(() => ({
|
|
type: "component",
|
|
item: { componentType: type },
|
|
collect: (monitor) => ({
|
|
isDragging: monitor.isDragging(),
|
|
}),
|
|
}));
|
|
|
|
return (
|
|
<div
|
|
ref={drag}
|
|
className={`flex h-20 cursor-move flex-col items-center justify-center gap-1.5 rounded-lg border border-gray-200 bg-gray-50 transition-colors hover:border-indigo-400 hover:bg-indigo-50 ${
|
|
isDragging ? "opacity-50" : ""
|
|
}`}
|
|
>
|
|
<div className="text-gray-600">{icon}</div>
|
|
<span className="text-xs font-medium text-gray-700">{label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ComponentPalette() {
|
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
|
new Set(CATEGORIES.map((c) => c.id)),
|
|
);
|
|
|
|
const toggleCategory = (id: string) => {
|
|
setExpandedCategories((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{CATEGORIES.map((category) => {
|
|
const isExpanded = expandedCategories.has(category.id);
|
|
return (
|
|
<div key={category.id}>
|
|
<button
|
|
onClick={() => toggleCategory(category.id)}
|
|
className="flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-100"
|
|
>
|
|
<ChevronRight
|
|
className={`h-4 w-4 text-gray-400 transition-transform ${isExpanded ? "rotate-90" : ""}`}
|
|
/>
|
|
<span className="text-gray-500">{category.icon}</span>
|
|
{category.label}
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="grid grid-cols-2 gap-3 px-1 pt-2 pb-3">
|
|
{category.items.map((item) => (
|
|
<DraggableComponentItem key={item.type} {...item} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|