mail-templates도 수정
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
// MailComponent 인터페이스 정의
|
||||
export interface MailComponent {
|
||||
@@ -30,7 +30,7 @@ export interface MailTemplate {
|
||||
queries: QueryConfig[];
|
||||
};
|
||||
recipientConfig?: {
|
||||
type: 'query' | 'manual';
|
||||
type: "query" | "manual";
|
||||
emailField?: string;
|
||||
nameField?: string;
|
||||
queryId?: string;
|
||||
@@ -45,19 +45,26 @@ class MailTemplateFileService {
|
||||
private templatesDir: string;
|
||||
|
||||
constructor() {
|
||||
// uploads/mail-templates 디렉토리 사용
|
||||
this.templatesDir = path.join(process.cwd(), 'uploads', 'mail-templates');
|
||||
// 운영 환경에서는 /app/uploads/mail-templates, 개발 환경에서는 프로젝트 루트
|
||||
this.templatesDir =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "/app/uploads/mail-templates"
|
||||
: path.join(process.cwd(), "uploads", "mail-templates");
|
||||
this.ensureDirectoryExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 디렉토리 생성 (없으면)
|
||||
* 템플릿 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
|
||||
*/
|
||||
private async ensureDirectoryExists() {
|
||||
try {
|
||||
await fs.access(this.templatesDir);
|
||||
} catch {
|
||||
await fs.mkdir(this.templatesDir, { recursive: true });
|
||||
try {
|
||||
await fs.mkdir(this.templatesDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error("메일 템플릿 디렉토리 생성 실패:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,24 +80,25 @@ class MailTemplateFileService {
|
||||
*/
|
||||
async getAllTemplates(): Promise<MailTemplate[]> {
|
||||
await this.ensureDirectoryExists();
|
||||
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this.templatesDir);
|
||||
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
||||
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'
|
||||
"utf-8"
|
||||
);
|
||||
return JSON.parse(content) as MailTemplate;
|
||||
})
|
||||
);
|
||||
|
||||
// 최신순 정렬
|
||||
return templates.sort((a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
return templates.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
} catch (error) {
|
||||
return [];
|
||||
@@ -102,7 +110,7 @@ class MailTemplateFileService {
|
||||
*/
|
||||
async getTemplateById(id: string): Promise<MailTemplate | null> {
|
||||
try {
|
||||
const content = await fs.readFile(this.getTemplatePath(id), 'utf-8');
|
||||
const content = await fs.readFile(this.getTemplatePath(id), "utf-8");
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -113,7 +121,7 @@ class MailTemplateFileService {
|
||||
* 템플릿 생성
|
||||
*/
|
||||
async createTemplate(
|
||||
data: Omit<MailTemplate, 'id' | 'createdAt' | 'updatedAt'>
|
||||
data: Omit<MailTemplate, "id" | "createdAt" | "updatedAt">
|
||||
): Promise<MailTemplate> {
|
||||
const id = `template-${Date.now()}`;
|
||||
const now = new Date().toISOString();
|
||||
@@ -128,7 +136,7 @@ class MailTemplateFileService {
|
||||
await fs.writeFile(
|
||||
this.getTemplatePath(id),
|
||||
JSON.stringify(template, null, 2),
|
||||
'utf-8'
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
return template;
|
||||
@@ -139,7 +147,7 @@ class MailTemplateFileService {
|
||||
*/
|
||||
async updateTemplate(
|
||||
id: string,
|
||||
data: Partial<Omit<MailTemplate, 'id' | 'createdAt'>>
|
||||
data: Partial<Omit<MailTemplate, "id" | "createdAt">>
|
||||
): Promise<MailTemplate | null> {
|
||||
try {
|
||||
const existing = await this.getTemplateById(id);
|
||||
@@ -161,7 +169,7 @@ class MailTemplateFileService {
|
||||
await fs.writeFile(
|
||||
this.getTemplatePath(id),
|
||||
JSON.stringify(updated, null, 2),
|
||||
'utf-8'
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
// console.log(`✅ 템플릿 저장 성공: ${id}`);
|
||||
@@ -188,40 +196,41 @@ class MailTemplateFileService {
|
||||
* 템플릿을 HTML로 렌더링
|
||||
*/
|
||||
renderTemplateToHtml(components: MailComponent[]): string {
|
||||
let html = '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
|
||||
let html =
|
||||
'<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
|
||||
|
||||
components.forEach(comp => {
|
||||
components.forEach((comp) => {
|
||||
const styles = Object.entries(comp.styles || {})
|
||||
.map(([key, value]) => `${this.camelToKebab(key)}: ${value}`)
|
||||
.join('; ');
|
||||
.join("; ");
|
||||
|
||||
switch (comp.type) {
|
||||
case 'text':
|
||||
html += `<div style="${styles}">${comp.content || ''}</div>`;
|
||||
case "text":
|
||||
html += `<div style="${styles}">${comp.content || ""}</div>`;
|
||||
break;
|
||||
case 'button':
|
||||
case "button":
|
||||
html += `<div style="text-align: center; ${styles}">
|
||||
<a href="${comp.url || '#'}"
|
||||
<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'};
|
||||
background-color: ${comp.styles?.backgroundColor || "#007bff"};
|
||||
color: ${comp.styles?.color || "#fff"};
|
||||
border-radius: 4px;">
|
||||
${comp.text || 'Button'}
|
||||
${comp.text || "Button"}
|
||||
</a>
|
||||
</div>`;
|
||||
break;
|
||||
case 'image':
|
||||
case "image":
|
||||
html += `<div style="${styles}">
|
||||
<img src="${comp.src || ''}" alt="" style="max-width: 100%; height: auto;" />
|
||||
<img src="${comp.src || ""}" alt="" style="max-width: 100%; height: auto;" />
|
||||
</div>`;
|
||||
break;
|
||||
case 'spacer':
|
||||
case "spacer":
|
||||
html += `<div style="height: ${comp.height || 20}px;"></div>`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
html += "</div>";
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -229,7 +238,7 @@ class MailTemplateFileService {
|
||||
* camelCase를 kebab-case로 변환
|
||||
*/
|
||||
private camelToKebab(str: string): string {
|
||||
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
||||
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,7 +246,7 @@ class MailTemplateFileService {
|
||||
*/
|
||||
async getTemplatesByCategory(category: string): Promise<MailTemplate[]> {
|
||||
const allTemplates = await this.getAllTemplates();
|
||||
return allTemplates.filter(t => t.category === category);
|
||||
return allTemplates.filter((t) => t.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,14 +255,14 @@ class MailTemplateFileService {
|
||||
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)
|
||||
|
||||
return allTemplates.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(lowerKeyword) ||
|
||||
t.subject.toLowerCase().includes(lowerKeyword) ||
|
||||
t.category?.toLowerCase().includes(lowerKeyword)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const mailTemplateFileService = new MailTemplateFileService();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user