feat: 완전한 사용자 관리 및 로그 모니터링 시스템 구현
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:
Hyungi Ahn
2025-09-09 12:58:14 +09:00
parent 881fc13580
commit 529777aa14
16 changed files with 4519 additions and 450 deletions

View 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);