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

407 lines
13 KiB
TypeScript
Raw Normal View History

2025-10-01 16:15:53 +09:00
"use client";
import React, { useState, useEffect } from 'react';
import { X, Mail, Server, Lock, Zap, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
MailAccount,
CreateMailAccountDto,
UpdateMailAccountDto,
testMailConnection,
} from '@/lib/api/mail';
interface MailAccountModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: CreateMailAccountDto | UpdateMailAccountDto) => Promise<void>;
account?: MailAccount | null;
mode: 'create' | 'edit';
}
export default function MailAccountModal({
isOpen,
onClose,
onSave,
account,
mode,
}: MailAccountModalProps) {
const [formData, setFormData] = useState<CreateMailAccountDto>({
name: '',
email: '',
smtpHost: '',
smtpPort: 587,
smtpSecure: false,
smtpUsername: '',
smtpPassword: '',
dailyLimit: 1000,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
// 수정 모드일 때 기존 데이터 로드
useEffect(() => {
if (mode === 'edit' && account) {
setFormData({
name: account.name,
email: account.email,
smtpHost: account.smtpHost,
smtpPort: account.smtpPort,
smtpSecure: account.smtpSecure,
smtpUsername: account.smtpUsername,
smtpPassword: '', // 비밀번호는 비워둠 (보안)
dailyLimit: account.dailyLimit,
});
} else {
// 생성 모드일 때 초기화
setFormData({
name: '',
email: '',
smtpHost: '',
smtpPort: 587,
smtpSecure: false,
smtpUsername: '',
smtpPassword: '',
dailyLimit: 1000,
});
}
setTestResult(null);
}, [mode, account, isOpen]);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value, type } = e.target;
setFormData((prev) => ({
...prev,
[name]:
type === 'number'
? parseInt(value)
: type === 'checkbox'
? (e.target as HTMLInputElement).checked
: value,
}));
};
const handleTestConnection = async () => {
if (!account?.id && mode === 'edit') return;
setIsTesting(true);
setTestResult(null);
try {
// 수정 모드에서만 테스트 가능 (저장된 계정만)
if (mode === 'edit' && account) {
const result = await testMailConnection(account.id);
setTestResult(result);
} else {
setTestResult({
success: false,
message: '계정을 먼저 저장한 후 테스트할 수 있습니다.',
});
}
} catch (error) {
setTestResult({
success: false,
message: error instanceof Error ? error.message : '연결 테스트 실패',
});
} finally {
setIsTesting(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
await onSave(formData);
onClose();
} catch (error) {
console.error('저장 실패:', error);
alert(error instanceof Error ? error.message : '저장 중 오류가 발생했습니다.');
} finally {
setIsSubmitting(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* 헤더 */}
<div className="sticky top-0 bg-gradient-to-r from-orange-500 to-orange-600 px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Mail className="w-6 h-6 text-white" />
<h2 className="text-xl font-bold text-white">
{mode === 'create' ? '새 메일 계정 추가' : '메일 계정 수정'}
</h2>
</div>
<button
onClick={onClose}
className="text-white hover:bg-white/20 rounded-lg p-2 transition"
>
<X className="w-5 h-5" />
</button>
</div>
{/* 폼 */}
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* 기본 정보 */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Mail className="w-5 h-5 text-orange-500" />
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
*
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="예: 회사 공식 메일"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
*
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="info@company.com"
/>
</div>
</div>
{/* SMTP 설정 */}
<div className="space-y-4 pt-4 border-t">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Server className="w-5 h-5 text-orange-500" />
SMTP
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
SMTP *
</label>
<input
type="text"
name="smtpHost"
value={formData.smtpHost}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="smtp.gmail.com"
/>
<p className="text-xs text-gray-500 mt-1">
: smtp.gmail.com, smtp.naver.com, smtp.office365.com
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SMTP *
</label>
<input
type="number"
name="smtpPort"
value={formData.smtpPort}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="587"
/>
<p className="text-xs text-gray-500 mt-1">
587 (TLS) 465 (SSL)
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
name="smtpSecure"
value={formData.smtpSecure ? 'true' : 'false'}
onChange={(e) =>
setFormData((prev) => ({
...prev,
smtpSecure: e.target.value === 'true',
}))
}
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="false">TLS ( 587)</option>
<option value="true">SSL ( 465)</option>
</select>
</div>
</div>
</div>
{/* 인증 정보 */}
<div className="space-y-4 pt-4 border-t">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Lock className="w-5 h-5 text-orange-500" />
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
*
</label>
<input
type="text"
name="smtpUsername"
value={formData.smtpUsername}
onChange={handleChange}
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="info@company.com"
/>
<p className="text-xs text-gray-500 mt-1">
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{mode === 'edit' && '(변경 시에만 입력)'}
</label>
<input
type="password"
name="smtpPassword"
value={formData.smtpPassword}
onChange={handleChange}
required={mode === 'create'}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder={mode === 'edit' ? '변경하지 않으려면 비워두세요' : '••••••••'}
/>
<p className="text-xs text-gray-500 mt-1">
Gmail의
</p>
</div>
</div>
{/* 발송 제한 */}
<div className="space-y-4 pt-4 border-t">
<h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Zap className="w-5 h-5 text-orange-500" />
</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="number"
name="dailyLimit"
value={formData.dailyLimit}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="1000"
/>
<p className="text-xs text-gray-500 mt-1">
(0 = )
</p>
</div>
</div>
{/* 연결 테스트 (수정 모드만) */}
{mode === 'edit' && account && (
<div className="pt-4 border-t">
<Button
type="button"
onClick={handleTestConnection}
disabled={isTesting}
className="w-full bg-blue-500 hover:bg-blue-600"
>
{isTesting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Zap className="w-4 h-4 mr-2" />
SMTP
</>
)}
</Button>
{testResult && (
<div
className={`mt-3 p-3 rounded-lg flex items-start gap-2 ${
testResult.success
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
{testResult.success ? (
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<p
className={`text-sm ${
testResult.success ? 'text-green-800' : 'text-red-800'
}`}
>
{testResult.message}
</p>
</div>
)}
</div>
)}
{/* 버튼 */}
<div className="flex gap-3 pt-4">
<Button
type="button"
onClick={onClose}
variant="outline"
className="flex-1"
disabled={isSubmitting}
>
</Button>
<Button
type="submit"
className="flex-1 bg-orange-500 hover:bg-orange-600"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
'저장'
)}
</Button>
</div>
</form>
</div>
</div>
);
}