/** * 메일 관리 시스템 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}`); }