Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -2,79 +2,130 @@
|
||||
* 메일 발송 이력 관리 서비스 (파일 기반)
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
SentMailHistory,
|
||||
SentMailListQuery,
|
||||
SentMailListResponse,
|
||||
AttachmentInfo,
|
||||
} from '../types/mailSentHistory';
|
||||
} from "../types/mailSentHistory";
|
||||
|
||||
const SENT_MAIL_DIR = path.join(__dirname, '../../data/mail-sent');
|
||||
// 운영 환경에서는 /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() {
|
||||
// 디렉토리 생성 (없으면)
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
||||
// 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
|
||||
try {
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("메일 발송 이력 디렉토리 생성 실패:", error);
|
||||
// 디렉토리가 이미 존재하거나 권한이 없어도 서비스는 계속 실행
|
||||
// 실제 파일 쓰기 시점에 에러 처리
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 이력 저장
|
||||
*/
|
||||
async saveSentMail(data: Omit<SentMailHistory, 'id' | 'sentAt'>): Promise<SentMailHistory> {
|
||||
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');
|
||||
try {
|
||||
// 디렉토리가 없으면 다시 시도
|
||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), "utf-8");
|
||||
|
||||
console.log("발송 이력 저장:", history.id);
|
||||
} catch (error) {
|
||||
console.error("발송 이력 저장 실패:", error);
|
||||
// 파일 저장 실패해도 history 객체는 반환 (메일 발송은 성공했으므로)
|
||||
}
|
||||
|
||||
console.log('💾 발송 이력 저장:', history.id);
|
||||
return history;
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 이력 목록 조회 (필터링, 페이징)
|
||||
*/
|
||||
async getSentMailList(query: SentMailListQuery): Promise<SentMailListResponse> {
|
||||
async getSentMailList(
|
||||
query: SentMailListQuery
|
||||
): Promise<SentMailListResponse> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
searchTerm = '',
|
||||
status = 'all',
|
||||
searchTerm = "",
|
||||
status = "all",
|
||||
accountId,
|
||||
startDate,
|
||||
endDate,
|
||||
sortBy = 'sentAt',
|
||||
sortOrder = 'desc',
|
||||
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);
|
||||
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 (status !== 'all') {
|
||||
if (status !== "all") {
|
||||
filtered = filtered.filter((h) => h.status === status);
|
||||
}
|
||||
|
||||
@@ -107,15 +158,15 @@ class MailSentHistoryService {
|
||||
let aVal: any = a[sortBy];
|
||||
let bVal: any = b[sortBy];
|
||||
|
||||
if (sortBy === 'sentAt') {
|
||||
if (sortBy === "sentAt") {
|
||||
aVal = new Date(aVal).getTime();
|
||||
bVal = new Date(bVal).getTime();
|
||||
} else {
|
||||
aVal = aVal ? aVal.toLowerCase() : '';
|
||||
bVal = bVal ? bVal.toLowerCase() : '';
|
||||
aVal = aVal ? aVal.toLowerCase() : "";
|
||||
bVal = bVal ? bVal.toLowerCase() : "";
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
if (sortOrder === "asc") {
|
||||
return aVal > bVal ? 1 : -1;
|
||||
} else {
|
||||
return aVal < bVal ? 1 : -1;
|
||||
@@ -149,10 +200,10 @@ class MailSentHistoryService {
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
return JSON.parse(content) as SentMailHistory;
|
||||
} catch (error) {
|
||||
console.error('발송 이력 읽기 실패:', error);
|
||||
console.error("발송 이력 읽기 실패:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -169,10 +220,10 @@ class MailSentHistoryService {
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
console.log('🗑️ 발송 이력 삭제:', id);
|
||||
console.log("🗑️ 발송 이력 삭제:", id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('발송 이력 삭제 실패:', error);
|
||||
console.error("발송 이력 삭제 실패:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -188,34 +239,74 @@ class MailSentHistoryService {
|
||||
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);
|
||||
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 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 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;
|
||||
const thisMonthCount = allHistory.filter(
|
||||
(h) => h.sentAt >= monthStart
|
||||
).length;
|
||||
const successRate =
|
||||
totalSent > 0 ? Math.round((successCount / totalSent) * 100) : 0;
|
||||
|
||||
return {
|
||||
totalSent,
|
||||
@@ -229,4 +320,3 @@ class MailSentHistoryService {
|
||||
}
|
||||
|
||||
export const mailSentHistoryService = new MailSentHistoryService();
|
||||
|
||||
|
||||
@@ -17,27 +17,37 @@ echo "======================================"
|
||||
|
||||
# Git 최신 코드 가져오기
|
||||
echo ""
|
||||
echo "[1/5] Git 최신 코드 가져오기..."
|
||||
echo "[1/6] Git 최신 코드 가져오기..."
|
||||
git pull origin main
|
||||
|
||||
# 호스트 디렉토리 준비
|
||||
echo ""
|
||||
echo "[2/6] 호스트 디렉토리 준비..."
|
||||
mkdir -p /home/vexplor/backend_data/data/mail-sent
|
||||
mkdir -p /home/vexplor/backend_data/uploads
|
||||
mkdir -p /home/vexplor/frontend_data
|
||||
chmod -R 755 /home/vexplor/backend_data
|
||||
chmod -R 755 /home/vexplor/frontend_data
|
||||
echo "디렉토리 생성 완료"
|
||||
|
||||
# 기존 컨테이너 중지 및 제거
|
||||
echo ""
|
||||
echo "[2/5] 기존 컨테이너 중지..."
|
||||
echo "[3/6] 기존 컨테이너 중지..."
|
||||
docker-compose -f "$COMPOSE_FILE" down
|
||||
|
||||
# 오래된 이미지 정리
|
||||
echo ""
|
||||
echo "[3/5] Docker 이미지 정리..."
|
||||
echo "[4/6] Docker 이미지 정리..."
|
||||
docker image prune -f
|
||||
|
||||
# 새로운 이미지 빌드
|
||||
echo ""
|
||||
echo "[4/5] Docker 이미지 빌드..."
|
||||
echo "[5/6] Docker 이미지 빌드..."
|
||||
docker-compose -f "$COMPOSE_FILE" build --no-cache
|
||||
|
||||
# 컨테이너 실행
|
||||
echo ""
|
||||
echo "[5/5] 컨테이너 실행..."
|
||||
echo "[6/6] 컨테이너 실행..."
|
||||
docker-compose -f "$COMPOSE_FILE" up -d
|
||||
|
||||
# 배포 완료
|
||||
|
||||
Reference in New Issue
Block a user