diff --git a/backend-node/src/services/mailSentHistoryService.ts b/backend-node/src/services/mailSentHistoryService.ts index b69e5c77..61fd6f89 100644 --- a/backend-node/src/services/mailSentHistoryService.ts +++ b/backend-node/src/services/mailSentHistoryService.ts @@ -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): Promise { + async saveSentMail( + data: Omit + ): Promise { 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 { + async getSentMailList( + query: SentMailListQuery + ): Promise { 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(); - diff --git a/scripts/prod/deploy.sh b/scripts/prod/deploy.sh index 409ed04b..428dd459 100755 --- a/scripts/prod/deploy.sh +++ b/scripts/prod/deploy.sh @@ -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 # 배포 완료