Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 시스템 관리자/관리자 권한별 대시보드 기능 추가 - 사용자 관리 페이지: 계정 생성, 역할 변경, 사용자 삭제 - 시스템 로그 페이지: 로그인 로그, 시스템 오류 로그 조회 - 로그 모니터링 대시보드: 실시간 통계, 최근 활동, 오류 모니터링 - 프론트엔드 ErrorBoundary 및 오류 로깅 시스템 통합 - 계정 설정 페이지: 프로필 업데이트, 비밀번호 변경 - 3단계 권한 시스템 (system/admin/user) 완전 구현 - 시스템 관리자 계정 생성 기능 (hyungi/000000) - 로그인 페이지 테스트 계정 안내 제거 - API 오류 수정: CORS, 이메일 검증, User 모델 import 등
440 lines
21 KiB
JavaScript
440 lines
21 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import api from '../api';
|
||
import { reportError, logUserActionError } from '../utils/errorLogger';
|
||
|
||
const SystemLogsPage = ({ onNavigate, user }) => {
|
||
const [activeTab, setActiveTab] = useState('login');
|
||
const [loginLogs, setLoginLogs] = useState([]);
|
||
const [systemLogs, setSystemLogs] = useState([]);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [message, setMessage] = useState({ type: '', text: '' });
|
||
|
||
// 필터 상태
|
||
const [filters, setFilters] = useState({
|
||
status: '',
|
||
level: '',
|
||
userId: '',
|
||
limit: 50
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (activeTab === 'login') {
|
||
loadLoginLogs();
|
||
} else {
|
||
loadSystemLogs();
|
||
}
|
||
}, [activeTab, filters]);
|
||
|
||
const loadLoginLogs = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
const params = {
|
||
limit: filters.limit,
|
||
...(filters.status && { status: filters.status }),
|
||
...(filters.userId && { user_id: filters.userId })
|
||
};
|
||
|
||
const response = await api.get('/auth/logs/login', { params });
|
||
|
||
if (response.data.success) {
|
||
setLoginLogs(response.data.logs);
|
||
} else {
|
||
setMessage({ type: 'error', text: '로그인 로그를 불러올 수 없습니다' });
|
||
}
|
||
} catch (err) {
|
||
console.error('Load login logs error:', err);
|
||
setMessage({ type: 'error', text: '로그인 로그 조회 중 오류가 발생했습니다' });
|
||
logUserActionError('load_login_logs', err, { userId: user?.user_id });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const loadSystemLogs = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
const params = {
|
||
limit: filters.limit,
|
||
...(filters.level && { level: filters.level })
|
||
};
|
||
|
||
const response = await api.get('/auth/logs/system', { params });
|
||
|
||
if (response.data.success) {
|
||
setSystemLogs(response.data.logs);
|
||
} else {
|
||
setMessage({ type: 'error', text: '시스템 로그를 불러올 수 없습니다' });
|
||
}
|
||
} catch (err) {
|
||
console.error('Load system logs error:', err);
|
||
setMessage({ type: 'error', text: '시스템 로그 조회 중 오류가 발생했습니다' });
|
||
logUserActionError('load_system_logs', err, { userId: user?.user_id });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const getStatusBadge = (status) => {
|
||
const colors = {
|
||
'success': { bg: '#d1edff', color: '#0c5460' },
|
||
'failed': { bg: '#f8d7da', color: '#721c24' }
|
||
};
|
||
const color = colors[status] || colors.failed;
|
||
|
||
return (
|
||
<span style={{
|
||
background: color.bg,
|
||
color: color.color,
|
||
padding: '4px 8px',
|
||
borderRadius: '12px',
|
||
fontSize: '12px',
|
||
fontWeight: '600'
|
||
}}>
|
||
{status === 'success' ? '성공' : '실패'}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const getLevelBadge = (level) => {
|
||
const colors = {
|
||
'ERROR': { bg: '#f8d7da', color: '#721c24' },
|
||
'WARNING': { bg: '#fff3cd', color: '#856404' },
|
||
'INFO': { bg: '#d1ecf1', color: '#0c5460' },
|
||
'DEBUG': { bg: '#e2e3e5', color: '#383d41' }
|
||
};
|
||
const color = colors[level] || colors.INFO;
|
||
|
||
return (
|
||
<span style={{
|
||
background: color.bg,
|
||
color: color.color,
|
||
padding: '4px 8px',
|
||
borderRadius: '12px',
|
||
fontSize: '12px',
|
||
fontWeight: '600'
|
||
}}>
|
||
{level}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const formatDateTime = (dateString) => {
|
||
try {
|
||
return new Date(dateString).toLocaleString('ko-KR', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
} catch {
|
||
return dateString;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div style={{ minHeight: '100vh', background: '#f8f9fa' }}>
|
||
{/* 헤더 */}
|
||
<div style={{
|
||
background: 'white',
|
||
borderBottom: '1px solid #e9ecef',
|
||
padding: '16px 32px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '16px'
|
||
}}>
|
||
<button
|
||
onClick={() => onNavigate('dashboard')}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
color: '#28a745',
|
||
fontSize: '20px',
|
||
cursor: 'pointer',
|
||
padding: '4px',
|
||
borderRadius: '4px',
|
||
transition: 'background-color 0.2s'
|
||
}}
|
||
onMouseEnter={(e) => e.target.style.background = '#f8f9fa'}
|
||
onMouseLeave={(e) => e.target.style.background = 'none'}
|
||
title="대시보드로 돌아가기"
|
||
>
|
||
←
|
||
</button>
|
||
<div>
|
||
<h1 style={{ fontSize: '24px', fontWeight: '700', color: '#2d3748', margin: 0 }}>
|
||
📊 시스템 로그
|
||
</h1>
|
||
<p style={{ color: '#6c757d', fontSize: '14px', margin: '4px 0 0 0' }}>
|
||
로그인 기록과 시스템 오류 로그를 조회하세요
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ padding: '32px', maxWidth: '1400px', margin: '0 auto' }}>
|
||
{/* 탭 메뉴 */}
|
||
<div style={{
|
||
display: 'flex',
|
||
borderBottom: '2px solid #e9ecef',
|
||
marginBottom: '24px'
|
||
}}>
|
||
<button
|
||
onClick={() => setActiveTab('login')}
|
||
style={{
|
||
padding: '12px 24px',
|
||
background: 'none',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'login' ? '2px solid #007bff' : '2px solid transparent',
|
||
color: activeTab === 'login' ? '#007bff' : '#6c757d',
|
||
fontSize: '16px',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
>
|
||
🔐 로그인 로그
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('system')}
|
||
style={{
|
||
padding: '12px 24px',
|
||
background: 'none',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'system' ? '2px solid #007bff' : '2px solid transparent',
|
||
color: activeTab === 'system' ? '#007bff' : '#6c757d',
|
||
fontSize: '16px',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
>
|
||
🖥️ 시스템 로그
|
||
</button>
|
||
</div>
|
||
|
||
{/* 필터 */}
|
||
<div style={{
|
||
background: 'white',
|
||
borderRadius: '8px',
|
||
padding: '16px',
|
||
marginBottom: '24px',
|
||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
||
}}>
|
||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||
{activeTab === 'login' && (
|
||
<div>
|
||
<label style={{ fontSize: '14px', fontWeight: '600', color: '#374151', marginRight: '8px' }}>
|
||
상태:
|
||
</label>
|
||
<select
|
||
value={filters.status}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value }))}
|
||
style={{
|
||
padding: '6px 12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
<option value="">전체</option>
|
||
<option value="success">성공</option>
|
||
<option value="failed">실패</option>
|
||
</select>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'system' && (
|
||
<div>
|
||
<label style={{ fontSize: '14px', fontWeight: '600', color: '#374151', marginRight: '8px' }}>
|
||
레벨:
|
||
</label>
|
||
<select
|
||
value={filters.level}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, level: e.target.value }))}
|
||
style={{
|
||
padding: '6px 12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
<option value="">전체</option>
|
||
<option value="ERROR">ERROR</option>
|
||
<option value="WARNING">WARNING</option>
|
||
<option value="INFO">INFO</option>
|
||
<option value="DEBUG">DEBUG</option>
|
||
</select>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<label style={{ fontSize: '14px', fontWeight: '600', color: '#374151', marginRight: '8px' }}>
|
||
개수:
|
||
</label>
|
||
<select
|
||
value={filters.limit}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, limit: parseInt(e.target.value) }))}
|
||
style={{
|
||
padding: '6px 12px',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
<option value={50}>50개</option>
|
||
<option value={100}>100개</option>
|
||
<option value={200}>200개</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => activeTab === 'login' ? loadLoginLogs() : loadSystemLogs()}
|
||
style={{
|
||
padding: '6px 16px',
|
||
background: '#007bff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
🔄 새로고침
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 메시지 표시 */}
|
||
{message.text && (
|
||
<div style={{
|
||
padding: '12px 16px',
|
||
borderRadius: '8px',
|
||
marginBottom: '24px',
|
||
backgroundColor: message.type === 'success' ? '#d1edff' : '#f8d7da',
|
||
border: `1px solid ${message.type === 'success' ? '#bee5eb' : '#f5c6cb'}`,
|
||
color: message.type === 'success' ? '#0c5460' : '#721c24'
|
||
}}>
|
||
{message.text}
|
||
</div>
|
||
)}
|
||
|
||
{/* 로그 테이블 */}
|
||
<div style={{
|
||
background: 'white',
|
||
borderRadius: '12px',
|
||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||
overflow: 'hidden'
|
||
}}>
|
||
<div style={{
|
||
padding: '20px 24px',
|
||
borderBottom: '1px solid #e9ecef',
|
||
background: '#f8f9fa'
|
||
}}>
|
||
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
|
||
{activeTab === 'login' ? '로그인 로그' : '시스템 로그'}
|
||
({activeTab === 'login' ? loginLogs.length : systemLogs.length}개)
|
||
</h2>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div style={{ padding: '40px', textAlign: 'center' }}>
|
||
<div style={{ fontSize: '16px', color: '#6c757d' }}>로딩 중...</div>
|
||
</div>
|
||
) : (
|
||
<div style={{ overflow: 'auto' }}>
|
||
{activeTab === 'login' ? (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ background: '#f8f9fa' }}>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>시간</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>사용자</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>상태</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>IP 주소</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>실패 사유</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{loginLogs.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={5} style={{ padding: '40px', textAlign: 'center', color: '#6c757d' }}>
|
||
로그인 로그가 없습니다
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
loginLogs.map((log, index) => (
|
||
<tr key={index} style={{ borderBottom: '1px solid #f1f3f4' }}>
|
||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#495057' }}>
|
||
{formatDateTime(log.login_time)}
|
||
</td>
|
||
<td style={{ padding: '12px 16px' }}>
|
||
<div style={{ fontSize: '14px', fontWeight: '600', color: '#2d3748' }}>
|
||
{log.name}
|
||
</div>
|
||
<div style={{ fontSize: '12px', color: '#6c757d' }}>
|
||
@{log.username}
|
||
</div>
|
||
</td>
|
||
<td style={{ padding: '12px 16px' }}>
|
||
{getStatusBadge(log.login_status)}
|
||
</td>
|
||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#495057' }}>
|
||
{log.ip_address || '-'}
|
||
</td>
|
||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#dc3545' }}>
|
||
{log.failure_reason || '-'}
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ background: '#f8f9fa' }}>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>시간</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>레벨</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>모듈</th>
|
||
<th style={{ padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#495057', borderBottom: '1px solid #dee2e6' }}>메시지</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{systemLogs.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={4} style={{ padding: '40px', textAlign: 'center', color: '#6c757d' }}>
|
||
시스템 로그가 없습니다
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
systemLogs.map((log, index) => (
|
||
<tr key={index} style={{ borderBottom: '1px solid #f1f3f4' }}>
|
||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#495057' }}>
|
||
{formatDateTime(log.timestamp)}
|
||
</td>
|
||
<td style={{ padding: '12px 16px' }}>
|
||
{getLevelBadge(log.level)}
|
||
</td>
|
||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#495057' }}>
|
||
{log.module || '-'}
|
||
</td>
|
||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#495057', maxWidth: '400px', wordBreak: 'break-word' }}>
|
||
{log.message}
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default SystemLogsPage;
|