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

393 lines
14 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 { Send, Mail, Eye, Plus, X, Loader2, CheckCircle } from "lucide-react";
import {
MailAccount,
MailTemplate,
getMailAccounts,
getMailTemplates,
sendMail,
extractTemplateVariables,
renderTemplateToHtml,
} from "@/lib/api/mail";
export default function MailSendPage() {
const [accounts, setAccounts] = useState<MailAccount[]>([]);
const [templates, setTemplates] = useState<MailTemplate[]>([]);
const [loading, setLoading] = useState(false);
// 폼 상태
const [selectedAccountId, setSelectedAccountId] = useState<string>("");
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
const [subject, setSubject] = useState<string>("");
const [recipients, setRecipients] = useState<string[]>([""]);
const [variables, setVariables] = useState<Record<string, string>>({});
// UI 상태
const [isSending, setIsSending] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [sendResult, setSendResult] = useState<{
success: boolean;
message: string;
} | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const [accountsData, templatesData] = await Promise.all([
getMailAccounts(),
getMailTemplates(),
]);
setAccounts(Array.isArray(accountsData) ? accountsData : []);
setTemplates(Array.isArray(templatesData) ? templatesData : []);
// 기본값 설정
if (accountsData.length > 0 && !selectedAccountId) {
setSelectedAccountId(accountsData[0].id);
}
} catch (error) {
console.error('데이터 로드 실패:', error);
} finally {
setLoading(false);
}
};
const selectedTemplate = templates.find((t) => t.id === selectedTemplateId);
const templateVariables = selectedTemplate
? extractTemplateVariables(selectedTemplate)
: [];
// 템플릿 선택 시 제목 자동 입력 및 변수 초기화
useEffect(() => {
if (selectedTemplate) {
setSubject(selectedTemplate.subject);
const initialVars: Record<string, string> = {};
templateVariables.forEach((varName) => {
initialVars[varName] = "";
});
setVariables(initialVars);
}
}, [selectedTemplateId]);
const addRecipient = () => {
setRecipients([...recipients, ""]);
};
const removeRecipient = (index: number) => {
setRecipients(recipients.filter((_, i) => i !== index));
};
const updateRecipient = (index: number, value: string) => {
const newRecipients = [...recipients];
newRecipients[index] = value;
setRecipients(newRecipients);
};
const handleSend = async () => {
// 유효성 검증
const validRecipients = recipients.filter((email) => email.trim() !== "");
if (validRecipients.length === 0) {
alert("수신자 이메일을 입력하세요.");
return;
}
if (!selectedAccountId) {
alert("발송 계정을 선택하세요.");
return;
}
if (!subject.trim()) {
alert("메일 제목을 입력하세요.");
return;
}
if (!selectedTemplateId) {
alert("템플릿을 선택하세요.");
return;
}
setIsSending(true);
setSendResult(null);
try {
const result = await sendMail({
accountId: selectedAccountId,
templateId: selectedTemplateId,
to: validRecipients,
subject,
variables,
});
setSendResult({
success: true,
message: `${result.accepted?.length || 0}개 발송 성공`,
});
// 성공 후 초기화
setRecipients([""]);
setVariables({});
} catch (error) {
setSendResult({
success: false,
message: error instanceof Error ? error.message : "발송 실패",
});
} finally {
setIsSending(false);
}
};
const previewHtml = selectedTemplate
? renderTemplateToHtml(selectedTemplate, variables)
: "";
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-orange-500" />
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<div className="w-full max-w-6xl mx-auto px-4 py-8 space-y-6">
{/* 페이지 제목 */}
<div className="bg-white rounded-lg shadow-sm border p-6">
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600">릿 </p>
</div>
{/* 메인 폼 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 왼쪽: 발송 설정 */}
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="w-5 h-5 text-orange-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 발송 계정 선택 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<select
value={selectedAccountId}
onChange={(e) => setSelectedAccountId(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
>
<option value=""> </option>
{accounts
.filter((acc) => acc.status === "active")
.map((account) => (
<option key={account.id} value={account.id}>
{account.name} ({account.email})
</option>
))}
</select>
</div>
{/* 템플릿 선택 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
릿 *
</label>
<select
value={selectedTemplateId}
onChange={(e) => setSelectedTemplateId(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
>
<option value="">릿 </option>
{templates.map((template) => (
<option key={template.id} value={template.id}>
{template.name}
</option>
))}
</select>
</div>
{/* 메일 제목 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="예: 환영합니다!"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
/>
</div>
{/* 수신자 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<div className="space-y-2">
{recipients.map((email, index) => (
<div key={index} className="flex gap-2">
<input
type="email"
value={email}
onChange={(e) => updateRecipient(index, e.target.value)}
placeholder="example@email.com"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
/>
{recipients.length > 1 && (
<Button
variant="outline"
size="sm"
onClick={() => removeRecipient(index)}
className="text-red-500 hover:text-red-600"
>
<X className="w-4 h-4" />
</Button>
)}
</div>
))}
<Button
variant="outline"
size="sm"
onClick={addRecipient}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 템플릿 변수 */}
{templateVariables.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
릿
</label>
<div className="space-y-2">
{templateVariables.map((varName) => (
<div key={varName}>
<label className="block text-xs text-gray-600 mb-1">
{varName}
</label>
<input
type="text"
value={variables[varName] || ""}
onChange={(e) =>
setVariables({ ...variables, [varName]: e.target.value })
}
placeholder={`{${varName}}`}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
/>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* 발송 버튼 */}
<div className="flex gap-3">
<Button
onClick={() => setShowPreview(!showPreview)}
variant="outline"
className="flex-1"
disabled={!selectedTemplateId}
>
<Eye className="w-4 h-4 mr-2" />
</Button>
<Button
onClick={handleSend}
disabled={isSending || !selectedAccountId || !selectedTemplateId}
className="flex-1 bg-orange-500 hover:bg-orange-600"
>
{isSending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
{/* 발송 결과 */}
{sendResult && (
<Card
className={
sendResult.success
? "border-green-200 bg-green-50"
: "border-red-200 bg-red-50"
}
>
<CardContent className="pt-6">
<div className="flex items-center gap-2">
{sendResult.success ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<X className="w-5 h-5 text-red-600" />
)}
<p
className={
sendResult.success ? "text-green-800" : "text-red-800"
}
>
{sendResult.message}
</p>
</div>
</CardContent>
</Card>
)}
</div>
{/* 오른쪽: 미리보기 */}
<div className="lg:col-span-1">
<Card className="sticky top-4">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Eye className="w-4 h-4 text-orange-500" />
</CardTitle>
</CardHeader>
<CardContent>
{showPreview && previewHtml ? (
<div className="border rounded-lg p-4 bg-white max-h-[600px] overflow-y-auto">
<div className="text-xs text-gray-500 mb-2">: {subject}</div>
<div dangerouslySetInnerHTML={{ __html: previewHtml }} />
</div>
) : (
<div className="text-center py-16 text-gray-400">
<Mail className="w-12 h-12 mx-auto mb-2 opacity-20" />
<p className="text-sm">
릿
<br />
</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
</div>
);
}