/** * 로깅 유틸리티 * * 애플리케이션 전체에서 사용하는 통합 로거 * * @author TK-FB-Project * @since 2025-12-11 */ const fs = require('fs'); const path = require('path'); /** * 로그 레벨 정의 */ const LogLevel = { ERROR: 'ERROR', WARN: 'WARN', INFO: 'INFO', DEBUG: 'DEBUG' }; /** * 로그 레벨별 이모지 */ const LogEmoji = { ERROR: '❌', WARN: '⚠️', INFO: 'ℹ️', DEBUG: '🔍' }; /** * 로그 레벨별 색상 (콘솔) */ const LogColor = { ERROR: '\x1b[31m', // Red WARN: '\x1b[33m', // Yellow INFO: '\x1b[36m', // Cyan DEBUG: '\x1b[90m', // Gray RESET: '\x1b[0m' }; class Logger { constructor() { // process.cwd() = /usr/src/app (컨테이너 WORKDIR) // __dirname 대신 사용하여 shared/ 위치와 무관하게 서비스의 logs/ 디렉토리에 기록 this.logDir = path.join(process.cwd(), 'logs'); this.logFile = path.join(this.logDir, 'app.log'); this.errorFile = path.join(this.logDir, 'error.log'); this.ensureLogDirectory(); } /** * 로그 디렉토리 생성 */ ensureLogDirectory() { if (!fs.existsSync(this.logDir)) { fs.mkdirSync(this.logDir, { recursive: true }); } } /** * 타임스탬프 생성 */ getTimestamp() { return new Date().toISOString(); } /** * 로그 포맷팅 */ formatLog(level, message, context = {}) { const timestamp = this.getTimestamp(); const emoji = LogEmoji[level] || ''; const contextStr = Object.keys(context).length > 0 ? `\n Context: ${JSON.stringify(context, null, 2)}` : ''; return `[${timestamp}] [${level}] ${emoji} ${message}${contextStr}`; } /** * 콘솔에 컬러 로그 출력 */ logToConsole(level, message, context = {}) { const color = LogColor[level] || LogColor.RESET; const formattedLog = this.formatLog(level, message, context); if (level === LogLevel.ERROR) { console.error(`${color}${formattedLog}${LogColor.RESET}`); } else if (level === LogLevel.WARN) { console.warn(`${color}${formattedLog}${LogColor.RESET}`); } else { console.log(`${color}${formattedLog}${LogColor.RESET}`); } } /** * 파일에 로그 기록 */ logToFile(level, message, context = {}) { const formattedLog = this.formatLog(level, message, context); const logEntry = `${formattedLog}\n`; try { // 모든 로그를 app.log에 기록 fs.appendFileSync(this.logFile, logEntry, 'utf8'); // 에러는 error.log에도 기록 if (level === LogLevel.ERROR) { fs.appendFileSync(this.errorFile, logEntry, 'utf8'); } } catch (err) { console.error('로그 파일 기록 실패:', err); } } /** * 로그 기록 메인 함수 */ log(level, message, context = {}) { // 개발 환경에서는 콘솔에 출력 if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV !== 'production') { this.logToConsole(level, message, context); } // 프로덕션에서는 파일에만 기록 if (process.env.NODE_ENV === 'production') { this.logToFile(level, message, context); } } /** * 에러 로그 */ error(message, context = {}) { this.log(LogLevel.ERROR, message, context); } /** * 경고 로그 */ warn(message, context = {}) { this.log(LogLevel.WARN, message, context); } /** * 정보 로그 */ info(message, context = {}) { this.log(LogLevel.INFO, message, context); } /** * 디버그 로그 */ debug(message, context = {}) { // DEBUG 로그는 개발 환경에서만 출력 if (process.env.NODE_ENV === 'development') { this.log(LogLevel.DEBUG, message, context); } } /** * HTTP 요청 로그 */ http(method, url, statusCode, duration, user = 'anonymous') { const level = statusCode >= 400 ? LogLevel.ERROR : LogLevel.INFO; const message = `${method} ${url} - ${statusCode} (${duration}ms)`; const context = { method, url, statusCode, duration, user }; this.log(level, message, context); } /** * 데이터베이스 쿼리 로그 */ query(sql, params = [], duration = 0) { if (process.env.NODE_ENV === 'development') { this.debug('DB Query', { sql, params, duration: `${duration}ms` }); } } } // 싱글톤 인스턴스 생성 및 내보내기 const logger = new Logger(); module.exports = logger;