feat: 완전한 사용자 관리 및 로그 모니터링 시스템 구현
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 시스템 관리자/관리자 권한별 대시보드 기능 추가 - 사용자 관리 페이지: 계정 생성, 역할 변경, 사용자 삭제 - 시스템 로그 페이지: 로그인 로그, 시스템 오류 로그 조회 - 로그 모니터링 대시보드: 실시간 통계, 최근 활동, 오류 모니터링 - 프론트엔드 ErrorBoundary 및 오류 로깅 시스템 통합 - 계정 설정 페이지: 프로필 업데이트, 비밀번호 변경 - 3단계 권한 시스템 (system/admin/user) 완전 구현 - 시스템 관리자 계정 생성 기능 (hyungi/000000) - 로그인 페이지 테스트 계정 안내 제거 - API 오류 수정: CORS, 이메일 검증, User 모델 import 등
This commit is contained in:
323
frontend/src/utils/errorLogger.js
Normal file
323
frontend/src/utils/errorLogger.js
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 프론트엔드 오류 로깅 시스템
|
||||
* 테스트 및 디버깅을 위한 오류 수집 및 전송
|
||||
*/
|
||||
|
||||
import api from '../api';
|
||||
|
||||
class ErrorLogger {
|
||||
constructor() {
|
||||
this.isEnabled = process.env.NODE_ENV === 'development' || process.env.REACT_APP_ERROR_LOGGING === 'true';
|
||||
this.maxRetries = 3;
|
||||
this.retryDelay = 1000; // 1초
|
||||
this.errorQueue = [];
|
||||
this.isProcessing = false;
|
||||
|
||||
// 전역 오류 핸들러 설정
|
||||
this.setupGlobalErrorHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 전역 오류 핸들러 설정
|
||||
*/
|
||||
setupGlobalErrorHandlers() {
|
||||
// JavaScript 오류 캐치
|
||||
window.addEventListener('error', (event) => {
|
||||
this.logError({
|
||||
type: 'javascript_error',
|
||||
message: event.message,
|
||||
filename: event.filename,
|
||||
lineno: event.lineno,
|
||||
colno: event.colno,
|
||||
stack: event.error?.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent
|
||||
});
|
||||
});
|
||||
|
||||
// Promise rejection 캐치
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
this.logError({
|
||||
type: 'promise_rejection',
|
||||
message: event.reason?.message || 'Unhandled Promise Rejection',
|
||||
stack: event.reason?.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent
|
||||
});
|
||||
});
|
||||
|
||||
// React Error Boundary에서 사용할 수 있도록 전역에 등록
|
||||
window.errorLogger = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 로깅
|
||||
* @param {Object} errorInfo - 오류 정보
|
||||
*/
|
||||
async logError(errorInfo) {
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
const errorData = {
|
||||
...errorInfo,
|
||||
sessionId: this.getSessionId(),
|
||||
userId: this.getUserId(),
|
||||
timestamp: errorInfo.timestamp || new Date().toISOString(),
|
||||
level: errorInfo.level || 'error'
|
||||
};
|
||||
|
||||
// 콘솔에도 출력 (개발 환경)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('🚨 Frontend Error:', errorData);
|
||||
}
|
||||
|
||||
// 로컬 스토리지에 임시 저장
|
||||
this.saveToLocalStorage(errorData);
|
||||
|
||||
// 서버로 전송 (큐에 추가)
|
||||
this.errorQueue.push(errorData);
|
||||
this.processErrorQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* API 오류 로깅
|
||||
* @param {Object} error - API 오류 객체
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
* @param {Object} requestData - 요청 데이터
|
||||
*/
|
||||
logApiError(error, endpoint, requestData = null) {
|
||||
const errorInfo = {
|
||||
type: 'api_error',
|
||||
message: error.message || 'API Error',
|
||||
endpoint: endpoint,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
responseData: error.response?.data,
|
||||
requestData: requestData,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href
|
||||
};
|
||||
|
||||
this.logError(errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 액션 오류 로깅
|
||||
* @param {string} action - 사용자 액션
|
||||
* @param {Object} error - 오류 객체
|
||||
* @param {Object} context - 추가 컨텍스트
|
||||
*/
|
||||
logUserActionError(action, error, context = {}) {
|
||||
const errorInfo = {
|
||||
type: 'user_action_error',
|
||||
action: action,
|
||||
message: error.message || 'User Action Error',
|
||||
stack: error.stack,
|
||||
context: context,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href
|
||||
};
|
||||
|
||||
this.logError(errorInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 성능 이슈 로깅
|
||||
* @param {string} operation - 작업명
|
||||
* @param {number} duration - 소요 시간 (ms)
|
||||
* @param {Object} details - 추가 세부사항
|
||||
*/
|
||||
logPerformanceIssue(operation, duration, details = {}) {
|
||||
if (duration > 5000) { // 5초 이상 걸린 작업만 로깅
|
||||
const performanceInfo = {
|
||||
type: 'performance_issue',
|
||||
operation: operation,
|
||||
duration: duration,
|
||||
details: details,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
level: 'warning'
|
||||
};
|
||||
|
||||
this.logError(performanceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 큐 처리
|
||||
*/
|
||||
async processErrorQueue() {
|
||||
if (this.isProcessing || this.errorQueue.length === 0) return;
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
while (this.errorQueue.length > 0) {
|
||||
const errorData = this.errorQueue.shift();
|
||||
|
||||
try {
|
||||
await this.sendErrorToServer(errorData);
|
||||
} catch (sendError) {
|
||||
console.error('Failed to send error to server:', sendError);
|
||||
// 실패한 오류는 다시 큐에 추가 (최대 재시도 횟수 확인)
|
||||
if (!errorData.retryCount) errorData.retryCount = 0;
|
||||
if (errorData.retryCount < this.maxRetries) {
|
||||
errorData.retryCount++;
|
||||
this.errorQueue.push(errorData);
|
||||
await this.delay(this.retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버로 오류 전송
|
||||
* @param {Object} errorData - 오류 데이터
|
||||
*/
|
||||
async sendErrorToServer(errorData) {
|
||||
try {
|
||||
await api.post('/logs/frontend-error', errorData);
|
||||
} catch (error) {
|
||||
// 로깅 API가 없는 경우 무시
|
||||
if (error.response?.status === 404) {
|
||||
console.warn('Error logging endpoint not available');
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로컬 스토리지에 오류 저장
|
||||
* @param {Object} errorData - 오류 데이터
|
||||
*/
|
||||
saveToLocalStorage(errorData) {
|
||||
try {
|
||||
const errors = JSON.parse(localStorage.getItem('frontend_errors') || '[]');
|
||||
errors.push(errorData);
|
||||
|
||||
// 최대 100개까지만 저장
|
||||
if (errors.length > 100) {
|
||||
errors.splice(0, errors.length - 100);
|
||||
}
|
||||
|
||||
localStorage.setItem('frontend_errors', JSON.stringify(errors));
|
||||
} catch (e) {
|
||||
console.error('Failed to save error to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로컬 스토리지에서 오류 목록 조회
|
||||
* @returns {Array} 오류 목록
|
||||
*/
|
||||
getLocalErrors() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('frontend_errors') || '[]');
|
||||
} catch (e) {
|
||||
console.error('Failed to get errors from localStorage:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로컬 스토리지 오류 삭제
|
||||
*/
|
||||
clearLocalErrors() {
|
||||
try {
|
||||
localStorage.removeItem('frontend_errors');
|
||||
} catch (e) {
|
||||
console.error('Failed to clear errors from localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 ID 조회
|
||||
* @returns {string} 세션 ID
|
||||
*/
|
||||
getSessionId() {
|
||||
let sessionId = sessionStorage.getItem('error_session_id');
|
||||
if (!sessionId) {
|
||||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
sessionStorage.setItem('error_session_id', sessionId);
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 ID 조회
|
||||
* @returns {string|null} 사용자 ID
|
||||
*/
|
||||
getUserId() {
|
||||
try {
|
||||
const userData = JSON.parse(localStorage.getItem('user_data') || '{}');
|
||||
return userData.user_id || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지연 함수
|
||||
* @param {number} ms - 지연 시간 (밀리초)
|
||||
*/
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 로깅 활성화/비활성화
|
||||
* @param {boolean} enabled - 활성화 여부
|
||||
*/
|
||||
setEnabled(enabled) {
|
||||
this.isEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 오류 보고
|
||||
* @param {string} message - 오류 메시지
|
||||
* @param {Object} details - 추가 세부사항
|
||||
*/
|
||||
reportError(message, details = {}) {
|
||||
this.logError({
|
||||
type: 'manual_report',
|
||||
message: message,
|
||||
details: details,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
level: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 경고 로깅
|
||||
* @param {string} message - 경고 메시지
|
||||
* @param {Object} details - 추가 세부사항
|
||||
*/
|
||||
reportWarning(message, details = {}) {
|
||||
this.logError({
|
||||
type: 'warning',
|
||||
message: message,
|
||||
details: details,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
level: 'warning'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글톤 인스턴스 생성 및 내보내기
|
||||
const errorLogger = new ErrorLogger();
|
||||
|
||||
export default errorLogger;
|
||||
|
||||
// 편의 함수들 내보내기
|
||||
export const logError = (error, context) => errorLogger.logError({ ...error, context });
|
||||
export const logApiError = (error, endpoint, requestData) => errorLogger.logApiError(error, endpoint, requestData);
|
||||
export const logUserActionError = (action, error, context) => errorLogger.logUserActionError(action, error, context);
|
||||
export const logPerformanceIssue = (operation, duration, details) => errorLogger.logPerformanceIssue(operation, duration, details);
|
||||
export const reportError = (message, details) => errorLogger.reportError(message, details);
|
||||
export const reportWarning = (message, details) => errorLogger.reportWarning(message, details);
|
||||
Reference in New Issue
Block a user