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:
268
frontend/src/components/ErrorBoundary.jsx
Normal file
268
frontend/src/components/ErrorBoundary.jsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import React from 'react';
|
||||
import errorLogger from '../utils/errorLogger';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트합니다.
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// 오류 정보를 상태에 저장
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
});
|
||||
|
||||
// 오류 로깅
|
||||
errorLogger.logError({
|
||||
type: 'react_error_boundary',
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
componentStack: errorInfo.componentStack,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
props: this.props.errorContext || {}
|
||||
});
|
||||
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleGoHome = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
handleReportError = () => {
|
||||
const errorReport = {
|
||||
error: this.state.error?.message,
|
||||
stack: this.state.error?.stack,
|
||||
componentStack: this.state.errorInfo?.componentStack,
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent
|
||||
};
|
||||
|
||||
// 오류 보고서를 클립보드에 복사
|
||||
navigator.clipboard.writeText(JSON.stringify(errorReport, null, 2))
|
||||
.then(() => {
|
||||
alert('오류 정보가 클립보드에 복사되었습니다.');
|
||||
})
|
||||
.catch(() => {
|
||||
// 클립보드 복사 실패 시 텍스트 영역에 표시
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = JSON.stringify(errorReport, null, 2);
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
alert('오류 정보가 클립보드에 복사되었습니다.');
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: '20px'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '600px',
|
||||
width: '100%',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
padding: '40px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '48px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
😵
|
||||
</div>
|
||||
|
||||
<h1 style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: '600',
|
||||
color: '#dc3545',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
앗! 문제가 발생했습니다
|
||||
</h1>
|
||||
|
||||
<p style={{
|
||||
fontSize: '16px',
|
||||
color: '#6c757d',
|
||||
marginBottom: '30px',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
예상치 못한 오류가 발생했습니다. <br />
|
||||
이 문제는 자동으로 개발팀에 보고되었습니다.
|
||||
</p>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: '30px'
|
||||
}}>
|
||||
<button
|
||||
onClick={this.handleReload}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
onMouseOver={(e) => e.target.style.backgroundColor = '#0056b3'}
|
||||
onMouseOut={(e) => e.target.style.backgroundColor = '#007bff'}
|
||||
>
|
||||
🔄 페이지 새로고침
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={this.handleGoHome}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#28a745',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
onMouseOver={(e) => e.target.style.backgroundColor = '#1e7e34'}
|
||||
onMouseOut={(e) => e.target.style.backgroundColor = '#28a745'}
|
||||
>
|
||||
🏠 홈으로 이동
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={this.handleReportError}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
}}
|
||||
onMouseOver={(e) => e.target.style.backgroundColor = '#545b62'}
|
||||
onMouseOut={(e) => e.target.style.backgroundColor = '#6c757d'}
|
||||
>
|
||||
📋 오류 정보 복사
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 개발 환경에서만 상세 오류 정보 표시 */}
|
||||
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||
<details style={{
|
||||
textAlign: 'left',
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: '16px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '20px',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
<summary style={{
|
||||
cursor: 'pointer',
|
||||
fontWeight: '600',
|
||||
marginBottom: '8px',
|
||||
color: '#495057'
|
||||
}}>
|
||||
개발자 정보 (클릭하여 펼치기)
|
||||
</summary>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<strong>오류 메시지:</strong>
|
||||
<pre style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
margin: '4px 0',
|
||||
color: '#dc3545'
|
||||
}}>
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<strong>스택 트레이스:</strong>
|
||||
<pre style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
margin: '4px 0',
|
||||
fontSize: '11px',
|
||||
color: '#6c757d'
|
||||
}}>
|
||||
{this.state.error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{this.state.errorInfo?.componentStack && (
|
||||
<div>
|
||||
<strong>컴포넌트 스택:</strong>
|
||||
<pre style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
margin: '4px 0',
|
||||
fontSize: '11px',
|
||||
color: '#6c757d'
|
||||
}}>
|
||||
{this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</details>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
color: '#1565c0'
|
||||
}}>
|
||||
💡 <strong>도움말:</strong> 문제가 계속 발생하면 페이지를 새로고침하거나
|
||||
브라우저 캐시를 삭제해보세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
Reference in New Issue
Block a user