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,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;