290 lines
11 KiB
TypeScript
290 lines
11 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 { Plus, FileText, Loader2, RefreshCw, Search, ChevronRight } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import {
|
|
MailTemplate,
|
|
getMailTemplates,
|
|
createMailTemplate,
|
|
updateMailTemplate,
|
|
deleteMailTemplate,
|
|
CreateMailTemplateDto,
|
|
UpdateMailTemplateDto,
|
|
} from "@/lib/api/mail";
|
|
import MailTemplateCard from "@/components/mail/MailTemplateCard";
|
|
import MailTemplatePreviewModal from "@/components/mail/MailTemplatePreviewModal";
|
|
import MailTemplateEditorModal from "@/components/mail/MailTemplateEditorModal";
|
|
import ConfirmDeleteModal from "@/components/mail/ConfirmDeleteModal";
|
|
|
|
export default function MailTemplatesPage() {
|
|
const router = useRouter();
|
|
const [templates, setTemplates] = useState<MailTemplate[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [categoryFilter, setCategoryFilter] = useState<string>("all");
|
|
|
|
// 모달 상태
|
|
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
const [selectedTemplate, setSelectedTemplate] = useState<MailTemplate | null>(null);
|
|
const [editorMode, setEditorMode] = useState<"create" | "edit">("create");
|
|
|
|
// 템플릿 목록 불러오기
|
|
const loadTemplates = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await getMailTemplates();
|
|
setTemplates(data);
|
|
} catch (error) {
|
|
// console.error('템플릿 로드 실패:', error);
|
|
alert("템플릿 목록을 불러오는데 실패했습니다.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadTemplates();
|
|
}, []);
|
|
|
|
// 필터링된 템플릿
|
|
const filteredTemplates = templates.filter((template) => {
|
|
const matchesSearch =
|
|
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
template.subject.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesCategory = categoryFilter === "all" || template.category === categoryFilter;
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
// 카테고리 목록 추출
|
|
const categories = Array.from(new Set(templates.map((t) => t.category).filter(Boolean)));
|
|
|
|
const handleOpenCreateModal = () => {
|
|
setEditorMode("create");
|
|
setSelectedTemplate(null);
|
|
setIsEditorOpen(true);
|
|
};
|
|
|
|
const handleOpenEditModal = (template: MailTemplate) => {
|
|
setEditorMode("edit");
|
|
setSelectedTemplate(template);
|
|
setIsEditorOpen(true);
|
|
};
|
|
|
|
const handleOpenPreviewModal = (template: MailTemplate) => {
|
|
setSelectedTemplate(template);
|
|
setIsPreviewOpen(true);
|
|
};
|
|
|
|
const handleOpenDeleteModal = (template: MailTemplate) => {
|
|
setSelectedTemplate(template);
|
|
setIsDeleteModalOpen(true);
|
|
};
|
|
|
|
const handleSaveTemplate = async (data: CreateMailTemplateDto | UpdateMailTemplateDto) => {
|
|
try {
|
|
if (editorMode === "create") {
|
|
await createMailTemplate(data as CreateMailTemplateDto);
|
|
} else if (editorMode === "edit" && selectedTemplate) {
|
|
await updateMailTemplate(selectedTemplate.id, data as UpdateMailTemplateDto);
|
|
}
|
|
await loadTemplates();
|
|
setIsEditorOpen(false);
|
|
} catch (error) {
|
|
throw error; // 모달에서 에러 처리
|
|
}
|
|
};
|
|
|
|
const handleDeleteTemplate = async () => {
|
|
if (!selectedTemplate) return;
|
|
|
|
try {
|
|
await deleteMailTemplate(selectedTemplate.id);
|
|
await loadTemplates();
|
|
alert("템플릿이 삭제되었습니다.");
|
|
} catch (error) {
|
|
// console.error('템플릿 삭제 실패:', error);
|
|
alert("템플릿 삭제에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
const handleDuplicateTemplate = async (template: MailTemplate) => {
|
|
try {
|
|
await createMailTemplate({
|
|
name: `${template.name} (복사본)`,
|
|
subject: template.subject,
|
|
components: template.components,
|
|
category: template.category,
|
|
});
|
|
await loadTemplates();
|
|
alert("템플릿이 복사되었습니다.");
|
|
} catch (error) {
|
|
// console.error('템플릿 복사 실패:', error);
|
|
alert("템플릿 복사에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-background min-h-screen">
|
|
<div className="w-full max-w-none space-y-8 px-4 py-8">
|
|
{/* 페이지 제목 */}
|
|
<div className="bg-card space-y-4 rounded-lg border p-6">
|
|
{/* 브레드크럼브 */}
|
|
<nav className="flex items-center gap-2 text-sm">
|
|
<Link
|
|
href="/admin/mail/dashboard"
|
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
메일 관리
|
|
</Link>
|
|
<ChevronRight className="text-muted-foreground h-4 w-4" />
|
|
<span className="text-foreground font-medium">템플릿 관리</span>
|
|
</nav>
|
|
|
|
<Separator />
|
|
|
|
{/* 제목 + 액션 버튼들 */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-foreground text-3xl font-bold">메일 템플릿 관리</h1>
|
|
<p className="text-muted-foreground mt-2">드래그 앤 드롭으로 메일 템플릿을 만들고 관리합니다</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" size="sm" onClick={loadTemplates} disabled={loading}>
|
|
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
|
새로고침
|
|
</Button>
|
|
<Button variant="default" onClick={handleOpenCreateModal}>
|
|
<Plus className="mr-2 h-4 w-4" />새 템플릿 만들기
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 검색 및 필터 */}
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
placeholder="템플릿 이름, 제목으로 검색..."
|
|
className="focus:ring-primary focus:border-primary bg-background w-full rounded-lg border py-2 pr-4 pl-10 focus:ring-2"
|
|
/>
|
|
</div>
|
|
<select
|
|
value={categoryFilter}
|
|
onChange={(e) => setCategoryFilter(e.target.value)}
|
|
className="focus:ring-primary focus:border-primary bg-background rounded-lg border px-4 py-2 focus:ring-2"
|
|
>
|
|
<option value="all">전체 카테고리</option>
|
|
{categories.map((cat) => (
|
|
<option key={cat} value={cat}>
|
|
{cat}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 메인 컨텐츠 */}
|
|
{loading ? (
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-16">
|
|
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
|
</CardContent>
|
|
</Card>
|
|
) : filteredTemplates.length === 0 ? (
|
|
<Card className="py-16 text-center">
|
|
<CardContent className="pt-6">
|
|
<FileText className="text-muted-foreground mx-auto mb-4 h-16 w-16" />
|
|
<p className="text-muted-foreground mb-4">
|
|
{templates.length === 0 ? "아직 생성된 템플릿이 없습니다" : "검색 결과가 없습니다"}
|
|
</p>
|
|
{templates.length === 0 && (
|
|
<Button variant="default" onClick={handleOpenCreateModal}>
|
|
<Plus className="mr-2 h-4 w-4" />첫 템플릿 만들기
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{filteredTemplates.map((template) => (
|
|
<MailTemplateCard
|
|
key={template.id}
|
|
template={template}
|
|
onEdit={handleOpenEditModal}
|
|
onDelete={handleOpenDeleteModal}
|
|
onPreview={handleOpenPreviewModal}
|
|
onDuplicate={handleDuplicateTemplate}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 안내 정보 */}
|
|
<Card className="bg-muted/50">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center text-lg">
|
|
<FileText className="text-foreground mr-2 h-5 w-5" />
|
|
템플릿 디자이너
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-foreground mb-4">💡 드래그 앤 드롭으로 손쉽게 메일 템플릿을 만들 수 있어요!</p>
|
|
<ul className="text-muted-foreground space-y-2 text-sm">
|
|
<li className="flex items-start">
|
|
<span className="text-foreground mr-2">✓</span>
|
|
<span>텍스트, 버튼, 이미지, 여백 컴포넌트 지원</span>
|
|
</li>
|
|
<li className="flex items-start">
|
|
<span className="text-foreground mr-2">✓</span>
|
|
<span>실시간 미리보기로 즉시 확인 가능</span>
|
|
</li>
|
|
<li className="flex items-start">
|
|
<span className="text-foreground mr-2">✓</span>
|
|
<span>동적 변수 지원 (예: {"{customer_name}"})</span>
|
|
</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 모달들 */}
|
|
<MailTemplateEditorModal
|
|
isOpen={isEditorOpen}
|
|
onClose={() => setIsEditorOpen(false)}
|
|
onSave={handleSaveTemplate}
|
|
template={selectedTemplate}
|
|
mode={editorMode}
|
|
/>
|
|
|
|
<MailTemplatePreviewModal
|
|
isOpen={isPreviewOpen}
|
|
onClose={() => setIsPreviewOpen(false)}
|
|
template={selectedTemplate}
|
|
/>
|
|
|
|
<ConfirmDeleteModal
|
|
isOpen={isDeleteModalOpen}
|
|
onClose={() => setIsDeleteModalOpen(false)}
|
|
onConfirm={handleDeleteTemplate}
|
|
title="템플릿 삭제"
|
|
message="이 템플릿을 삭제하시겠습니까?"
|
|
itemName={selectedTemplate?.name}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|