"use client"; import React, { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Mail, Type, Image as ImageIcon, Square, MousePointer, Eye, Send, Save, Plus, Trash2, Settings, Upload, X, GripVertical, ChevronUp, ChevronDown, LayoutTemplate, Table2, AlertCircle, Minus, Building2, ListOrdered } from "lucide-react"; import { getMailTemplates } from "@/lib/api/mail"; export interface MailComponent { id: string; type: "text" | "button" | "image" | "spacer" | "table" | "header" | "infoTable" | "alertBox" | "divider" | "footer" | "numberedList"; content?: string; text?: string; url?: string; src?: string; height?: number; styles?: Record; // 헤더 컴포넌트용 logoSrc?: string; brandName?: string; sendDate?: string; headerBgColor?: string; // 정보 테이블용 rows?: Array<{ label: string; value: string }>; tableTitle?: string; // 강조 박스용 alertType?: "info" | "warning" | "danger" | "success"; alertTitle?: string; // 푸터용 companyName?: string; ceoName?: string; businessNumber?: string; address?: string; phone?: string; email?: string; copyright?: string; // 번호 리스트용 listItems?: string[]; listTitle?: string; } export interface QueryConfig { id: string; name: string; sql: string; parameters: Array<{ name: string; type: string; value?: any; }>; } interface MailDesignerProps { templateId?: string; onSave?: (data: any) => void; onPreview?: (data: any) => void; onSend?: (data: any) => void; } export default function MailDesigner({ templateId, onSave, onPreview, onSend, }: MailDesignerProps) { const [components, setComponents] = useState([]); const [selectedComponent, setSelectedComponent] = useState(null); const [templateName, setTemplateName] = useState(""); const [subject, setSubject] = useState(""); const [queries, setQueries] = useState([]); const [isLoading, setIsLoading] = useState(false); // 드래그 앤 드롭 상태 const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); // 템플릿 데이터 로드 (수정 모드) useEffect(() => { if (templateId) { loadTemplate(templateId); } }, [templateId]); const loadTemplate = async (id: string) => { try { setIsLoading(true); const templates = await getMailTemplates(); const template = templates.find(t => t.id === id); if (template) { setTemplateName(template.name); setSubject(template.subject); setComponents(template.components || []); console.log('✅ 템플릿 로드 완료:', { name: template.name, components: template.components?.length || 0 }); } } catch (error) { console.error('❌ 템플릿 로드 실패:', error); } finally { setIsLoading(false); } }; // 컴포넌트 타입 정의 const componentTypes = [ // 레이아웃 컴포넌트 { type: "header", icon: LayoutTemplate, label: "헤더", color: "bg-indigo-100 hover:bg-indigo-200", category: "layout" }, { type: "divider", icon: Minus, label: "구분선", color: "bg-gray-100 hover:bg-gray-200", category: "layout" }, { type: "spacer", icon: Square, label: "여백", color: "bg-muted hover:bg-muted/80", category: "layout" }, { type: "footer", icon: Building2, label: "푸터", color: "bg-slate-100 hover:bg-slate-200", category: "layout" }, // 컨텐츠 컴포넌트 { type: "text", icon: Type, label: "텍스트", color: "bg-primary/20 hover:bg-blue-200", category: "content" }, { type: "button", icon: MousePointer, label: "버튼", color: "bg-success/20 hover:bg-success/30", category: "content" }, { type: "image", icon: ImageIcon, label: "이미지", color: "bg-purple-100 hover:bg-purple-200", category: "content" }, { type: "infoTable", icon: Table2, label: "정보 테이블", color: "bg-cyan-100 hover:bg-cyan-200", category: "content" }, { type: "alertBox", icon: AlertCircle, label: "안내 박스", color: "bg-amber-100 hover:bg-amber-200", category: "content" }, { type: "numberedList", icon: ListOrdered, label: "번호 리스트", color: "bg-emerald-100 hover:bg-emerald-200", category: "content" }, ]; // 컴포넌트 추가 const addComponent = (type: string) => { const newComponent: MailComponent = { id: `comp-${Date.now()}`, type: type as any, content: type === "text" ? "" : undefined, text: type === "button" ? "버튼 텍스트" : undefined, url: type === "button" || type === "image" ? "" : undefined, src: type === "image" ? "https://placehold.co/600x200/e5e7eb/64748b?text=이미지를+업로드하세요" : undefined, height: type === "spacer" ? 30 : type === "divider" ? 1 : undefined, styles: { padding: type === "divider" ? "0" : "10px", backgroundColor: type === "button" ? "#007bff" : "transparent", color: type === "button" ? "#fff" : "#333", }, // 헤더 기본값 logoSrc: type === "header" ? "" : undefined, brandName: type === "header" ? "회사명" : undefined, sendDate: type === "header" ? new Date().toLocaleDateString("ko-KR") : undefined, headerBgColor: type === "header" ? "#f8f9fa" : undefined, // 정보 테이블 기본값 rows: type === "infoTable" ? [{ label: "항목", value: "내용" }] : undefined, tableTitle: type === "infoTable" ? "" : undefined, // 안내 박스 기본값 alertType: type === "alertBox" ? "info" : undefined, alertTitle: type === "alertBox" ? "안내" : undefined, // 푸터 기본값 companyName: type === "footer" ? "회사명" : undefined, ceoName: type === "footer" ? "" : undefined, businessNumber: type === "footer" ? "" : undefined, address: type === "footer" ? "" : undefined, phone: type === "footer" ? "" : undefined, email: type === "footer" ? "" : undefined, copyright: type === "footer" ? `© ${new Date().getFullYear()} All rights reserved.` : undefined, // 번호 리스트 기본값 listItems: type === "numberedList" ? ["첫 번째 항목"] : undefined, listTitle: type === "numberedList" ? "" : undefined, }; setComponents([...components, newComponent]); }; // 드래그 앤 드롭 핸들러 const handleDragStart = (index: number) => { setDraggedIndex(index); }; const handleDragOver = (e: React.DragEvent, index: number) => { e.preventDefault(); if (draggedIndex !== null && draggedIndex !== index) { setDragOverIndex(index); } }; const handleDrop = (index: number) => { if (draggedIndex !== null && draggedIndex !== index) { moveComponent(draggedIndex, index); } setDraggedIndex(null); setDragOverIndex(null); }; const handleDragEnd = () => { setDraggedIndex(null); setDragOverIndex(null); }; const moveComponent = (fromIndex: number, toIndex: number) => { const newComponents = [...components]; const [movedItem] = newComponents.splice(fromIndex, 1); newComponents.splice(toIndex, 0, movedItem); setComponents(newComponents); }; // 컴포넌트 삭제 const removeComponent = (id: string) => { setComponents(components.filter(c => c.id !== id)); if (selectedComponent === id) { setSelectedComponent(null); } }; // 컴포넌트 선택 const selectComponent = (id: string) => { setSelectedComponent(id); }; // 컴포넌트 내용 업데이트 const updateComponent = (id: string, updates: Partial) => { setComponents( components.map(c => c.id === id ? { ...c, ...updates } : c) ); }; // 저장 const handleSave = () => { const data = { name: templateName, subject, components, queries, }; if (onSave) { onSave(data); } }; // 미리보기 const handlePreview = () => { if (onPreview) { onPreview({ components, subject }); } }; // 발송 const handleSend = () => { if (onSend) { onSend({ components, subject, queries }); } }; // 선택된 컴포넌트 가져오기 const selected = components.find(c => c.id === selectedComponent); // 로딩 중일 때 if (isLoading) { return (

템플릿을 불러오는 중...

); } return (
{/* 왼쪽: 컴포넌트 팔레트 */}
{/* 레이아웃 컴포넌트 */}

레이아웃

{componentTypes.filter(c => c.category === "layout").map(({ type, icon: Icon, label, color }) => ( ))}
{/* 컨텐츠 컴포넌트 */}

컨텐츠

{componentTypes.filter(c => c.category === "content").map(({ type, icon: Icon, label, color }) => ( ))}
{/* 템플릿 정보 */} 템플릿 정보
setTemplateName(e.target.value)} placeholder="예: 고객 환영 메일" className="mt-1" />
setSubject(e.target.value)} placeholder="예: {customer_name}님 환영합니다!" className="mt-1" />
{/* 액션 버튼 */}
{/* 중앙: 캔버스 */}
메일 미리보기 {components.length}개 컴포넌트 {/* 제목 영역 */} {subject && (

제목:

{subject}

)} {/* 컴포넌트 렌더링 */}
{components.length === 0 ? (

왼쪽에서 컴포넌트를 추가하세요

) : ( components.map((comp, index) => (
handleDragStart(index)} onDragOver={(e) => handleDragOver(e, index)} onDrop={() => handleDrop(index)} onDragEnd={handleDragEnd} onClick={() => selectComponent(comp.id)} className={`relative group cursor-pointer rounded-lg transition-all ${ selectedComponent === comp.id ? "ring-2 ring-orange-500 bg-orange-50/30" : "hover:ring-2 hover:ring-gray-300" } ${draggedIndex === index ? "opacity-50 scale-95" : ""} ${ dragOverIndex === index ? "ring-2 ring-primary ring-dashed bg-primary/10" : "" }`} style={comp.styles} > {/* 드래그 핸들 & 순서 이동 버튼 */}
{/* 순서 배지 */}
{index + 1}
{/* 삭제 버튼 */} {/* 컴포넌트 내용 */} {comp.type === "text" && (
)} {comp.type === "button" && ( {comp.text} )} {comp.type === "image" && ( 메일 이미지 )} {comp.type === "spacer" && (
여백 {comp.height}px
)} {comp.type === "header" && (
{comp.logoSrc && 로고} {comp.brandName}
{comp.sendDate}
)} {comp.type === "infoTable" && (
{comp.tableTitle && (
{comp.tableTitle}
)} {comp.rows?.map((row, i) => ( ))}
{row.label} {row.value}
)} {comp.type === "alertBox" && (
{comp.alertTitle &&
{comp.alertTitle}
}
{comp.content}
)} {comp.type === "divider" && (
)} {comp.type === "footer" && (
{comp.companyName &&
{comp.companyName}
} {(comp.ceoName || comp.businessNumber) && (
{comp.ceoName && 대표: {comp.ceoName}} {comp.ceoName && comp.businessNumber && |} {comp.businessNumber && 사업자등록번호: {comp.businessNumber}}
)} {comp.address &&
{comp.address}
} {(comp.phone || comp.email) && (
{comp.phone && Tel: {comp.phone}} {comp.phone && comp.email && |} {comp.email && Email: {comp.email}}
)} {comp.copyright &&
{comp.copyright}
}
)} {comp.type === "numberedList" && (
{comp.listTitle &&
{comp.listTitle}
}
    {comp.listItems?.map((item, i) => (
  1. {item}
  2. ))}
)}
)) )}
{/* 오른쪽: 속성 패널 */}
{selected ? (

속성 편집

{/* 텍스트 컴포넌트 */} {selected.type === "text" && (

메일에 표시될 텍스트를 입력하세요