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() { // 운영 환경에서는 /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; } }); 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();