import fs from "fs/promises"; import path from "path"; // MailComponent 인터페이스 정의 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; } // 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() { // 운영 환경에서는 /app/uploads/mail-templates, 개발 환경에서는 프로젝트 루트 this.templatesDir = process.env.NODE_ENV === "production" ? "/app/uploads/mail-templates" : path.join(process.cwd(), "uploads", "mail-templates"); // 동기적으로 디렉토리 생성 this.ensureDirectoryExistsSync(); } /** * 템플릿 디렉토리 생성 (동기) */ private ensureDirectoryExistsSync() { try { const fsSync = require('fs'); fsSync.accessSync(this.templatesDir); } catch { const fsSync = require('fs'); fsSync.mkdirSync(this.templatesDir, { recursive: true, mode: 0o755 }); } } /** * 템플릿 디렉토리 생성 (비동기) */ private async ensureDirectoryExists() { try { await fs.access(this.templatesDir); } catch { await fs.mkdir(this.templatesDir, { recursive: true, mode: 0o755 }); } } /** * 템플릿 파일 경로 생성 */ private getTemplatePath(id: string): string { return path.join(this.templatesDir, `${id}.json`); } /** * 모든 템플릿 목록 조회 */ async getAllTemplates(): Promise { 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 { 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" ); // // console.log(`✅ 템플릿 저장 성공: ${id}`); return updated; } catch (error) { // console.error(`❌ 템플릿 저장 실패: ${id}`, error); throw error; // 에러를 컨트롤러로 전달 } } /** * 템플릿 삭제 */ 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; case "header": html += `
${comp.logoSrc ? `로고` : ''} ${comp.brandName || ''} ${comp.sendDate || ''}
`; break; case "infoTable": html += `
${comp.tableTitle ? `
${comp.tableTitle}
` : ''} ${(comp.rows || []).map((row, i) => ` `).join('')}
${row.label} ${row.value}
`; break; case "alertBox": const alertColors: Record = { 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[comp.alertType || 'info']; html += `
${comp.alertTitle ? `
${comp.alertTitle}
` : ''}
${comp.content || ''}
`; break; case "divider": html += `
`; break; case "footer": html += `
${comp.companyName ? `
${comp.companyName}
` : ''} ${(comp.ceoName || comp.businessNumber) ? `
${comp.ceoName ? `대표: ${comp.ceoName}` : ''} ${comp.ceoName && comp.businessNumber ? ' | ' : ''} ${comp.businessNumber ? `사업자등록번호: ${comp.businessNumber}` : ''}
` : ''} ${comp.address ? `
${comp.address}
` : ''} ${(comp.phone || comp.email) ? `
${comp.phone ? `Tel: ${comp.phone}` : ''} ${comp.phone && comp.email ? ' | ' : ''} ${comp.email ? `Email: ${comp.email}` : ''}
` : ''} ${comp.copyright ? `
${comp.copyright}
` : ''}
`; break; case "numberedList": html += `
${comp.listTitle ? `
${comp.listTitle}
` : ''}
    ${(comp.listItems || []).map(item => `
  1. ${item}
  2. `).join('')}
`; 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();