233 lines
6.1 KiB
TypeScript
233 lines
6.1 KiB
TypeScript
/**
|
|
* 메일 발송 이력 관리 서비스 (파일 기반)
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import {
|
|
SentMailHistory,
|
|
SentMailListQuery,
|
|
SentMailListResponse,
|
|
AttachmentInfo,
|
|
} from '../types/mailSentHistory';
|
|
|
|
const SENT_MAIL_DIR = path.join(__dirname, '../../data/mail-sent');
|
|
|
|
class MailSentHistoryService {
|
|
constructor() {
|
|
// 디렉토리 생성 (없으면)
|
|
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
|
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 발송 이력 저장
|
|
*/
|
|
async saveSentMail(data: Omit<SentMailHistory, 'id' | 'sentAt'>): Promise<SentMailHistory> {
|
|
const history: SentMailHistory = {
|
|
id: uuidv4(),
|
|
sentAt: new Date().toISOString(),
|
|
...data,
|
|
};
|
|
|
|
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
|
|
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), 'utf-8');
|
|
|
|
console.log('💾 발송 이력 저장:', history.id);
|
|
return history;
|
|
}
|
|
|
|
/**
|
|
* 발송 이력 목록 조회 (필터링, 페이징)
|
|
*/
|
|
async getSentMailList(query: SentMailListQuery): Promise<SentMailListResponse> {
|
|
const {
|
|
page = 1,
|
|
limit = 20,
|
|
searchTerm = '',
|
|
status = 'all',
|
|
accountId,
|
|
startDate,
|
|
endDate,
|
|
sortBy = 'sentAt',
|
|
sortOrder = 'desc',
|
|
} = query;
|
|
|
|
// 모든 발송 이력 파일 읽기
|
|
const files = fs.readdirSync(SENT_MAIL_DIR).filter((f) => f.endsWith('.json'));
|
|
let allHistory: SentMailHistory[] = [];
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 필터링
|
|
let filtered = allHistory;
|
|
|
|
// 상태 필터
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 발송 이력 삭제
|
|
*/
|
|
async deleteSentMail(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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 통계 조회
|
|
*/
|
|
async getStatistics(accountId?: string): Promise<{
|
|
totalSent: number;
|
|
successCount: number;
|
|
failedCount: number;
|
|
todayCount: number;
|
|
thisMonthCount: number;
|
|
successRate: number;
|
|
}> {
|
|
const files = fs.readdirSync(SENT_MAIL_DIR).filter((f) => f.endsWith('.json'));
|
|
let allHistory: SentMailHistory[] = [];
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|