ERP-node/frontend/app/(main)/admin/mail/templates/page.tsx

287 lines
10 KiB
TypeScript
Raw Normal View History

2025-10-01 16:15:53 +09:00
"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 } from "lucide-react";
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 [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="min-h-screen bg-gray-50">
<div className="w-full max-w-none px-4 py-8 space-y-8">
{/* 페이지 제목 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> 릿 </h1>
<p className="mt-2 text-gray-600"> 릿 </p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={loadTemplates}
disabled={loading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</Button>
<Button
onClick={handleOpenCreateModal}
className="bg-orange-500 hover:bg-orange-600"
>
<Plus className="w-4 h-4 mr-2" />
릿
</Button>
</div>
</div>
{/* 검색 및 필터 */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="flex gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="템플릿 이름, 제목으로 검색..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
/>
</div>
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
>
<option value="all"> </option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
</div>
</CardContent>
</Card>
{/* 메인 컨텐츠 */}
{loading ? (
<Card className="shadow-sm">
<CardContent className="flex justify-center items-center py-16">
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
</CardContent>
</Card>
) : filteredTemplates.length === 0 ? (
<Card className="text-center py-16 bg-white shadow-sm">
<CardContent className="pt-6">
<FileText className="w-16 h-16 mx-auto mb-4 text-gray-300" />
<p className="text-gray-500 mb-4">
{templates.length === 0
? '아직 생성된 템플릿이 없습니다'
: '검색 결과가 없습니다'}
</p>
{templates.length === 0 && (
<Button
onClick={handleOpenCreateModal}
className="bg-orange-500 hover:bg-orange-600"
>
<Plus className="w-4 h-4 mr-2" />
릿
</Button>
)}
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredTemplates.map((template) => (
<MailTemplateCard
key={template.id}
template={template}
onEdit={handleOpenEditModal}
onDelete={handleOpenDeleteModal}
onPreview={handleOpenPreviewModal}
onDuplicate={handleDuplicateTemplate}
/>
))}
</div>
)}
{/* 안내 정보 */}
<Card className="bg-gradient-to-r from-orange-50 to-amber-50 border-orange-200 shadow-sm">
<CardHeader>
<CardTitle className="text-lg flex items-center">
<FileText className="w-5 h-5 mr-2 text-orange-500" />
릿
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-700 mb-4">
💡 릿 !
</p>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start">
<span className="text-orange-500 mr-2"></span>
<span>, , , </span>
</li>
<li className="flex items-start">
<span className="text-orange-500 mr-2"></span>
<span> </span>
</li>
<li className="flex items-start">
<span className="text-orange-500 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>
);
}