ERP-node/frontend/components/mail/MailDesigner.tsx

1110 lines
44 KiB
TypeScript

"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<string, string>;
// 헤더 컴포넌트용
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<MailComponent[]>([]);
const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
const [templateName, setTemplateName] = useState("");
const [subject, setSubject] = useState("");
const [queries, setQueries] = useState<QueryConfig[]>([]);
const [isLoading, setIsLoading] = useState(false);
// 드래그 앤 드롭 상태
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(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<MailComponent>) => {
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 (
<div className="flex items-center justify-center h-screen bg-muted/30">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-500 mx-auto mb-4"></div>
<p className="text-muted-foreground">릿 ...</p>
</div>
</div>
);
}
return (
<div className="flex h-screen bg-muted/30">
{/* 왼쪽: 컴포넌트 팔레트 */}
<div className="w-64 bg-white border-r p-4 space-y-4 overflow-y-auto">
{/* 레이아웃 컴포넌트 */}
<div>
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center">
<LayoutTemplate className="w-4 h-4 mr-2 text-indigo-500" />
</h3>
<div className="space-y-2">
{componentTypes.filter(c => c.category === "layout").map(({ type, icon: Icon, label, color }) => (
<Button
key={type}
onClick={() => addComponent(type)}
variant="outline"
className={`w-full justify-start ${color} border`}
>
<Icon className="w-4 h-4 mr-2" />
{label}
</Button>
))}
</div>
</div>
{/* 컨텐츠 컴포넌트 */}
<div>
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center">
<Mail className="w-4 h-4 mr-2 text-primary" />
</h3>
<div className="space-y-2">
{componentTypes.filter(c => c.category === "content").map(({ type, icon: Icon, label, color }) => (
<Button
key={type}
onClick={() => addComponent(type)}
variant="outline"
className={`w-full justify-start ${color} border`}
>
<Icon className="w-4 h-4 mr-2" />
{label}
</Button>
))}
</div>
</div>
{/* 템플릿 정보 */}
<Card>
<CardHeader>
<CardTitle className="text-sm">릿 </CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<Label className="text-xs">릿 </Label>
<Input
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="예: 고객 환영 메일"
className="mt-1"
/>
</div>
<div>
<Label className="text-xs"></Label>
<Input
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="예: {customer_name}님 환영합니다!"
className="mt-1"
/>
</div>
</CardContent>
</Card>
{/* 액션 버튼 */}
<div className="space-y-2">
<Button onClick={handleSave} className="w-full" variant="default">
<Save className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handlePreview} className="w-full" variant="outline">
<Eye className="w-4 h-4 mr-2" />
</Button>
<Button onClick={handleSend} variant="default" className="w-full">
<Send className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 중앙: 캔버스 */}
<div className="flex-1 p-8 overflow-y-auto">
<Card className="max-w-3xl mx-auto">
<CardHeader className="bg-gradient-to-r from-muted to-muted border-b">
<CardTitle className="flex items-center justify-between">
<span> </span>
<span className="text-sm text-muted-foreground font-normal">
{components.length}
</span>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* 제목 영역 */}
{subject && (
<div className="p-6 bg-muted/30 border-b">
<p className="text-sm text-muted-foreground">:</p>
<p className="font-semibold text-lg">{subject}</p>
</div>
)}
{/* 컴포넌트 렌더링 */}
<div className="p-6 pl-14 space-y-4">
{components.length === 0 ? (
<div className="text-center py-16 text-muted-foreground/50">
<Mail className="w-16 h-16 mx-auto mb-4 opacity-20" />
<p> </p>
</div>
) : (
components.map((comp, index) => (
<div
key={comp.id}
draggable
onDragStart={() => 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}
>
{/* 드래그 핸들 & 순서 이동 버튼 */}
<div className="absolute -left-10 top-1/2 -translate-y-1/2 flex flex-col items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => { e.stopPropagation(); if (index > 0) moveComponent(index, index - 1); }}
className="p-1 hover:bg-gray-200 rounded disabled:opacity-30"
disabled={index === 0}
>
<ChevronUp className="w-3 h-3" />
</button>
<div className="cursor-grab active:cursor-grabbing p-1 hover:bg-gray-200 rounded">
<GripVertical className="w-4 h-4 text-gray-400" />
</div>
<button
onClick={(e) => { e.stopPropagation(); if (index < components.length - 1) moveComponent(index, index + 1); }}
className="p-1 hover:bg-gray-200 rounded disabled:opacity-30"
disabled={index === components.length - 1}
>
<ChevronDown className="w-3 h-3" />
</button>
</div>
{/* 순서 배지 */}
<div className="absolute -left-10 top-0 text-xs text-gray-400 opacity-0 group-hover:opacity-100">
{index + 1}
</div>
{/* 삭제 버튼 */}
<button
onClick={(e) => {
e.stopPropagation();
removeComponent(comp.id);
}}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-destructive text-white rounded-full p-1 hover:bg-destructive/90"
>
<Trash2 className="w-4 h-4" />
</button>
{/* 컴포넌트 내용 */}
{comp.type === "text" && (
<div dangerouslySetInnerHTML={{ __html: comp.content || "" }} />
)}
{comp.type === "button" && (
<a
href={comp.url}
target="_blank"
rel="noopener noreferrer"
className="inline-block px-6 py-3 rounded-md"
style={comp.styles}
>
{comp.text}
</a>
)}
{comp.type === "image" && (
<img src={comp.src} alt="메일 이미지" className="w-full rounded" />
)}
{comp.type === "spacer" && (
<div style={{ height: `${comp.height}px` }} className="bg-gray-100 rounded flex items-center justify-center text-xs text-gray-400">
{comp.height}px
</div>
)}
{comp.type === "header" && (
<div className="p-4 rounded-lg" style={{ backgroundColor: comp.headerBgColor || "#f8f9fa" }}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{comp.logoSrc && <img src={comp.logoSrc} alt="로고" className="h-10" />}
<span className="font-bold text-lg">{comp.brandName}</span>
</div>
<span className="text-sm text-gray-500">{comp.sendDate}</span>
</div>
</div>
)}
{comp.type === "infoTable" && (
<div className="border rounded-lg overflow-hidden">
{comp.tableTitle && (
<div className="bg-gray-50 px-4 py-2 font-semibold border-b">{comp.tableTitle}</div>
)}
<table className="w-full">
<tbody>
{comp.rows?.map((row, i) => (
<tr key={i} className={i % 2 === 0 ? "bg-white" : "bg-gray-50"}>
<td className="px-4 py-2 font-medium text-gray-600 w-1/3 border-r">{row.label}</td>
<td className="px-4 py-2">{row.value}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{comp.type === "alertBox" && (
<div className={`p-4 rounded-lg border-l-4 ${
comp.alertType === "info" ? "bg-blue-50 border-blue-500 text-blue-800" :
comp.alertType === "warning" ? "bg-amber-50 border-amber-500 text-amber-800" :
comp.alertType === "danger" ? "bg-red-50 border-red-500 text-red-800" :
"bg-emerald-50 border-emerald-500 text-emerald-800"
}`}>
{comp.alertTitle && <div className="font-bold mb-1">{comp.alertTitle}</div>}
<div>{comp.content}</div>
</div>
)}
{comp.type === "divider" && (
<hr className="border-gray-300" style={{ borderWidth: `${comp.height || 1}px` }} />
)}
{comp.type === "footer" && (
<div className="text-center text-sm text-gray-500 py-4 border-t bg-gray-50">
{comp.companyName && <div className="font-semibold text-gray-700">{comp.companyName}</div>}
{(comp.ceoName || comp.businessNumber) && (
<div className="mt-1">
{comp.ceoName && <span>: {comp.ceoName}</span>}
{comp.ceoName && comp.businessNumber && <span className="mx-2">|</span>}
{comp.businessNumber && <span>: {comp.businessNumber}</span>}
</div>
)}
{comp.address && <div className="mt-1">{comp.address}</div>}
{(comp.phone || comp.email) && (
<div className="mt-1">
{comp.phone && <span>Tel: {comp.phone}</span>}
{comp.phone && comp.email && <span className="mx-2">|</span>}
{comp.email && <span>Email: {comp.email}</span>}
</div>
)}
{comp.copyright && <div className="mt-2 text-xs text-gray-400">{comp.copyright}</div>}
</div>
)}
{comp.type === "numberedList" && (
<div className="p-4">
{comp.listTitle && <div className="font-semibold mb-2">{comp.listTitle}</div>}
<ol className="list-decimal list-inside space-y-1">
{comp.listItems?.map((item, i) => (
<li key={i}>{item}</li>
))}
</ol>
</div>
)}
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
{/* 오른쪽: 속성 패널 */}
<div className="w-80 bg-background border-l p-4 overflow-y-auto">
{selected ? (
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-foreground flex items-center">
<Settings className="w-4 h-4 mr-2 text-primary" />
</h3>
<Button
size="sm"
variant="ghost"
onClick={() => setSelectedComponent(null)}
>
</Button>
</div>
{/* 텍스트 컴포넌트 */}
{selected.type === "text" && (
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-foreground flex items-center gap-2">
</Label>
<p className="text-xs text-muted-foreground mt-1 mb-2">
</p>
<Textarea
value={(() => {
// 🎯 HTML 태그 자동 제거 (비개발자 친화적)
const content = selected.content || "";
const tmp = document.createElement("DIV");
tmp.innerHTML = content;
return tmp.textContent || tmp.innerText || "";
})()}
onChange={(e) =>
updateComponent(selected.id, { content: e.target.value })
}
onFocus={(e) => {
// 🎯 클릭 시 placeholder 같은 텍스트 자동 제거
const currentValue = e.target.value.trim();
if (currentValue === '텍스트를 입력하세요' ||
currentValue === '텍스트를 입력하세요...') {
e.target.value = '';
updateComponent(selected.id, { content: '' });
}
}}
rows={8}
className="mt-1"
placeholder="예) 안녕하세요! 특별한 소식을 전해드립니다..."
/>
</div>
</div>
)}
{/* 버튼 컴포넌트 */}
{selected.type === "button" && (
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-foreground flex items-center gap-2">
</Label>
<p className="text-xs text-muted-foreground mt-1 mb-2">
</p>
<Input
value={selected.text || ""}
onChange={(e) =>
updateComponent(selected.id, { text: e.target.value })
}
className="mt-1"
placeholder="예) 자세히 보기, 지금 시작하기"
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground flex items-center gap-2">
</Label>
<p className="text-xs text-muted-foreground mt-1 mb-2">
</p>
<Input
value={selected.url || ""}
onChange={(e) =>
updateComponent(selected.id, { url: e.target.value })
}
className="mt-1"
placeholder="예) https://www.example.com"
/>
</div>
<div>
<Label className="text-sm font-medium text-foreground flex items-center gap-2">
</Label>
<p className="text-xs text-muted-foreground mt-1 mb-2">
</p>
<div className="flex items-center gap-3">
<Input
type="color"
value={selected.styles?.backgroundColor || "#007bff"}
onChange={(e) =>
updateComponent(selected.id, {
styles: { ...selected.styles, backgroundColor: e.target.value },
})
}
className="w-16 h-10 cursor-pointer"
/>
<span className="text-sm text-muted-foreground">
{selected.styles?.backgroundColor || "#007bff"}
</span>
</div>
</div>
</div>
)}
{/* 이미지 컴포넌트 */}
{selected.type === "image" && (
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-foreground flex items-center gap-2">
</Label>
<p className="text-xs text-muted-foreground mt-1 mb-2">
</p>
{/* 파일 업로드 버튼 */}
<div className="space-y-3">
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (e: any) => {
const file = e.target?.files?.[0];
if (file) {
// 파일을 Base64로 변환
const reader = new FileReader();
reader.onload = (event) => {
updateComponent(selected.id, {
src: event.target?.result as string
});
};
reader.readAsDataURL(file);
}
};
input.click();
}}
>
<Upload className="w-4 h-4 mr-2" />
</Button>
{/* 구분선 */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border"></div>
</div>
<div className="relative flex justify-center text-xs">
<span className="px-2 bg-white text-muted-foreground"></span>
</div>
</div>
{/* URL 입력 */}
<div>
<Label className="text-xs text-muted-foreground mb-1 block">
</Label>
<Input
value={selected.src || ""}
onChange={(e) =>
updateComponent(selected.id, { src: e.target.value })
}
className="text-sm"
placeholder="예) https://example.com/image.jpg"
/>
</div>
</div>
</div>
{/* 미리보기 */}
{selected.src && (
<div className="mt-4">
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-muted-foreground">:</p>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => updateComponent(selected.id, { src: "" })}
className="h-6 text-xs text-red-600 hover:text-red-700 hover:bg-red-50"
>
<X className="w-3 h-3 mr-1" />
</Button>
</div>
<div className="border rounded-lg p-2 bg-muted/30">
<img
src={selected.src}
alt="미리보기"
className="w-full rounded"
onError={(e) => {
(e.target as HTMLImageElement).src = 'https://placehold.co/600x200?text=이미지+로드+실패';
}}
/>
</div>
</div>
)}
</div>
)}
{/* 여백 컴포넌트 */}
{selected.type === "spacer" && (
<div className="space-y-4">
<div>
<Label className="text-sm font-medium text-foreground flex items-center gap-2">
</Label>
<p className="text-xs text-muted-foreground mt-1 mb-2">
</p>
<div className="flex items-center gap-3">
<Input
type="number"
value={selected.height || 20}
onChange={(e) =>
updateComponent(selected.id, { height: parseInt(e.target.value) || 20 })
}
className="w-24"
min="0"
max="200"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
</div>
)}
{/* 헤더 컴포넌트 */}
{selected.type === "header" && (
<div className="space-y-4">
<div>
<Label></Label>
<Input
value={selected.brandName || ""}
onChange={(e) => updateComponent(selected.id, { brandName: e.target.value })}
placeholder="회사명"
className="mt-1"
/>
</div>
<div>
<Label> URL</Label>
<Input
value={selected.logoSrc || ""}
onChange={(e) => updateComponent(selected.id, { logoSrc: e.target.value })}
placeholder="https://example.com/logo.png"
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Input
value={selected.sendDate || ""}
onChange={(e) => updateComponent(selected.id, { sendDate: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label></Label>
<div className="flex items-center gap-3 mt-1">
<Input
type="color"
value={selected.headerBgColor || "#f8f9fa"}
onChange={(e) => updateComponent(selected.id, { headerBgColor: e.target.value })}
className="w-16 h-10"
/>
<span className="text-sm text-muted-foreground">{selected.headerBgColor || "#f8f9fa"}</span>
</div>
</div>
</div>
)}
{/* 정보 테이블 컴포넌트 */}
{selected.type === "infoTable" && (
<div className="space-y-4">
<div>
<Label> </Label>
<Input
value={selected.tableTitle || ""}
onChange={(e) => updateComponent(selected.id, { tableTitle: e.target.value })}
placeholder="예: 주문 정보"
className="mt-1"
/>
</div>
<div>
<Label> </Label>
<div className="space-y-2 mt-2">
{selected.rows?.map((row, i) => (
<div key={i} className="flex gap-2">
<Input
value={row.label}
onChange={(e) => {
const newRows = [...(selected.rows || [])];
newRows[i] = { ...newRows[i], label: e.target.value };
updateComponent(selected.id, { rows: newRows });
}}
placeholder="항목명"
className="flex-1"
/>
<Input
value={row.value}
onChange={(e) => {
const newRows = [...(selected.rows || [])];
newRows[i] = { ...newRows[i], value: e.target.value };
updateComponent(selected.id, { rows: newRows });
}}
placeholder="값"
className="flex-1"
/>
<Button
variant="ghost"
size="sm"
onClick={() => {
const newRows = selected.rows?.filter((_, idx) => idx !== i);
updateComponent(selected.id, { rows: newRows });
}}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => {
const newRows = [...(selected.rows || []), { label: "", value: "" }];
updateComponent(selected.id, { rows: newRows });
}}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</div>
)}
{/* 안내 박스 컴포넌트 */}
{selected.type === "alertBox" && (
<div className="space-y-4">
<div>
<Label> </Label>
<div className="grid grid-cols-2 gap-2 mt-2">
{(["info", "warning", "danger", "success"] as const).map((type) => (
<Button
key={type}
variant={selected.alertType === type ? "default" : "outline"}
size="sm"
onClick={() => updateComponent(selected.id, { alertType: type })}
className={
type === "info" ? "border-blue-300" :
type === "warning" ? "border-amber-300" :
type === "danger" ? "border-red-300" : "border-emerald-300"
}
>
{type === "info" ? "정보" : type === "warning" ? "주의" : type === "danger" ? "위험" : "성공"}
</Button>
))}
</div>
</div>
<div>
<Label></Label>
<Input
value={selected.alertTitle || ""}
onChange={(e) => updateComponent(selected.id, { alertTitle: e.target.value })}
placeholder="안내 제목"
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Textarea
value={selected.content || ""}
onChange={(e) => updateComponent(selected.id, { content: e.target.value })}
placeholder="안내 내용을 입력하세요"
rows={4}
className="mt-1"
/>
</div>
</div>
)}
{/* 구분선 컴포넌트 */}
{selected.type === "divider" && (
<div className="space-y-4">
<div>
<Label> </Label>
<div className="flex items-center gap-3 mt-1">
<Input
type="number"
value={selected.height || 1}
onChange={(e) => updateComponent(selected.id, { height: parseInt(e.target.value) || 1 })}
className="w-24"
min="1"
max="10"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
</div>
)}
{/* 푸터 컴포넌트 */}
{selected.type === "footer" && (
<div className="space-y-4">
<div>
<Label></Label>
<Input
value={selected.companyName || ""}
onChange={(e) => updateComponent(selected.id, { companyName: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Input
value={selected.ceoName || ""}
onChange={(e) => updateComponent(selected.id, { ceoName: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Input
value={selected.businessNumber || ""}
onChange={(e) => updateComponent(selected.id, { businessNumber: e.target.value })}
placeholder="000-00-00000"
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Input
value={selected.address || ""}
onChange={(e) => updateComponent(selected.id, { address: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Input
value={selected.phone || ""}
onChange={(e) => updateComponent(selected.id, { phone: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label></Label>
<Input
value={selected.email || ""}
onChange={(e) => updateComponent(selected.id, { email: e.target.value })}
className="mt-1"
/>
</div>
<div>
<Label> </Label>
<Input
value={selected.copyright || ""}
onChange={(e) => updateComponent(selected.id, { copyright: e.target.value })}
className="mt-1"
/>
</div>
</div>
)}
{/* 번호 리스트 컴포넌트 */}
{selected.type === "numberedList" && (
<div className="space-y-4">
<div>
<Label> </Label>
<Input
value={selected.listTitle || ""}
onChange={(e) => updateComponent(selected.id, { listTitle: e.target.value })}
placeholder="예: 안내 사항"
className="mt-1"
/>
</div>
<div>
<Label></Label>
<div className="space-y-2 mt-2">
{selected.listItems?.map((item, i) => (
<div key={i} className="flex gap-2">
<span className="flex items-center justify-center w-6 text-sm text-muted-foreground">{i + 1}.</span>
<Input
value={item}
onChange={(e) => {
const newItems = [...(selected.listItems || [])];
newItems[i] = e.target.value;
updateComponent(selected.id, { listItems: newItems });
}}
className="flex-1"
/>
<Button
variant="ghost"
size="sm"
onClick={() => {
const newItems = selected.listItems?.filter((_, idx) => idx !== i);
updateComponent(selected.id, { listItems: newItems });
}}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => {
const newItems = [...(selected.listItems || []), ""];
updateComponent(selected.id, { listItems: newItems });
}}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</div>
)}
</div>
) : (
<div className="text-center py-16 text-muted-foreground">
<Settings className="w-12 h-12 mx-auto mb-4 opacity-20" />
<p className="text-sm"> </p>
</div>
)}
</div>
</div>
);
}