"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 { LoadingSpinner } from "@/components/common/LoadingSpinner"; 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, RefreshCw, } from "lucide-react"; import { useTemplates, TemplateStandard } from "@/hooks/admin/useTemplates"; import { toast } from "sonner"; // 템플릿 컴포넌트 타입 정의 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 getIconByName = (iconName?: string): React.ReactNode => { const iconMap: Record = { table: , "mouse-pointer": , upload: , layout: , form: , grid: , folder: , square: , columns: , rows: , card: , sidebar: , }; return iconMap[iconName || ""] || ; }; // TemplateStandard를 TemplateComponent로 변환하는 함수 const convertTemplateStandardToComponent = (template: TemplateStandard): TemplateComponent => { return { id: template.template_code, name: template.template_name, description: template.description || "", category: template.category as TemplateComponent["category"], icon: getIconByName(template.icon_name), defaultSize: template.default_size || { width: 300, height: 200 }, components: template.layout_config?.components || [], }; }; // 폴백 템플릿 (데이터베이스 연결 실패 시) const fallbackTemplates: TemplateComponent[] = [ // 고급 데이터 테이블 템플릿 { id: "advanced-data-table", name: "고급 데이터 테이블", description: "컬럼 설정, 필터링, 페이지네이션이 포함된 완전한 데이터 테이블", category: "table", icon:
, 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: "area-box", name: "기본 박스 영역", description: "컴포넌트들을 그룹화할 수 있는 기본 박스 형태의 영역", category: "area", icon: , 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: , 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: , 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: , 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: , 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: , 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: , 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: , 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: , // 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 = ({ onDragStart }) => { const [searchTerm, setSearchTerm] = React.useState(""); const [selectedCategory, setSelectedCategory] = React.useState("all"); // 동적 템플릿 데이터 조회 const { templates: dbTemplates, categories: dbCategories, isLoading, error, refetch, } = useTemplates({ active: "Y", // 활성화된 템플릿만 조회 is_public: "Y", // 공개 템플릿만 조회 (회사별 템플릿도 포함됨) }); // 데이터베이스 템플릿을 TemplateComponent 형태로 변환 const dynamicTemplates = React.useMemo(() => { if (error || !dbTemplates) { // 오류 발생 시 폴백 템플릿 사용 // console.warn("템플릿 로딩 실패, 폴백 템플릿 사용:", error); return fallbackTemplates; } return dbTemplates.map(convertTemplateStandardToComponent); }, [dbTemplates, error]); // 카테고리 목록 동적 생성 const categories = React.useMemo(() => { const allCategories = [{ id: "all", name: "전체", icon: }]; if (dbCategories && dbCategories.length > 0) { // 데이터베이스에서 가져온 카테고리 사용 dbCategories.forEach((category) => { const icon = getIconByName(category); allCategories.push({ id: category, name: category, icon, }); }); } else { // 폴백 카테고리 (실제 템플릿만) allCategories.push( { id: "area", name: "영역", icon: }, { id: "table", name: "테이블", icon:
}, ); } return allCategories; }, [dbCategories]); const filteredTemplates = dynamicTemplates.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 (
{/* 헤더 */}

템플릿

캔버스로 드래그하여 화면을 구성하세요

{/* 검색 */}
setSearchTerm(e.target.value)} className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors" />
{/* 카테고리 필터 */}
{categories.map((category) => ( ))}
{/* 새로고침 버튼 */} {error && (
템플릿 로딩 실패, 기본 템플릿 사용 중
)} {/* 템플릿 목록 */}
{isLoading ? (
템플릿 로딩 중...
) : filteredTemplates.length === 0 ? (

템플릿을 찾을 수 없습니다

검색어나 필터를 조정해보세요

) : ( filteredTemplates.map((template) => (
{ onDragStart(e, template); // 드래그 시작 시 시각적 피드백 e.currentTarget.style.opacity = '0.6'; e.currentTarget.style.transform = 'rotate(2deg) scale(0.98)'; }} onDragEnd={(e) => { // 드래그 종료 시 원래 상태로 복원 e.currentTarget.style.opacity = '1'; e.currentTarget.style.transform = 'none'; }} className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-6 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-blue-500/15 hover:scale-[1.02] hover:border-blue-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0" >
{template.icon}

{template.name}

{template.components.length}

{template.description}

{template.defaultSize.width}×{template.defaultSize.height}
{template.category}
)) )}
{/* 도움말 */}

사용 방법

템플릿을 캔버스로 드래그하여 빠르게 화면을 구성하세요.

); }; export default TemplatesPanel;