ERP-node/backend-node/src/services/mailTemplateFileService.ts

260 lines
6.5 KiB
TypeScript
Raw Normal View History

2025-10-01 16:15:53 +09:00
import fs from 'fs/promises';
import path from 'path';
2025-10-01 17:01:31 +09:00
// MailComponent 인터페이스 정의
export interface MailComponent {
id: string;
type: "text" | "button" | "image" | "spacer";
content?: string;
text?: string;
url?: string;
src?: string;
height?: number;
styles?: Record<string, string>;
}
// QueryConfig 인터페이스 정의 (사용하지 않지만 타입 호환성 유지)
export interface QueryConfig {
id: string;
name: string;
sql: string;
parameters: any[];
}
2025-10-01 16:15:53 +09:00
export interface MailTemplate {
id: string;
name: string;
subject: string;
components: MailComponent[];
queryConfig?: {
queries: QueryConfig[];
};
recipientConfig?: {
type: 'query' | 'manual';
emailField?: string;
nameField?: string;
queryId?: string;
manualList?: Array<{ email: string; name?: string }>;
};
category?: string;
createdAt: string;
updatedAt: string;
}
class MailTemplateFileService {
private templatesDir: string;
constructor() {
// uploads/mail-templates 디렉토리 사용
this.templatesDir = path.join(process.cwd(), 'uploads', 'mail-templates');
this.ensureDirectoryExists();
}
/**
* 릿 ()
*/
private async ensureDirectoryExists() {
try {
await fs.access(this.templatesDir);
} catch {
await fs.mkdir(this.templatesDir, { recursive: true });
}
}
/**
* 릿
*/
private getTemplatePath(id: string): string {
return path.join(this.templatesDir, `${id}.json`);
}
/**
* 릿
*/
async getAllTemplates(): Promise<MailTemplate[]> {
await this.ensureDirectoryExists();
try {
const files = await fs.readdir(this.templatesDir);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const templates = await Promise.all(
jsonFiles.map(async (file) => {
const content = await fs.readFile(
path.join(this.templatesDir, file),
'utf-8'
);
return JSON.parse(content) as MailTemplate;
})
);
// 최신순 정렬
return templates.sort((a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
} catch (error) {
return [];
}
}
/**
* 릿
*/
async getTemplateById(id: string): Promise<MailTemplate | null> {
try {
const content = await fs.readFile(this.getTemplatePath(id), 'utf-8');
return JSON.parse(content);
} catch {
return null;
}
}
/**
* 릿
*/
async createTemplate(
data: Omit<MailTemplate, 'id' | 'createdAt' | 'updatedAt'>
): Promise<MailTemplate> {
const id = `template-${Date.now()}`;
const now = new Date().toISOString();
const template: MailTemplate = {
...data,
id,
createdAt: now,
updatedAt: now,
};
await fs.writeFile(
this.getTemplatePath(id),
JSON.stringify(template, null, 2),
'utf-8'
);
return template;
}
/**
* 릿
*/
async updateTemplate(
id: string,
data: Partial<Omit<MailTemplate, 'id' | 'createdAt'>>
): Promise<MailTemplate | null> {
2025-10-13 15:17:34 +09:00
try {
const existing = await this.getTemplateById(id);
if (!existing) {
// console.error(`❌ 템플릿을 찾을 수 없음: ${id}`);
return null;
}
const updated: MailTemplate = {
...existing,
...data,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
// console.log(`📝 템플릿 저장 시도: ${id}, 크기: ${JSON.stringify(updated).length} bytes`);
await fs.writeFile(
this.getTemplatePath(id),
JSON.stringify(updated, null, 2),
'utf-8'
);
2025-10-01 16:15:53 +09:00
2025-10-13 15:17:34 +09:00
// console.log(`✅ 템플릿 저장 성공: ${id}`);
return updated;
} catch (error) {
// console.error(`❌ 템플릿 저장 실패: ${id}`, error);
throw error; // 에러를 컨트롤러로 전달
}
2025-10-01 16:15:53 +09:00
}
/**
* 릿
*/
async deleteTemplate(id: string): Promise<boolean> {
try {
await fs.unlink(this.getTemplatePath(id));
return true;
} catch {
return false;
}
}
/**
* 릿 HTML로
*/
renderTemplateToHtml(components: MailComponent[]): string {
let html = '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
components.forEach(comp => {
const styles = Object.entries(comp.styles || {})
.map(([key, value]) => `${this.camelToKebab(key)}: ${value}`)
.join('; ');
switch (comp.type) {
case 'text':
html += `<div style="${styles}">${comp.content || ''}</div>`;
break;
case 'button':
html += `<div style="text-align: center; ${styles}">
<a href="${comp.url || '#'}"
style="display: inline-block; padding: 12px 24px; text-decoration: none;
background-color: ${comp.styles?.backgroundColor || '#007bff'};
color: ${comp.styles?.color || '#fff'};
border-radius: 4px;">
${comp.text || 'Button'}
</a>
</div>`;
break;
case 'image':
html += `<div style="${styles}">
<img src="${comp.src || ''}" alt="" style="max-width: 100%; height: auto;" />
</div>`;
break;
case 'spacer':
html += `<div style="height: ${comp.height || 20}px;"></div>`;
break;
}
});
html += '</div>';
return html;
}
/**
* camelCase를 kebab-case로
*/
private camelToKebab(str: string): string {
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}
/**
* 릿
*/
async getTemplatesByCategory(category: string): Promise<MailTemplate[]> {
const allTemplates = await this.getAllTemplates();
return allTemplates.filter(t => t.category === category);
}
/**
* 릿
*/
async searchTemplates(keyword: string): Promise<MailTemplate[]> {
const allTemplates = await this.getAllTemplates();
const lowerKeyword = keyword.toLowerCase();
return allTemplates.filter(t =>
t.name.toLowerCase().includes(lowerKeyword) ||
t.subject.toLowerCase().includes(lowerKeyword) ||
t.category?.toLowerCase().includes(lowerKeyword)
);
}
}
export const mailTemplateFileService = new MailTemplateFileService();