/** * 메일 관리 시스템 API 클라이언트 * 파일 기반 메일 계정 및 템플릿 관리 */ // API 기본 URL const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "/api"; // ============================================ // 타입 정의 // ============================================ export interface MailAccount { id: string; name: string; email: string; smtpHost: string; smtpPort: number; smtpSecure: boolean; smtpUsername: string; smtpPassword: string; // 암호화된 상태 dailyLimit: number; status: "active" | "inactive"; createdAt: string; updatedAt: string; } export interface CreateMailAccountDto { name: string; email: string; smtpHost: string; smtpPort: number; smtpSecure: boolean; smtpUsername: string; smtpPassword: string; dailyLimit?: number; } export interface UpdateMailAccountDto extends Partial { status?: "active" | "inactive"; } export interface MailComponent { id: string; type: | "text" | "button" | "image" | "spacer" | "header" | "infoTable" | "alertBox" | "divider" | "footer" | "numberedList"; content?: string; text?: string; url?: string; src?: string; height?: number; styles?: Record; // 헤더 컴포넌트용 logoSrc?: string; brandName?: string; sendDate?: string; headerBgColor?: string; // 정보 테이블용 rows?: Array<{ label: string; value: string }>; tableTitle?: string; // 강조 박스용 alertType?: "info" | "warning" | "danger" | "success"; alertTitle?: string; // 푸터용 companyName?: string; ceoName?: string; businessNumber?: string; address?: string; phone?: string; email?: string; copyright?: string; // 번호 리스트용 listItems?: string[]; listTitle?: string; } export interface MailTemplate { id: string; name: string; subject: string; components: MailComponent[]; category?: string; createdAt: string; updatedAt: string; } export interface CreateMailTemplateDto { name: string; subject: string; components: MailComponent[]; category?: string; } export interface UpdateMailTemplateDto extends Partial {} export interface SendMailDto { accountId: string; templateId?: 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" | "draft"; messageId?: string; errorMessage?: string; accepted?: string[]; rejected?: string[]; isDraft?: boolean; deletedAt?: string; updatedAt?: string; } export interface SentMailListQuery { page?: number; limit?: number; searchTerm?: string; status?: "success" | "failed" | "draft" | "all"; accountId?: string; startDate?: string; endDate?: string; sortBy?: "sentAt" | "subject" | "updatedAt"; sortOrder?: "asc" | "desc"; includeDeleted?: boolean; onlyDeleted?: boolean; } 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; error?: string; } // ============================================ // API 기본 설정 // ============================================ import { apiClient } from "./client"; async function fetchApi(endpoint: string, options: { method?: string; data?: unknown } = {}): Promise { const { method = "GET", data } = options; try { const response = await apiClient({ url: endpoint, // `/mail` 접두사 제거 (apiClient는 이미 /api를 포함) method, data, }); // 백엔드가 { success: true, data: ... } 형식으로 반환하는 경우 처리 if (response.data.success && response.data.data !== undefined) { return response.data.data as T; } return response.data as T; } catch (error: unknown) { if (error && typeof error === "object" && "response" in error) { const axiosError = error as { response?: { data?: { message?: string }; status?: number } }; throw new Error(axiosError.response?.data?.message || `HTTP ${axiosError.response?.status}`); } throw new Error("Unknown error"); } } // ============================================ // 메일 계정 API // ============================================ /** * 전체 메일 계정 목록 조회 */ export async function getMailAccounts(): Promise { return fetchApi("/mail/accounts"); } /** * 특정 메일 계정 조회 */ export async function getMailAccount(id: string): Promise { return fetchApi(`/mail/accounts/${id}`); } /** * 메일 계정 생성 */ export async function createMailAccount(data: CreateMailAccountDto): Promise { return fetchApi("/mail/accounts", { method: "POST", data, }); } /** * 메일 계정 수정 */ export async function updateMailAccount(id: string, data: UpdateMailAccountDto): Promise { return fetchApi(`/mail/accounts/${id}`, { method: "PUT", data, }); } /** * 메일 계정 삭제 */ export async function deleteMailAccount(id: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(`/mail/accounts/${id}`, { method: "DELETE", }); } /** * SMTP 연결 테스트 */ export async function testMailAccountConnection(id: string): Promise<{ success: boolean; message: string }> { return fetchApi<{ success: boolean; message: string }>(`/mail/accounts/${id}/test-connection`, { method: "POST", }); } /** * SMTP 연결 테스트 */ export async function testMailConnection(id: string): Promise<{ success: boolean; message: string; }> { return fetchApi<{ success: boolean; message: string }>(`/mail/accounts/${id}/test-connection`, { method: "POST", }); } // ============================================ // 메일 템플릿 API // ============================================ /** * 전체 메일 템플릿 목록 조회 */ export async function getMailTemplates(): Promise { return fetchApi("/mail/templates-file"); } /** * 특정 메일 템플릿 조회 */ export async function getMailTemplate(id: string): Promise { return fetchApi(`/mail/templates-file/${id}`); } /** * 메일 템플릿 생성 */ export async function createMailTemplate(data: CreateMailTemplateDto): Promise { return fetchApi("/mail/templates-file", { method: "POST", data, }); } /** * 메일 템플릿 수정 */ export async function updateMailTemplate(id: string, data: UpdateMailTemplateDto): Promise { return fetchApi(`/mail/templates-file/${id}`, { method: "PUT", data, }); } /** * 메일 템플릿 삭제 */ export async function deleteMailTemplate(id: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(`/mail/templates-file/${id}`, { method: "DELETE", }); } /** * 메일 템플릿 미리보기 (샘플 데이터) */ export async function previewMailTemplate(id: string, sampleData?: Record): Promise<{ html: string }> { return fetchApi<{ html: string }>(`/mail/templates-file/${id}/preview`, { method: "POST", data: { sampleData }, }); } // ============================================ // 메일 발송 API (간단한 버전 - 쿼리 제외) // ============================================ /** * 메일 발송 (단건 또는 소규모 발송) */ export async function sendMail(data: SendMailDto): Promise { return fetchApi("/mail/send/simple", { method: "POST", data, }); } /** * 대량 메일 발송 */ export interface BulkSendRequest { accountId: string; templateId?: string; // 템플릿 ID (선택) customHtml?: string; // 직접 작성한 HTML (선택) subject: string; recipients: Array<{ email: string; variables?: Record; // 템플릿 사용 시에만 필요 }>; onProgress?: (sent: number, total: number) => void; } export interface BulkSendResult { total: number; success: number; failed: number; results: Array<{ email: string; success: boolean; messageId?: string; error?: string; }>; } export async function sendBulkMail(request: BulkSendRequest): Promise { const { onProgress, ...data } = request; // 프로그레스 콜백이 있으면 시뮬레이션 (실제로는 서버에서 스트리밍 필요) if (onProgress) { onProgress(0, data.recipients.length); } const result = await fetchApi("/mail/send/bulk", { method: "POST", data, }); if (onProgress) { onProgress(result.success, result.total); } return result; } /** * 템플릿 변수 추출 (템플릿에서 {변수명} 형식 추출) */ export function extractTemplateVariables(template: MailTemplate): string[] { const variableRegex = /\{(\w+)\}/g; const variables = new Set(); // subject에서 추출 const subjectMatches = template.subject.matchAll(variableRegex); for (const match of subjectMatches) { variables.add(match[1]); } // 컴포넌트 content에서 추출 template.components.forEach((component) => { if (component.content) { const contentMatches = component.content.matchAll(variableRegex); for (const match of contentMatches) { variables.add(match[1]); } } if (component.text) { const textMatches = component.text.matchAll(variableRegex); for (const match of textMatches) { variables.add(match[1]); } } }); return Array.from(variables); } /** * 템플릿을 HTML로 렌더링 (프론트엔드 미리보기용) */ export function renderTemplateToHtml(template: MailTemplate, variables?: Record): string { let html = '
'; template.components.forEach((component) => { switch (component.type) { case "text": let content = component.content || ""; if (variables) { Object.entries(variables).forEach(([key, value]) => { content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value); }); } html += `
${content}
`; break; case "button": let buttonText = component.text || "Button"; if (variables) { Object.entries(variables).forEach(([key, value]) => { buttonText = buttonText.replace(new RegExp(`\\{${key}\\}`, "g"), value); }); } html += ` ${buttonText} `; break; case "image": html += ``; break; case "spacer": html += `
`; break; case "header": html += `
${component.logoSrc ? `로고` : ""} ${component.brandName || ""} ${component.sendDate || ""}
`; break; case "infoTable": html += `
${component.tableTitle ? `
${component.tableTitle}
` : ""} ${(component.rows || []) .map( (row, i) => ` `, ) .join("")}
${row.label} ${row.value}
`; break; case "alertBox": const alertColors = { info: { bg: "#eff6ff", border: "#3b82f6", text: "#1e40af" }, warning: { bg: "#fffbeb", border: "#f59e0b", text: "#92400e" }, danger: { bg: "#fef2f2", border: "#ef4444", text: "#991b1b" }, success: { bg: "#ecfdf5", border: "#10b981", text: "#065f46" }, }; const colors = alertColors[component.alertType || "info"]; html += `
${component.alertTitle ? `
${component.alertTitle}
` : ""}
${component.content || ""}
`; break; case "divider": html += `
`; break; case "footer": html += `
${component.companyName ? `
${component.companyName}
` : ""} ${ component.ceoName || component.businessNumber ? `
${component.ceoName ? `대표: ${component.ceoName}` : ""} ${component.ceoName && component.businessNumber ? " | " : ""} ${component.businessNumber ? `사업자등록번호: ${component.businessNumber}` : ""}
` : "" } ${component.address ? `
${component.address}
` : ""} ${ component.phone || component.email ? `
${component.phone ? `Tel: ${component.phone}` : ""} ${component.phone && component.email ? " | " : ""} ${component.email ? `Email: ${component.email}` : ""}
` : "" } ${component.copyright ? `
${component.copyright}
` : ""}
`; break; case "numberedList": html += `
${component.listTitle ? `
${component.listTitle}
` : ""}
    ${(component.listItems || []).map((item) => `
  1. ${item}
  2. `).join("")}
`; break; } }); html += "
"; return html; } function styleObjectToString(styles?: Record): string { if (!styles) return ""; return Object.entries(styles) .map(([key, value]) => `${camelToKebab(key)}: ${value}`) .join("; "); } function camelToKebab(str: string): string { return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); } // ============================================ // 📥 메일 수신 API (Step 2) // ============================================ export interface ReceivedMail { id: string; messageId: string; from: string; to: string; subject: string; date: string; preview: string; isRead: boolean; hasAttachments: boolean; } export interface MailDetail extends ReceivedMail { htmlBody: string; textBody: string; cc?: string; bcc?: string; attachments: Array<{ filename: string; contentType: string; size: number; }>; } /** * 받은 메일 목록 조회 */ export async function getReceivedMails(accountId: string, limit: number = 50): Promise { return fetchApi(`/mail/receive/${accountId}?limit=${limit}`); } /** * 오늘 수신한 메일 수 조회 (통계) */ export async function getTodayReceivedCount(accountId?: string): Promise { const params = accountId ? `?accountId=${accountId}` : ""; const response = await fetchApi<{ count: number }>(`/mail/receive/today-count${params}`); return response.count; } /** * 메일 상세 조회 */ export async function getMailDetail(accountId: string, seqno: number): Promise { return fetchApi(`/mail/receive/${accountId}/${seqno}`); } /** * 메일 첨부파일 다운로드 */ export async function downloadMailAttachment(accountId: string, seqno: number, attachmentIndex: number): Promise { const token = localStorage.getItem("authToken"); const response = await fetch(`${API_BASE_URL}/mail/receive/${accountId}/${seqno}/attachment/${attachmentIndex}`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error(`첨부파일 다운로드 실패: ${response.status}`); } return response.blob(); } /** * 메일을 읽음으로 표시 */ export async function markMailAsRead(accountId: string, seqno: number): Promise<{ success: boolean; message: string }> { return fetchApi(`/mail/receive/${accountId}/${seqno}/mark-read`, { method: "POST", }); } /** * IMAP 연결 테스트 */ export async function testImapConnection(accountId: string): Promise<{ success: boolean; message: string }> { return fetchApi(`/mail/receive/${accountId}/test-imap`, { 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); if (query.includeDeleted) params.append("includeDeleted", "true"); if (query.onlyDeleted) params.append("onlyDeleted", "true"); return fetchApi(`/mail/sent?${params.toString()}`); } /** * 특정 발송 이력 상세 조회 */ export async function getSentMailById(id: string): Promise { return fetchApi(`/mail/sent/${id}`); } /** * 발송 이력 삭제 (Soft Delete) */ export async function deleteSentMail(id: string): Promise<{ success: boolean; message: string }> { return fetchApi(`/mail/sent/${id}`, { method: "DELETE", }); } /** * 임시 저장 (Draft) */ export async function saveDraft(data: Partial & { accountId: string }): Promise { const response = await apiClient.post("/mail/sent/draft", data); return response.data.data; } /** * 임시 저장 업데이트 */ export async function updateDraft(id: string, data: Partial): Promise { const response = await apiClient.put(`/mail/sent/draft/${id}`, data); return response.data.data; } /** * 메일 복구 */ export async function restoreMail(id: string): Promise<{ success: boolean; message: string }> { return fetchApi(`/mail/sent/${id}/restore`, { method: "POST", }); } /** * 메일 영구 삭제 */ export async function permanentlyDeleteMail(id: string): Promise<{ success: boolean; message: string }> { return fetchApi(`/mail/sent/${id}/permanent`, { method: "DELETE", }); } /** * 일괄 삭제 */ export async function bulkDeleteMails( ids: string[], ): Promise<{ success: boolean; message: string; data: { successCount: number; failCount: number } }> { return fetchApi("/mail/sent/bulk/delete", { method: "POST", body: JSON.stringify({ ids }), }); } /** * 일괄 영구 삭제 */ export async function bulkPermanentlyDeleteMails( ids: string[], ): Promise<{ success: boolean; message: string; data: { successCount: number; failCount: number } }> { return fetchApi("/mail/sent/bulk/permanent-delete", { method: "POST", body: JSON.stringify({ ids }), }); } /** * 일괄 복구 */ export async function bulkRestoreMails( ids: string[], ): Promise<{ success: boolean; message: string; data: { successCount: number; failCount: number } }> { return fetchApi("/mail/sent/bulk/restore", { method: "POST", body: JSON.stringify({ ids }), }); } /** * 메일 발송 통계 조회 */ export async function getMailStatistics(accountId?: string): Promise { const params = accountId ? `?accountId=${accountId}` : ""; return fetchApi(`/mail/sent/statistics${params}`); }