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"
- >
- 템플릿 선택
- {templates.map((template) => (
-
- {template.name}
-
+
+ {/* 메일 작성 폼 */}
+
+ {/* 발송 설정 */}
+
+
+
+
+ 발송 설정
+
+
+
+ {/* 발송 계정 선택 */}
+
+ 발송 계정 *
+
+
+
+
+
+ {accounts.map((account) => (
+
+ {account.name} ({account.email})
+
))}
-
-
+
+
+
- {/* 메일 제목 */}
-
-
- 메일 제목 *
-
- 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"
- />
-
+ {/* 템플릿 선택 */}
+
+ 템플릿 (선택)
+
+
+
+
+
+ 직접 작성
+ {templates.map((template) => (
+
+ {template.name}
+
+ ))}
+
+
+
+
+
- {/* 수신자 */}
-
-
- 수신자 이메일 *
-
-
- {recipients.map((email, index) => (
-
-
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 && (
-
removeRecipient(index)}
- className="text-red-500 hover:text-red-600"
- >
-
-
- )}
+ {/* 수신자 */}
+
+
+
+
+ 수신자
+
+
+
+ {/* 받는 사람 */}
+
+
+
+ 받는 사람 *
+
+
+
+ {to.map((email) => (
+
+ {email}
+ removeEmail(email, "to")}
+ className="hover:bg-blue-200 rounded p-0.5"
+ >
+
+
+
+ ))}
+
setToInput(e.target.value)}
+ onKeyDown={(e) => handleEmailInput(e, "to")}
+ placeholder={to.length === 0 ? "이메일 주소 입력 후 엔터, 쉼표, 스페이스" : ""}
+ className="flex-1 outline-none min-w-[200px] text-sm"
+ />
+
+
+ 💡 이메일 주소를 입력하고 엔터, 쉼표(,), 스페이스를 눌러 추가하세요
+
+
+
+
+ {/* 참조 (CC) */}
+
+
+
+ 참조 (CC)
+
+
+
+ {cc.map((email) => (
+
+ {email}
+ removeEmail(email, "cc")}
+ className="hover:bg-green-200 rounded p-0.5"
+ >
+
+
+
+ ))}
+
setCcInput(e.target.value)}
+ onKeyDown={(e) => handleEmailInput(e, "cc")}
+ placeholder={cc.length === 0 ? "참조로 받을 이메일 주소" : ""}
+ className="flex-1 outline-none min-w-[200px] text-sm"
+ />
+
+
+ 다른 수신자에게도 공개됩니다
+
+
+
+
+ {/* 숨은참조 (BCC) */}
+
+
+
+ 숨은참조 (BCC)
+
+
+
+ {bcc.map((email) => (
+
+ {email}
+ removeEmail(email, "bcc")}
+ className="hover:bg-purple-200 rounded p-0.5"
+ >
+
+
+
+ ))}
+
setBccInput(e.target.value)}
+ onKeyDown={(e) => handleEmailInput(e, "bcc")}
+ placeholder={bcc.length === 0 ? "숨은참조로 받을 이메일 주소" : ""}
+ className="flex-1 outline-none min-w-[200px] text-sm"
+ />
+
+
+ 🔒 다른 수신자에게 보이지 않습니다 (모니터링용)
+
+
+
+
+
+
+ {/* 메일 내용 */}
+
+
+
+
+ 메일 내용
+
+
+
+ {/* 제목 */}
+
+ 제목 *
+ setSubject(e.target.value)}
+ placeholder="메일 제목을 입력하세요"
+ />
+
+
+ {/* 템플릿 변수 입력 */}
+ {templateVariables.length > 0 && (
+
+
템플릿 변수
+ {templateVariables.map((varName) => (
+
+
+ {varName}
+
+
+ setVariables({ ...variables, [varName]: e.target.value })
+ }
+ placeholder={`{${varName}} 변수 값`}
+ />
+
+ ))}
+
+ )}
+
+ {/* 직접 작성 */}
+ {!selectedTemplateId && (
+
+ )}
+
+
+
+ {/* 파일 첨부 */}
+
+
+
+
+ 파일 첨부
+
+
+
+ {/* 드래그 앤 드롭 영역 */}
+ document.getElementById("file-input")?.click()}
+ >
+
+
+
+ 파일을 드래그하거나 클릭하여 선택하세요
+
+
+ 최대 5개, 각 10MB 이하
+
+
+
+ {/* 첨부된 파일 목록 */}
+ {attachments.length > 0 && (
+
+
첨부된 파일 ({attachments.length})
+
+ {attachments.map((file, index) => (
+
+
+
+
+
+ {file.name}
+
+
+ {formatFileSize(file.size)}
+
+
+
+
removeFile(index)}
+ className="flex-shrink-0 text-red-500 hover:text-red-600 hover:bg-red-50"
+ >
+
+
))}
-
-
- 수신자 추가
-
+ )}
+
+
- {/* 템플릿 변수 */}
- {templateVariables.length > 0 && (
-
-
- 템플릿 변수
-
-
- {templateVariables.map((varName) => (
-
-
- {varName}
-
-
- 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"
- />
-
- ))}
-
-
- )}
-
-
-
- {/* 발송 버튼 */}
-
- setShowPreview(!showPreview)}
- variant="outline"
- className="flex-1"
- disabled={!selectedTemplateId}
- >
-
- 미리보기
-
-
- {isSending ? (
- <>
-
- 발송 중...
- >
- ) : (
- <>
-
- 발송
- >
- )}
-
-
-
- {/* 발송 결과 */}
- {sendResult && (
-
-
-
- {sendResult.success ? (
-
- ) : (
-
- )}
-
- {sendResult.message}
-
-
-
-
- )}
-
-
- {/* 오른쪽: 미리보기 */}
-
-
-
-
-
- 미리보기
-
-
-
- {showPreview && previewHtml ? (
-
- ) : (
-
-
-
- 템플릿을 선택하고
-
- 미리보기 버튼을 클릭하세요
-
-
- )}
-
-
+ {/* 발송 버튼 */}
+
+
+ {sending ? (
+ <>
+
+ 발송 중...
+ >
+ ) : (
+ <>
+
+ 메일 발송
+ >
+ )}
+
+
+
+ 미리보기
+
+
+ {/* 미리보기 패널 */}
+ {showPreview && (
+
+
+
+
+
+
+ 미리보기
+
+ setShowPreview(false)}
+ >
+
+
+
+
+
+
+
+
+ 받는 사람: {to.join(", ") || "-"}
+
+ {cc.length > 0 && (
+
+ 참조: {cc.join(", ")}
+
+ )}
+ {bcc.length > 0 && (
+
+ 숨은참조: {bcc.join(", ")}
+
+ )}
+
+ 제목: {subject || "-"}
+
+ {attachments.length > 0 && (
+
+
첨부파일: {attachments.length}개
+
+ {attachments.map((file, index) => (
+
+
+ {file.name}
+ ({formatFileSize(file.size)})
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+ )}
);
diff --git a/frontend/app/(main)/admin/mail/sent/page.tsx b/frontend/app/(main)/admin/mail/sent/page.tsx
new file mode 100644
index 00000000..9a96e0b8
--- /dev/null
+++ b/frontend/app/(main)/admin/mail/sent/page.tsx
@@ -0,0 +1,616 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Inbox,
+ Search,
+ Filter,
+ Eye,
+ Trash2,
+ RefreshCw,
+ CheckCircle2,
+ XCircle,
+ Mail,
+ Calendar,
+ User,
+ Paperclip,
+ Loader2,
+ X,
+ File,
+ LayoutDashboard,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ SentMailHistory,
+ getSentMailList,
+ deleteSentMail,
+ getMailAccounts,
+ MailAccount,
+} from "@/lib/api/mail";
+import { useToast } from "@/hooks/use-toast";
+
+export default function SentMailPage() {
+ const router = useRouter();
+ const { toast } = useToast();
+ const [mails, setMails] = useState
([]);
+ const [accounts, setAccounts] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [selectedMail, setSelectedMail] = useState(null);
+
+ // 필터 및 페이징
+ const [searchTerm, setSearchTerm] = useState("");
+ const [filterStatus, setFilterStatus] = useState<'all' | 'success' | 'failed'>('all');
+ const [filterAccountId, setFilterAccountId] = useState('all');
+ const [sortBy, setSortBy] = useState<'sentAt' | 'subject'>('sentAt');
+ const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
+ const [page, setPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const [total, setTotal] = useState(0);
+
+ useEffect(() => {
+ loadAccounts();
+ loadMails();
+ }, [page, filterStatus, filterAccountId, sortBy, sortOrder]);
+
+ const loadAccounts = async () => {
+ try {
+ const data = await getMailAccounts();
+ setAccounts(data);
+ } catch (error: unknown) {
+ const err = error as Error;
+ toast({
+ title: "계정 로드 실패",
+ description: err.message,
+ variant: "destructive",
+ });
+ }
+ };
+
+ const loadMails = async () => {
+ try {
+ setLoading(true);
+ const result = await getSentMailList({
+ page,
+ limit: 20,
+ searchTerm: searchTerm || undefined,
+ status: filterStatus,
+ accountId: filterAccountId !== 'all' ? filterAccountId : undefined,
+ sortBy,
+ sortOrder,
+ });
+
+ setMails(result.items);
+ setTotalPages(result.totalPages);
+ setTotal(result.total);
+ } catch (error: unknown) {
+ const err = error as Error;
+ toast({
+ title: "발송 이력 로드 실패",
+ description: err.message,
+ variant: "destructive",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSearch = () => {
+ setPage(1);
+ loadMails();
+ };
+
+ const handleDelete = async (id: string) => {
+ if (!confirm("이 발송 이력을 삭제하시겠습니까?")) {
+ return;
+ }
+
+ try {
+ await deleteSentMail(id);
+ toast({
+ title: "삭제 완료",
+ description: "발송 이력이 삭제되었습니다.",
+ });
+ loadMails();
+ } catch (error: unknown) {
+ const err = error as Error;
+ toast({
+ title: "삭제 실패",
+ description: err.message,
+ variant: "destructive",
+ });
+ }
+ };
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+
+ const formatFileSize = (bytes: number) => {
+ if (bytes === 0) return "0 Bytes";
+ const k = 1024;
+ const sizes = ["Bytes", "KB", "MB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
+ };
+
+ if (loading && page === 1) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* 헤더 */}
+
+
+
+
+ 보낸메일함
+
+
발송된 메일의 이력을 확인하고 관리하세요
+
+
router.push('/admin/mail/dashboard')}
+ >
+
+ 대시보드
+
+
+
+ {/* 필터 및 검색 */}
+
+
+
+
+ 필터 및 검색
+
+
+
+
+ {/* 검색 */}
+
+
검색
+
+ setSearchTerm(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
+ placeholder="제목 또는 받는사람 검색..."
+ />
+
+
+
+
+
+
+ {/* 상태 필터 */}
+
+ 상태
+ {
+ setFilterStatus(v);
+ setPage(1);
+ }}>
+
+
+
+
+ 전체
+ 성공
+ 실패
+
+
+
+
+ {/* 계정 필터 */}
+
+ 발송 계정
+ {
+ setFilterAccountId(v);
+ setPage(1);
+ }}>
+
+
+
+
+ 전체 계정
+ {accounts.map((account) => (
+
+ {account.name}
+
+ ))}
+
+
+
+
+
+
+
+ {
+ setSortBy('sentAt');
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
+ }}
+ >
+
+ 날짜순 {sortBy === 'sentAt' && (sortOrder === 'asc' ? '↑' : '↓')}
+
+ {
+ setSortBy('subject');
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
+ }}
+ >
+ 제목순 {sortBy === 'subject' && (sortOrder === 'asc' ? '↑' : '↓')}
+
+
+
+
+
+ 새로고침
+
+
+
+
+
+ {/* 메일 목록 */}
+
+
+
+ 발송 이력 ({total}건)
+
+
+
+ {mails.length === 0 ? (
+
+ ) : (
+
+ {mails.map((mail) => (
+
+
+
+ {mail.status === 'success' ? (
+
+ ) : (
+
+ )}
+
+ {mail.subject}
+
+ {mail.attachments && mail.attachments.length > 0 && (
+
+ )}
+
+
+
+
+ {mail.accountName}
+
+
+
+ 받는사람: {mail.to.length}명
+ {mail.cc && mail.cc.length > 0 && (
+ (참조 {mail.cc.length}명)
+ )}
+
+
+
+ {formatDate(mail.sentAt)}
+
+
+ {mail.status === 'failed' && mail.errorMessage && (
+
+ 오류: {mail.errorMessage}
+
+ )}
+
+
+ setSelectedMail(mail)}
+ >
+
+
+ handleDelete(mail.id)}
+ className="text-red-500 hover:text-red-600 hover:bg-red-50"
+ >
+
+
+
+
+ ))}
+
+ )}
+
+ {/* 페이징 */}
+ {totalPages > 1 && (
+
+
setPage((p) => Math.max(1, p - 1))}
+ disabled={page === 1}
+ >
+ 이전
+
+
+ {page} / {totalPages}
+
+
setPage((p) => Math.min(totalPages, p + 1))}
+ disabled={page === totalPages}
+ >
+ 다음
+
+
+ )}
+
+
+
+ {/* 상세보기 모달 */}
+
!open && setSelectedMail(null)}>
+
+
+
+
+ {selectedMail?.status === 'success' ? (
+
+ ) : (
+
+ )}
+ 발송 상세 정보
+
+ setSelectedMail(null)}
+ >
+
+
+
+
+
+ {selectedMail && (
+
+ {/* 발송 정보 */}
+
+
+ 발송 정보
+
+
+
+
+
발송 계정
+
+ {selectedMail.accountName} ({selectedMail.accountEmail})
+
+
+
+
발송 시간
+
+ {formatDate(selectedMail.sentAt)}
+
+
+
+
+
+
상태
+
+ {selectedMail.status === 'success' ? (
+
+ 발송 성공
+
+ ) : (
+
+ 발송 실패
+
+ )}
+
+
+
+ {selectedMail.messageId && (
+
+
메시지 ID
+
+ {selectedMail.messageId}
+
+
+ )}
+
+ {selectedMail.errorMessage && (
+
+
오류 메시지
+
+ {selectedMail.errorMessage}
+
+
+ )}
+
+
+
+ {/* 수신자 정보 */}
+
+
+ 수신자 정보
+
+
+
+
받는 사람
+
+ {selectedMail.to.map((email, i) => (
+
+ {email}
+
+ ))}
+
+
+
+ {selectedMail.cc && selectedMail.cc.length > 0 && (
+
+
참조 (CC)
+
+ {selectedMail.cc.map((email, i) => (
+
+ {email}
+
+ ))}
+
+
+ )}
+
+ {selectedMail.bcc && selectedMail.bcc.length > 0 && (
+
+
숨은참조 (BCC)
+
+ {selectedMail.bcc.map((email, i) => (
+
+ {email}
+
+ ))}
+
+
+ )}
+
+ {selectedMail.accepted && selectedMail.accepted.length > 0 && (
+
+
수락됨
+
+ {selectedMail.accepted.join(", ")}
+
+
+ )}
+
+ {selectedMail.rejected && selectedMail.rejected.length > 0 && (
+
+
거부됨
+
+ {selectedMail.rejected.join(", ")}
+
+
+ )}
+
+
+
+ {/* 메일 내용 */}
+
+
+ 메일 내용
+
+
+
+
제목
+
+ {selectedMail.subject}
+
+
+
+ {selectedMail.templateName && (
+
+
사용한 템플릿
+
+ {selectedMail.templateName}
+
+
+ )}
+
+
+
+
+
+ {/* 첨부파일 */}
+ {selectedMail.attachments && selectedMail.attachments.length > 0 && (
+
+
+
+
+ 첨부파일 ({selectedMail.attachments.length})
+
+
+
+
+ {selectedMail.attachments.map((file, i) => (
+
+
+
+
+
+ {file.originalName}
+
+
+ {formatFileSize(file.size)} • {file.mimetype}
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
diff --git a/frontend/app/(main)/admin/mail/templates/page.tsx b/frontend/app/(main)/admin/mail/templates/page.tsx
index 6814af06..e4155b5c 100644
--- a/frontend/app/(main)/admin/mail/templates/page.tsx
+++ b/frontend/app/(main)/admin/mail/templates/page.tsx
@@ -3,7 +3,8 @@
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 { Plus, FileText, Loader2, RefreshCw, Search, LayoutDashboard } from "lucide-react";
+import { useRouter } from "next/navigation";
import {
MailTemplate,
getMailTemplates,
@@ -19,6 +20,7 @@ import MailTemplateEditorModal from "@/components/mail/MailTemplateEditorModal";
import ConfirmDeleteModal from "@/components/mail/ConfirmDeleteModal";
export default function MailTemplatesPage() {
+ const router = useRouter();
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
@@ -137,6 +139,14 @@ export default function MailTemplatesPage() {
드래그 앤 드롭으로 메일 템플릿을 만들고 관리합니다
+ router.push('/admin/mail/dashboard')}
+ >
+
+ 대시보드
+
{}
export interface SendMailDto {
accountId: string;
templateId?: string;
- to: string[]; // 수신자 이메일 배열
+ to: string[]; // 받는 사람
+ cc?: string[]; // 참조 (Carbon Copy)
+ bcc?: string[]; // 숨은참조 (Blind Carbon Copy)
subject: string;
variables?: Record; // 템플릿 변수 치환
customHtml?: string; // 템플릿 없이 직접 HTML 작성 시
}
+// ============================================
+// 발송 이력 타입
+// ============================================
+
+export interface AttachmentInfo {
+ filename: string;
+ originalName: string;
+ size: number;
+ path: string;
+ mimetype: string;
+}
+
+export interface SentMailHistory {
+ id: string;
+ accountId: string;
+ accountName: string;
+ accountEmail: string;
+ to: string[];
+ cc?: string[];
+ bcc?: string[];
+ subject: string;
+ htmlContent: string;
+ templateId?: string;
+ templateName?: string;
+ attachments?: AttachmentInfo[];
+ sentAt: string;
+ status: 'success' | 'failed';
+ messageId?: string;
+ errorMessage?: string;
+ accepted?: string[];
+ rejected?: string[];
+}
+
+export interface SentMailListQuery {
+ page?: number;
+ limit?: number;
+ searchTerm?: string;
+ status?: 'success' | 'failed' | 'all';
+ accountId?: string;
+ startDate?: string;
+ endDate?: string;
+ sortBy?: 'sentAt' | 'subject';
+ sortOrder?: 'asc' | 'desc';
+}
+
+export interface SentMailListResponse {
+ items: SentMailHistory[];
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+}
+
+export interface MailStatistics {
+ totalSent: number;
+ successCount: number;
+ failedCount: number;
+ todayCount: number;
+ thisMonthCount: number;
+ successRate: number;
+}
+
export interface MailSendResult {
success: boolean;
messageId?: string;
@@ -96,7 +160,7 @@ async function fetchApi(
try {
const response = await apiClient({
- url: `/mail${endpoint}`,
+ url: endpoint, // `/mail` 접두사 제거 (apiClient는 이미 /api를 포함)
method,
data,
});
@@ -124,14 +188,14 @@ async function fetchApi(
* 전체 메일 계정 목록 조회
*/
export async function getMailAccounts(): Promise {
- return fetchApi('/accounts');
+ return fetchApi('/mail/accounts');
}
/**
* 특정 메일 계정 조회
*/
export async function getMailAccount(id: string): Promise {
- return fetchApi(`/accounts/${id}`);
+ return fetchApi(`/mail/accounts/${id}`);
}
/**
@@ -140,7 +204,7 @@ export async function getMailAccount(id: string): Promise {
export async function createMailAccount(
data: CreateMailAccountDto
): Promise {
- return fetchApi('/accounts', {
+ return fetchApi('/mail/accounts', {
method: 'POST',
data,
});
@@ -153,7 +217,7 @@ export async function updateMailAccount(
id: string,
data: UpdateMailAccountDto
): Promise {
- return fetchApi(`/accounts/${id}`, {
+ return fetchApi(`/mail/accounts/${id}`, {
method: 'PUT',
data,
});
@@ -163,7 +227,7 @@ export async function updateMailAccount(
* 메일 계정 삭제
*/
export async function deleteMailAccount(id: string): Promise<{ success: boolean }> {
- return fetchApi<{ success: boolean }>(`/accounts/${id}`, {
+ return fetchApi<{ success: boolean }>(`/mail/accounts/${id}`, {
method: 'DELETE',
});
}
@@ -172,7 +236,7 @@ export async function deleteMailAccount(id: string): Promise<{ success: boolean
* SMTP 연결 테스트
*/
export async function testMailAccountConnection(id: string): Promise<{ success: boolean; message: string }> {
- return fetchApi<{ success: boolean; message: string }>(`/accounts/${id}/test-connection`, {
+ return fetchApi<{ success: boolean; message: string }>(`/mail/accounts/${id}/test-connection`, {
method: 'POST',
});
}
@@ -185,7 +249,7 @@ export async function testMailConnection(id: string): Promise<{
message: string;
}> {
return fetchApi<{ success: boolean; message: string }>(
- `/accounts/${id}/test-connection`,
+ `/mail/accounts/${id}/test-connection`,
{
method: 'POST',
}
@@ -200,14 +264,14 @@ export async function testMailConnection(id: string): Promise<{
* 전체 메일 템플릿 목록 조회
*/
export async function getMailTemplates(): Promise {
- return fetchApi('/templates-file');
+ return fetchApi('/mail/templates-file');
}
/**
* 특정 메일 템플릿 조회
*/
export async function getMailTemplate(id: string): Promise {
- return fetchApi(`/templates-file/${id}`);
+ return fetchApi(`/mail/templates-file/${id}`);
}
/**
@@ -216,7 +280,7 @@ export async function getMailTemplate(id: string): Promise {
export async function createMailTemplate(
data: CreateMailTemplateDto
): Promise {
- return fetchApi('/templates-file', {
+ return fetchApi('/mail/templates-file', {
method: 'POST',
data,
});
@@ -229,7 +293,7 @@ export async function updateMailTemplate(
id: string,
data: UpdateMailTemplateDto
): Promise {
- return fetchApi(`/templates-file/${id}`, {
+ return fetchApi(`/mail/templates-file/${id}`, {
method: 'PUT',
data,
});
@@ -239,7 +303,7 @@ export async function updateMailTemplate(
* 메일 템플릿 삭제
*/
export async function deleteMailTemplate(id: string): Promise<{ success: boolean }> {
- return fetchApi<{ success: boolean }>(`/templates-file/${id}`, {
+ return fetchApi<{ success: boolean }>(`/mail/templates-file/${id}`, {
method: 'DELETE',
});
}
@@ -251,7 +315,7 @@ export async function previewMailTemplate(
id: string,
sampleData?: Record
): Promise<{ html: string }> {
- return fetchApi<{ html: string }>(`/templates-file/${id}/preview`, {
+ return fetchApi<{ html: string }>(`/mail/templates-file/${id}/preview`, {
method: 'POST',
data: { sampleData },
});
@@ -265,7 +329,7 @@ export async function previewMailTemplate(
* 메일 발송 (단건 또는 소규모 발송)
*/
export async function sendMail(data: SendMailDto): Promise {
- return fetchApi('/send/simple', {
+ return fetchApi('/mail/send/simple', {
method: 'POST',
data,
});
@@ -439,3 +503,52 @@ export async function testImapConnection(
method: 'POST',
});
}
+
+// ============================================
+// 발송 이력 API
+// ============================================
+
+/**
+ * 발송 이력 목록 조회
+ */
+export async function getSentMailList(
+ query: SentMailListQuery = {}
+): Promise {
+ const params = new URLSearchParams();
+
+ if (query.page) params.append('page', query.page.toString());
+ if (query.limit) params.append('limit', query.limit.toString());
+ if (query.searchTerm) params.append('searchTerm', query.searchTerm);
+ if (query.status && query.status !== 'all') params.append('status', query.status);
+ if (query.accountId) params.append('accountId', query.accountId);
+ if (query.startDate) params.append('startDate', query.startDate);
+ if (query.endDate) params.append('endDate', query.endDate);
+ if (query.sortBy) params.append('sortBy', query.sortBy);
+ if (query.sortOrder) params.append('sortOrder', query.sortOrder);
+
+ return fetchApi(`/mail/sent?${params.toString()}`);
+}
+
+/**
+ * 특정 발송 이력 상세 조회
+ */
+export async function getSentMailById(id: string): Promise {
+ return fetchApi(`/mail/sent/${id}`);
+}
+
+/**
+ * 발송 이력 삭제
+ */
+export async function deleteSentMail(id: string): Promise<{ success: boolean; message: string }> {
+ return fetchApi(`/mail/sent/${id}`, {
+ method: 'DELETE',
+ });
+}
+
+/**
+ * 메일 발송 통계 조회
+ */
+export async function getMailStatistics(accountId?: string): Promise {
+ const params = accountId ? `?accountId=${accountId}` : '';
+ return fetchApi(`/mail/sent/statistics${params}`);
+}