Phase 1: notifyHelper.js → shared/utils/ (4개 서비스 중복 제거) Phase 2: auth.js → shared/middleware/ (system1/system2 통합) Phase 3: errors.js + logger.js → shared/utils/ (system1/system2 통합) Phase 4: DB pool → shared/config/database.js (Group B 4개 서비스 통합) - Docker 빌드 컨텍스트를 루트로 변경 (6개 API 서비스) - 기존 파일은 re-export 패턴으로 consumer 변경 0개 유지 - .dockerignore 추가로 빌드 최적화 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
4.4 KiB
JavaScript
200 lines
4.4 KiB
JavaScript
/**
|
||
* 로깅 유틸리티
|
||
*
|
||
* 애플리케이션 전체에서 사용하는 통합 로거
|
||
*
|
||
* @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;
|