Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
6f23d12be1
|
|
@ -2,79 +2,130 @@
|
||||||
* 메일 발송 이력 관리 서비스 (파일 기반)
|
* 메일 발송 이력 관리 서비스 (파일 기반)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import {
|
import {
|
||||||
SentMailHistory,
|
SentMailHistory,
|
||||||
SentMailListQuery,
|
SentMailListQuery,
|
||||||
SentMailListResponse,
|
SentMailListResponse,
|
||||||
AttachmentInfo,
|
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 {
|
class MailSentHistoryService {
|
||||||
constructor() {
|
constructor() {
|
||||||
// 디렉토리 생성 (없으면)
|
// 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
|
||||||
|
try {
|
||||||
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
if (!fs.existsSync(SENT_MAIL_DIR)) {
|
||||||
fs.mkdirSync(SENT_MAIL_DIR, { recursive: true });
|
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 = {
|
const history: SentMailHistory = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
sentAt: new Date().toISOString(),
|
sentAt: new Date().toISOString(),
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filePath = path.join(SENT_MAIL_DIR, `${history.id}.json`);
|
try {
|
||||||
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), 'utf-8');
|
// 디렉토리가 없으면 다시 시도
|
||||||
|
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;
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 발송 이력 목록 조회 (필터링, 페이징)
|
* 발송 이력 목록 조회 (필터링, 페이징)
|
||||||
*/
|
*/
|
||||||
async getSentMailList(query: SentMailListQuery): Promise<SentMailListResponse> {
|
async getSentMailList(
|
||||||
|
query: SentMailListQuery
|
||||||
|
): Promise<SentMailListResponse> {
|
||||||
const {
|
const {
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
searchTerm = '',
|
searchTerm = "",
|
||||||
status = 'all',
|
status = "all",
|
||||||
accountId,
|
accountId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
sortBy = 'sentAt',
|
sortBy = "sentAt",
|
||||||
sortOrder = 'desc',
|
sortOrder = "desc",
|
||||||
} = query;
|
} = query;
|
||||||
|
|
||||||
// 모든 발송 이력 파일 읽기
|
// 모든 발송 이력 파일 읽기
|
||||||
const files = fs.readdirSync(SENT_MAIL_DIR).filter((f) => f.endsWith('.json'));
|
|
||||||
let allHistory: SentMailHistory[] = [];
|
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) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(SENT_MAIL_DIR, file);
|
const filePath = path.join(SENT_MAIL_DIR, file);
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
const history: SentMailHistory = JSON.parse(content);
|
const history: SentMailHistory = JSON.parse(content);
|
||||||
allHistory.push(history);
|
allHistory.push(history);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`발송 이력 파일 읽기 실패: ${file}`, error);
|
console.error(`발송 이력 파일 읽기 실패: ${file}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("메일 발송 이력 조회 실패:", error);
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 필터링
|
// 필터링
|
||||||
let filtered = allHistory;
|
let filtered = allHistory;
|
||||||
|
|
||||||
// 상태 필터
|
// 상태 필터
|
||||||
if (status !== 'all') {
|
if (status !== "all") {
|
||||||
filtered = filtered.filter((h) => h.status === status);
|
filtered = filtered.filter((h) => h.status === status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,15 +158,15 @@ class MailSentHistoryService {
|
||||||
let aVal: any = a[sortBy];
|
let aVal: any = a[sortBy];
|
||||||
let bVal: any = b[sortBy];
|
let bVal: any = b[sortBy];
|
||||||
|
|
||||||
if (sortBy === 'sentAt') {
|
if (sortBy === "sentAt") {
|
||||||
aVal = new Date(aVal).getTime();
|
aVal = new Date(aVal).getTime();
|
||||||
bVal = new Date(bVal).getTime();
|
bVal = new Date(bVal).getTime();
|
||||||
} else {
|
} else {
|
||||||
aVal = aVal ? aVal.toLowerCase() : '';
|
aVal = aVal ? aVal.toLowerCase() : "";
|
||||||
bVal = bVal ? bVal.toLowerCase() : '';
|
bVal = bVal ? bVal.toLowerCase() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortOrder === 'asc') {
|
if (sortOrder === "asc") {
|
||||||
return aVal > bVal ? 1 : -1;
|
return aVal > bVal ? 1 : -1;
|
||||||
} else {
|
} else {
|
||||||
return aVal < bVal ? 1 : -1;
|
return aVal < bVal ? 1 : -1;
|
||||||
|
|
@ -149,10 +200,10 @@ class MailSentHistoryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
return JSON.parse(content) as SentMailHistory;
|
return JSON.parse(content) as SentMailHistory;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('발송 이력 읽기 실패:', error);
|
console.error("발송 이력 읽기 실패:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,10 +220,10 @@ class MailSentHistoryService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
console.log('🗑️ 발송 이력 삭제:', id);
|
console.log("🗑️ 발송 이력 삭제:", id);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('발송 이력 삭제 실패:', error);
|
console.error("발송 이력 삭제 실패:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,13 +239,29 @@ class MailSentHistoryService {
|
||||||
thisMonthCount: number;
|
thisMonthCount: number;
|
||||||
successRate: number;
|
successRate: number;
|
||||||
}> {
|
}> {
|
||||||
const files = fs.readdirSync(SENT_MAIL_DIR).filter((f) => f.endsWith('.json'));
|
|
||||||
let allHistory: SentMailHistory[] = [];
|
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) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(SENT_MAIL_DIR, file);
|
const filePath = path.join(SENT_MAIL_DIR, file);
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, "utf-8");
|
||||||
const history: SentMailHistory = JSON.parse(content);
|
const history: SentMailHistory = JSON.parse(content);
|
||||||
|
|
||||||
// 계정 필터
|
// 계정 필터
|
||||||
|
|
@ -205,17 +272,41 @@ class MailSentHistoryService {
|
||||||
console.error(`발송 이력 파일 읽기 실패: ${file}`, 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 now = new Date();
|
||||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
|
const todayStart = new Date(
|
||||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate()
|
||||||
|
).toISOString();
|
||||||
|
const monthStart = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
1
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
const totalSent = allHistory.length;
|
const totalSent = allHistory.length;
|
||||||
const successCount = allHistory.filter((h) => h.status === 'success').length;
|
const successCount = allHistory.filter(
|
||||||
const failedCount = allHistory.filter((h) => h.status === 'failed').length;
|
(h) => h.status === "success"
|
||||||
|
).length;
|
||||||
|
const failedCount = allHistory.filter((h) => h.status === "failed").length;
|
||||||
const todayCount = allHistory.filter((h) => h.sentAt >= todayStart).length;
|
const todayCount = allHistory.filter((h) => h.sentAt >= todayStart).length;
|
||||||
const thisMonthCount = allHistory.filter((h) => h.sentAt >= monthStart).length;
|
const thisMonthCount = allHistory.filter(
|
||||||
const successRate = totalSent > 0 ? Math.round((successCount / totalSent) * 100) : 0;
|
(h) => h.sentAt >= monthStart
|
||||||
|
).length;
|
||||||
|
const successRate =
|
||||||
|
totalSent > 0 ? Math.round((successCount / totalSent) * 100) : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalSent,
|
totalSent,
|
||||||
|
|
@ -229,4 +320,3 @@ class MailSentHistoryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mailSentHistoryService = new MailSentHistoryService();
|
export const mailSentHistoryService = new MailSentHistoryService();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,37 @@ echo "======================================"
|
||||||
|
|
||||||
# Git 최신 코드 가져오기
|
# Git 최신 코드 가져오기
|
||||||
echo ""
|
echo ""
|
||||||
echo "[1/5] Git 최신 코드 가져오기..."
|
echo "[1/6] Git 최신 코드 가져오기..."
|
||||||
git pull origin main
|
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 ""
|
||||||
echo "[2/5] 기존 컨테이너 중지..."
|
echo "[3/6] 기존 컨테이너 중지..."
|
||||||
docker-compose -f "$COMPOSE_FILE" down
|
docker-compose -f "$COMPOSE_FILE" down
|
||||||
|
|
||||||
# 오래된 이미지 정리
|
# 오래된 이미지 정리
|
||||||
echo ""
|
echo ""
|
||||||
echo "[3/5] Docker 이미지 정리..."
|
echo "[4/6] Docker 이미지 정리..."
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
|
|
||||||
# 새로운 이미지 빌드
|
# 새로운 이미지 빌드
|
||||||
echo ""
|
echo ""
|
||||||
echo "[4/5] Docker 이미지 빌드..."
|
echo "[5/6] Docker 이미지 빌드..."
|
||||||
docker-compose -f "$COMPOSE_FILE" build --no-cache
|
docker-compose -f "$COMPOSE_FILE" build --no-cache
|
||||||
|
|
||||||
# 컨테이너 실행
|
# 컨테이너 실행
|
||||||
echo ""
|
echo ""
|
||||||
echo "[5/5] 컨테이너 실행..."
|
echo "[6/6] 컨테이너 실행..."
|
||||||
docker-compose -f "$COMPOSE_FILE" up -d
|
docker-compose -f "$COMPOSE_FILE" up -d
|
||||||
|
|
||||||
# 배포 완료
|
# 배포 완료
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue