ERP-node/frontend/lib/api/mail.ts

362 lines
8.8 KiB
TypeScript
Raw Normal View History

2025-10-01 16:15:53 +09:00
/**
* 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<CreateMailAccountDto> {
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<string, 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<CreateMailTemplateDto> {}
export interface SendMailDto {
accountId: string;
templateId?: string;
to: string[]; // 수신자 이메일 배열
subject: string;
variables?: Record<string, string>; // 템플릿 변수 치환
customHtml?: string; // 템플릿 없이 직접 HTML 작성 시
}
export interface MailSendResult {
success: boolean;
messageId?: string;
error?: string;
}
// ============================================
// API 기본 설정
// ============================================
const API_BASE_URL = '/api/mail';
async function fetchApi<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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<MailAccount[]> {
return fetchApi<MailAccount[]>('/accounts');
}
/**
*
*/
export async function getMailAccount(id: string): Promise<MailAccount> {
return fetchApi<MailAccount>(`/accounts/${id}`);
}
/**
*
*/
export async function createMailAccount(
data: CreateMailAccountDto
): Promise<MailAccount> {
return fetchApi<MailAccount>('/accounts', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
*
*/
export async function updateMailAccount(
id: string,
data: UpdateMailAccountDto
): Promise<MailAccount> {
return fetchApi<MailAccount>(`/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<MailTemplate[]> {
return fetchApi<MailTemplate[]>('/templates-file');
}
/**
* 릿
*/
export async function getMailTemplate(id: string): Promise<MailTemplate> {
return fetchApi<MailTemplate>(`/templates-file/${id}`);
}
/**
* 릿
*/
export async function createMailTemplate(
data: CreateMailTemplateDto
): Promise<MailTemplate> {
return fetchApi<MailTemplate>('/templates-file', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 릿
*/
export async function updateMailTemplate(
id: string,
data: UpdateMailTemplateDto
): Promise<MailTemplate> {
return fetchApi<MailTemplate>(`/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<string, string>
): 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<MailSendResult> {
return fetchApi<MailSendResult>('/send/simple', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 릿 (릿 {} )
*/
export function extractTemplateVariables(template: MailTemplate): string[] {
const variableRegex = /\{(\w+)\}/g;
const variables = new Set<string>();
// 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, string>
): string {
let html = '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
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 += `<div style="${styleObjectToString(component.styles)}">${content}</div>`;
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 += `
<a href="${component.url || '#'}" style="
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
${styleObjectToString(component.styles)}
">${buttonText}</a>
`;
break;
case 'image':
html += `<img src="${component.src || ''}" style="max-width: 100%; ${styleObjectToString(component.styles)}" />`;
break;
case 'spacer':
html += `<div style="height: ${component.height || 20}px;"></div>`;
break;
}
});
html += '</div>';
return html;
}
function styleObjectToString(styles?: Record<string, string>): 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();
}