Merge remote-tracking branch 'upstream/main'

This commit is contained in:
dohyeons
2025-10-13 16:04:43 +09:00
2 changed files with 160 additions and 60 deletions

View File

@@ -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();

View File

@@ -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
# 배포 완료