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}
|
2025-10-02 14:34:15 +09:00
|
|
|
variant="default"
|
|
|
|
|
className="w-full"
|
2025-10-01 16:15:53 +09:00
|
|
|
>
|
|
|
|
|
{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
|
2025-10-02 14:34:15 +09:00
|
|
|
? 'bg-green-50 border border-green-500/20'
|
|
|
|
|
: 'bg-destructive/10 border border-destructive/20'
|
2025-10-01 16:15:53 +09:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{testResult.success ? (
|
|
|
|
|
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
|
|
|
|
|
) : (
|
2025-10-02 14:34:15 +09:00
|
|
|
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
2025-10-01 16:15:53 +09:00
|
|
|
)}
|
|
|
|
|
<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"
|
2025-10-02 14:34:15 +09:00
|
|
|
variant="default"
|
|
|
|
|
className="flex-1"
|
2025-10-01 16:15:53 +09:00
|
|
|
disabled={isSubmitting}
|
|
|
|
|
>
|
|
|
|
|
{isSubmitting ? (
|
|
|
|
|
<>
|
|
|
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
|
|
저장 중...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
'저장'
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|