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

515 lines
13 KiB
TypeScript

/**
* 메일 발송 이력 관리 서비스 (파일 기반)
*/
import fs from "fs";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import {
SentMailHistory,
SentMailListQuery,
SentMailListResponse,
AttachmentInfo,
} from "../types/mailSentHistory";
// 운영 환경에서는 /app/data/mail-sent, 개발 환경에서는 프로젝트 루트의 data/mail-sent 사용
const SENT_MAIL_DIR =
process.env.NODE_ENV === "production"
? "/app/data/mail-sent"
: path.join(process.cwd(), "data", "mail-sent");
class MailSentHistoryService {
constructor() {
try {
if (!fs.existsSync(SENT_MAIL_DIR)) {
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
}
} catch (error) {
console.error("메일 발송 이력 디렉토리 생성 실패:", error);
throw error;
}
}
/**
* 발송 이력 저장
*/
async saveSentMail(
data: Omit<SentMailHistory, "id" | "sentAt">
): Promise<SentMailHistory> {
const history: SentMailHistory = {
id: uuidv4(),
sentAt: new Date().toISOString(),
...data,
};
try {
if (!fs.existsSync(SENT_MAIL_DIR)) {
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
}
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), {
encoding: "utf-8",
mode: 0o644,
});
// console.log("발송 이력 저장:", history.id);
} catch (error) {
console.error("발송 이력 저장 실패:", error);
// 파일 저장 실패해도 history 객체는 반환 (메일 발송은 성공했으므로)
}
return history;
}
/**
* 발송 이력 목록 조회 (필터링, 페이징)
*/
async getSentMailList(
query: SentMailListQuery
): Promise<SentMailListResponse> {
const {
page = 1,
limit = 20,
searchTerm = "",
status = "all",
accountId,
startDate,
endDate,
sortBy = "sentAt",
sortOrder = "desc",
} = query;
// 모든 발송 이력 파일 읽기
let allHistory: SentMailHistory[] = [];
try {
// 디렉토리가 없으면 빈 배열 반환
if (!fs.existsSync(SENT_MAIL_DIR)) {
// console.warn("메일 발송 이력 디렉토리가 없습니다:", SENT_MAIL_DIR);
return {
items: [],
total: 0,
page,
limit,
totalPages: 0,
};
}
const files = fs
.readdirSync(SENT_MAIL_DIR)
.filter((f) => f.endsWith(".json"));
for (const file of files) {
try {
const filePath = path.join(SENT_MAIL_DIR, file);
const content = fs.readFileSync(filePath, "utf-8");
const history: SentMailHistory = JSON.parse(content);
allHistory.push(history);
} catch (error) {
console.error(`발송 이력 파일 읽기 실패: ${file}`, error);
}
}
} catch (error) {
console.error("메일 발송 이력 조회 실패:", error);
return {
items: [],
total: 0,
page,
limit,
totalPages: 0,
};
}
// 필터링
let filtered = allHistory;
// 삭제된 메일 필터
if (query.onlyDeleted) {
filtered = filtered.filter((h) => h.deletedAt);
} else if (!query.includeDeleted) {
filtered = filtered.filter((h) => !h.deletedAt);
}
// 상태 필터
if (status !== "all") {
filtered = filtered.filter((h) => h.status === status);
}
// 계정 필터
if (accountId) {
filtered = filtered.filter((h) => h.accountId === accountId);
}
// 날짜 필터
if (startDate) {
filtered = filtered.filter((h) => h.sentAt >= startDate);
}
if (endDate) {
filtered = filtered.filter((h) => h.sentAt <= endDate);
}
// 검색어 필터 (제목, 받는사람)
if (searchTerm) {
const term = searchTerm.toLowerCase();
filtered = filtered.filter(
(h) =>
h.subject.toLowerCase().includes(term) ||
h.to.some((email) => email.toLowerCase().includes(term)) ||
(h.cc && h.cc.some((email) => email.toLowerCase().includes(term)))
);
}
// 정렬
filtered.sort((a, b) => {
let aVal: any = a[sortBy];
let bVal: any = b[sortBy];
if (sortBy === "sentAt") {
aVal = new Date(aVal).getTime();
bVal = new Date(bVal).getTime();
} else {
aVal = aVal ? aVal.toLowerCase() : "";
bVal = bVal ? bVal.toLowerCase() : "";
}
if (sortOrder === "asc") {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
// 페이징
const total = filtered.length;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const items = filtered.slice(start, end);
return {
items,
total,
page,
limit,
totalPages,
};
}
/**
* 특정 발송 이력 조회
*/
async getSentMailById(id: string): Promise<SentMailHistory | null> {
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, "utf-8");
return JSON.parse(content) as SentMailHistory;
} catch (error) {
console.error("발송 이력 읽기 실패:", error);
return null;
}
}
/**
* 임시 저장 (Draft)
*/
async saveDraft(
data: Partial<SentMailHistory> & { accountId: string }
): Promise<SentMailHistory> {
// console.log("📥 백엔드에서 받은 임시 저장 데이터:", data);
const now = new Date().toISOString();
const draft: SentMailHistory = {
id: data.id || uuidv4(),
accountId: data.accountId,
accountName: data.accountName || "",
accountEmail: data.accountEmail || "",
to: data.to || [],
cc: data.cc,
bcc: data.bcc,
subject: data.subject || "",
htmlContent: data.htmlContent || "",
templateId: data.templateId,
templateName: data.templateName,
attachments: data.attachments,
sentAt: data.sentAt || now,
status: "draft",
isDraft: true,
updatedAt: now,
};
// console.log("💾 저장할 draft 객체:", draft);
try {
if (!fs.existsSync(SENT_MAIL_DIR)) {
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true, mode: 0o755 });
}
const filePath = path.join(SENT_MAIL_DIR, `${draft.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(draft, null, 2), {
encoding: "utf-8",
mode: 0o644,
});
// console.log("💾 임시 저장:", draft.id);
} catch (error) {
console.error("임시 저장 실패:", error);
throw error;
}
return draft;
}
/**
* 임시 저장 업데이트
*/
async updateDraft(
id: string,
data: Partial<SentMailHistory>
): Promise<SentMailHistory | null> {
const existing = await this.getSentMailById(id);
if (!existing) {
return null;
}
const updated: SentMailHistory = {
...existing,
...data,
id: existing.id,
updatedAt: new Date().toISOString(),
};
try {
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), {
encoding: "utf-8",
mode: 0o644,
});
// console.log("✏️ 임시 저장 업데이트:", id);
return updated;
} catch (error) {
console.error("임시 저장 업데이트 실패:", error);
return null;
}
}
/**
* 발송 이력 삭제 (Soft Delete)
*/
async deleteSentMail(id: string): Promise<boolean> {
const existing = await this.getSentMailById(id);
if (!existing) {
return false;
}
const updated: SentMailHistory = {
...existing,
deletedAt: new Date().toISOString(),
};
try {
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), {
encoding: "utf-8",
mode: 0o644,
});
// console.log("🗑️ 메일 삭제 (Soft Delete):", id);
return true;
} catch (error) {
console.error("메일 삭제 실패:", error);
return false;
}
}
/**
* 메일 복구
*/
async restoreMail(id: string): Promise<boolean> {
const existing = await this.getSentMailById(id);
if (!existing || !existing.deletedAt) {
return false;
}
const updated: SentMailHistory = {
...existing,
deletedAt: undefined,
};
try {
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), {
encoding: "utf-8",
mode: 0o644,
});
// console.log("♻️ 메일 복구:", id);
return true;
} catch (error) {
console.error("메일 복구 실패:", error);
return false;
}
}
/**
* 메일 영구 삭제 (Hard Delete)
*/
async permanentlyDeleteMail(id: string): Promise<boolean> {
const filePath = path.join(SENT_MAIL_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return false;
}
try {
fs.unlinkSync(filePath);
// console.log("🗑️ 메일 영구 삭제:", id);
return true;
} catch (error) {
console.error("메일 영구 삭제 실패:", error);
return false;
}
}
/**
* 30일 이상 지난 삭제된 메일 자동 영구 삭제
*/
async cleanupOldDeletedMails(): Promise<number> {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
let deletedCount = 0;
try {
if (!fs.existsSync(SENT_MAIL_DIR)) {
return 0;
}
const files = fs
.readdirSync(SENT_MAIL_DIR)
.filter((f) => f.endsWith(".json"));
for (const file of files) {
try {
const filePath = path.join(SENT_MAIL_DIR, file);
const content = fs.readFileSync(filePath, "utf-8");
const mail: SentMailHistory = JSON.parse(content);
if (mail.deletedAt) {
const deletedDate = new Date(mail.deletedAt);
if (deletedDate < thirtyDaysAgo) {
fs.unlinkSync(filePath);
deletedCount++;
// console.log("🗑️ 30일 지난 메일 자동 삭제:", mail.id);
}
}
} catch (error) {
console.error(`파일 처리 실패: ${file}`, error);
}
}
} catch (error) {
console.error("자동 삭제 실패:", error);
}
return deletedCount;
}
/**
* 통계 조회
*/
async getStatistics(accountId?: string): Promise<{
totalSent: number;
successCount: number;
failedCount: number;
todayCount: number;
thisMonthCount: number;
successRate: number;
}> {
let allHistory: SentMailHistory[] = [];
try {
// 디렉토리가 없으면 빈 통계 반환
if (!fs.existsSync(SENT_MAIL_DIR)) {
return {
totalSent: 0,
successCount: 0,
failedCount: 0,
todayCount: 0,
thisMonthCount: 0,
successRate: 0,
};
}
const files = fs
.readdirSync(SENT_MAIL_DIR)
.filter((f) => f.endsWith(".json"));
for (const file of files) {
try {
const filePath = path.join(SENT_MAIL_DIR, file);
const content = fs.readFileSync(filePath, "utf-8");
const history: SentMailHistory = JSON.parse(content);
// 계정 필터
if (!accountId || history.accountId === accountId) {
allHistory.push(history);
}
} catch (error) {
console.error(`발송 이력 파일 읽기 실패: ${file}`, error);
}
}
} catch (error) {
console.error("통계 조회 실패:", error);
return {
totalSent: 0,
successCount: 0,
failedCount: 0,
todayCount: 0,
thisMonthCount: 0,
successRate: 0,
};
}
const now = new Date();
const todayStart = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate()
).toISOString();
const monthStart = new Date(
now.getFullYear(),
now.getMonth(),
1
).toISOString();
const totalSent = allHistory.length;
const successCount = allHistory.filter(
(h) => h.status === "success"
).length;
const failedCount = allHistory.filter((h) => h.status === "failed").length;
const todayCount = allHistory.filter((h) => h.sentAt >= todayStart).length;
const thisMonthCount = allHistory.filter(
(h) => h.sentAt >= monthStart
).length;
const successRate =
totalSent > 0 ? Math.round((successCount / totalSent) * 100) : 0;
return {
totalSent,
successCount,
failedCount,
todayCount,
thisMonthCount,
successRate,
};
}
}
export const mailSentHistoryService = new MailSentHistoryService();