/** * 메일 관리 시스템 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'; content?: string; text?: string; url?: string; src?: string; height?: number; styles?: Record; } 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[]; // 수신자 이메일 배열 subject: string; variables?: Record; // 템플릿 변수 치환 customHtml?: string; // 템플릿 없이 직접 HTML 작성 시 } export interface MailSendResult { success: boolean; messageId?: string; error?: string; } // ============================================ // API 기본 설정 // ============================================ const API_BASE_URL = '/api/mail'; async function fetchApi( endpoint: string, options: RequestInit = {} ): Promise { const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Unknown error' })); throw new Error(error.message || `HTTP ${response.status}`); } const result = await response.json(); // 백엔드가 { success: true, data: ... } 형식으로 반환하는 경우 처리 if (result.success && result.data !== undefined) { return result.data as T; } return result as T; } // ============================================ // 메일 계정 API // ============================================ /** * 전체 메일 계정 목록 조회 */ export async function getMailAccounts(): Promise { return fetchApi('/accounts'); } /** * 특정 메일 계정 조회 */ export async function getMailAccount(id: string): Promise { return fetchApi(`/accounts/${id}`); } /** * 메일 계정 생성 */ export async function createMailAccount( data: CreateMailAccountDto ): Promise { return fetchApi('/accounts', { method: 'POST', body: JSON.stringify(data), }); } /** * 메일 계정 수정 */ export async function updateMailAccount( id: string, data: UpdateMailAccountDto ): Promise { return fetchApi(`/accounts/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } /** * 메일 계정 삭제 */ export async function deleteMailAccount(id: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(`/accounts/${id}`, { method: 'DELETE', }); } /** * SMTP 연결 테스트 */ export async function testMailConnection(id: string): Promise<{ success: boolean; message: string; }> { return fetchApi<{ success: boolean; message: string }>( `/accounts/${id}/test-connection`, { method: 'POST', } ); } // ============================================ // 메일 템플릿 API // ============================================ /** * 전체 메일 템플릿 목록 조회 */ export async function getMailTemplates(): Promise { return fetchApi('/templates-file'); } /** * 특정 메일 템플릿 조회 */ export async function getMailTemplate(id: string): Promise { return fetchApi(`/templates-file/${id}`); } /** * 메일 템플릿 생성 */ export async function createMailTemplate( data: CreateMailTemplateDto ): Promise { return fetchApi('/templates-file', { method: 'POST', body: JSON.stringify(data), }); } /** * 메일 템플릿 수정 */ export async function updateMailTemplate( id: string, data: UpdateMailTemplateDto ): Promise { return fetchApi(`/templates-file/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } /** * 메일 템플릿 삭제 */ export async function deleteMailTemplate(id: string): Promise<{ success: boolean }> { return fetchApi<{ success: boolean }>(`/templates-file/${id}`, { method: 'DELETE', }); } /** * 메일 템플릿 미리보기 (샘플 데이터) */ export async function previewMailTemplate( id: string, sampleData?: Record ): Promise<{ html: string }> { return fetchApi<{ html: string }>(`/templates-file/${id}/preview`, { method: 'POST', body: JSON.stringify({ sampleData }), }); } // ============================================ // 메일 발송 API (간단한 버전 - 쿼리 제외) // ============================================ /** * 메일 발송 (단건 또는 소규모 발송) */ export async function sendMail(data: SendMailDto): Promise { return fetchApi('/send/simple', { method: 'POST', body: JSON.stringify(data), }); } /** * 템플릿 변수 추출 (템플릿에서 {변수명} 형식 추출) */ 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; } }); 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 getMailDetail( accountId: string, seqno: number ): Promise { return fetchApi(`/mail/receive/${accountId}/${seqno}`); } /** * 메일을 읽음으로 표시 */ 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', }); }