import fs from 'fs/promises'; import path from 'path'; // MailComponent 인터페이스 정의 export interface MailComponent { id: string; type: "text" | "button" | "image" | "spacer"; content?: string; text?: string; url?: string; src?: string; height?: number; styles?: Record; } // QueryConfig 인터페이스 정의 (사용하지 않지만 타입 호환성 유지) export interface QueryConfig { id: string; name: string; sql: string; parameters: any[]; } 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 { 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 { try { const content = await fs.readFile(this.getTemplatePath(id), 'utf-8'); return JSON.parse(content); } catch { return null; } } /** * 템플릿 생성 */ async createTemplate( data: Omit ): Promise { 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> ): Promise { const existing = await this.getTemplateById(id); if (!existing) { return null; } const updated: MailTemplate = { ...existing, ...data, id: existing.id, createdAt: existing.createdAt, updatedAt: new Date().toISOString(), }; await fs.writeFile( this.getTemplatePath(id), JSON.stringify(updated, null, 2), 'utf-8' ); return updated; } /** * 템플릿 삭제 */ async deleteTemplate(id: string): Promise { try { await fs.unlink(this.getTemplatePath(id)); return true; } catch { return false; } } /** * 템플릿을 HTML로 렌더링 */ renderTemplateToHtml(components: MailComponent[]): string { let html = '
'; components.forEach(comp => { const styles = Object.entries(comp.styles || {}) .map(([key, value]) => `${this.camelToKebab(key)}: ${value}`) .join('; '); switch (comp.type) { case 'text': html += `
${comp.content || ''}
`; break; case 'button': html += ``; break; case 'image': html += `
`; break; case 'spacer': html += `
`; break; } }); html += '
'; return html; } /** * camelCase를 kebab-case로 변환 */ private camelToKebab(str: string): string { return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); } /** * 카테고리별 템플릿 조회 */ async getTemplatesByCategory(category: string): Promise { const allTemplates = await this.getAllTemplates(); return allTemplates.filter(t => t.category === category); } /** * 템플릿 검색 */ async searchTemplates(keyword: string): Promise { 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();