Files
TK-BOM-Project/frontend/src/pages/LogMonitoringPage.jsx
Hyungi Ahn 50570e4624
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 사용자 요구사항 기능 완전 구현 및 전체 카테고리 추가
- 사용자 요구사항 저장/로드/엑셀 내보내기 기능 완전 구현
- 백엔드 API 수정: Request Body 방식으로 변경
- 데이터베이스 스키마: material_id 컬럼 추가
- 프론트엔드 상태 관리 개선: 저장 후 자동 리로드
- 입력 필드 연결 문제 해결: 누락된 onChange 핸들러 추가
- NewMaterialsPage에 '전체' 카테고리 버튼 추가 (기본 선택)
- Docker 환경 개선: 프론트엔드 볼륨 마운트 및 포트 수정
- UI 개선: 벌레 이모지 제거, 디버그 코드 정리
2025-09-30 08:55:20 +09:00

529 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import api from '../api';
import { reportError, logUserActionError } from '../utils/errorLogger';
const LogMonitoringPage = ({ onNavigate, user }) => {
const [activeTab, setActiveTab] = useState('login-logs'); // 'login-logs', 'activity-logs', 'system-logs'
const [stats, setStats] = useState({
totalUsers: 0,
activeUsers: 0,
todayLogins: 0,
failedLogins: 0,
recentErrors: 0
});
const [recentActivity, setRecentActivity] = useState([]);
const [activityLogs, setActivityLogs] = useState([]);
const [frontendErrors, setFrontendErrors] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [message, setMessage] = useState({ type: '', text: '' });
useEffect(() => {
loadDashboardData();
// 30초마다 자동 새로고침
const interval = setInterval(loadDashboardData, 30000);
return () => clearInterval(interval);
}, []);
const loadActivityLogs = async () => {
try {
const response = await api.get('/auth/logs/system?limit=50');
if (response.data.success) {
setActivityLogs(response.data.logs);
}
} catch (error) {
console.error('활동 로그 로딩 실패:', error);
}
};
const loadDashboardData = async () => {
try {
setIsLoading(true);
// 활동 로그도 함께 로드
if (activeTab === 'activity-logs') {
await loadActivityLogs();
}
// 병렬로 데이터 로드
const [usersResponse, loginLogsResponse] = await Promise.all([
api.get('/auth/users'),
api.get('/auth/logs/login', { params: { limit: 20 } })
]);
// 사용자 통계
if (usersResponse.data.success) {
const users = usersResponse.data.users;
setStats(prev => ({
...prev,
totalUsers: users.length,
activeUsers: users.filter(u => u.is_active).length
}));
}
// 로그인 로그 통계
if (loginLogsResponse.data.success) {
const logs = loginLogsResponse.data.logs;
const today = new Date().toDateString();
const todayLogins = logs.filter(log =>
new Date(log.login_time).toDateString() === today &&
log.login_status === 'success'
).length;
const failedLogins = logs.filter(log =>
new Date(log.login_time).toDateString() === today &&
log.login_status === 'failed'
).length;
setStats(prev => ({
...prev,
todayLogins,
failedLogins
}));
setRecentActivity(logs.slice(0, 10));
}
// 프론트엔드 오류 로그 (로컬 스토리지에서)
const localErrors = JSON.parse(localStorage.getItem('frontend_errors') || '[]');
const recentErrors = localErrors.filter(error => {
const errorDate = new Date(error.timestamp);
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
return errorDate > oneDayAgo;
});
setFrontendErrors(recentErrors.slice(0, 10));
setStats(prev => ({
...prev,
recentErrors: recentErrors.length
}));
} catch (err) {
console.error('Load dashboard data error:', err);
setMessage({ type: 'error', text: '대시보드 데이터 로드 중 오류가 발생했습니다' });
logUserActionError('load_dashboard_data', err, { userId: user?.user_id });
} finally {
setIsLoading(false);
}
};
const clearFrontendErrors = () => {
localStorage.removeItem('frontend_errors');
setFrontendErrors([]);
setStats(prev => ({ ...prev, recentErrors: 0 }));
setMessage({ type: 'success', text: '프론트엔드 오류 로그가 삭제되었습니다' });
};
const formatDateTime = (dateString) => {
try {
return new Date(dateString).toLocaleString('ko-KR', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateString;
}
};
const getActivityIcon = (status) => {
return status === 'success' ? '✅' : '❌';
};
const getErrorTypeIcon = (type) => {
const icons = {
'javascript_error': '❌',
'api_error': '🌐',
'user_action_error': '👤',
'promise_rejection': '⚠️',
'react_error_boundary': '⚛️'
};
return icons[type] || '❓';
};
return (
<div style={{ minHeight: '100vh', background: '#f8f9fa' }}>
{/* 헤더 */}
<div style={{
background: 'white',
borderBottom: '1px solid #e9ecef',
padding: '16px 32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ 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>
</div>
</div>
{/* 탭 네비게이션 */}
<div style={{ background: 'white', borderBottom: '1px solid #e9ecef', padding: '0 32px' }}>
<div style={{ display: 'flex', gap: '0' }}>
<button
onClick={() => setActiveTab('login-logs')}
style={{
padding: '12px 24px',
border: 'none',
background: activeTab === 'login-logs' ? '#4299e1' : 'transparent',
color: activeTab === 'login-logs' ? 'white' : '#4a5568',
borderRadius: '8px 8px 0 0',
cursor: 'pointer',
fontWeight: '600',
fontSize: '14px',
borderBottom: activeTab === 'login-logs' ? '2px solid #4299e1' : '2px solid transparent'
}}
>
🔐 로그인 로그
</button>
<button
onClick={() => setActiveTab('activity-logs')}
style={{
padding: '12px 24px',
border: 'none',
background: activeTab === 'activity-logs' ? '#4299e1' : 'transparent',
color: activeTab === 'activity-logs' ? 'white' : '#4a5568',
borderRadius: '8px 8px 0 0',
cursor: 'pointer',
fontWeight: '600',
fontSize: '14px',
borderBottom: activeTab === 'activity-logs' ? '2px solid #4299e1' : '2px solid transparent'
}}
>
📊 활동 로그
</button>
<button
onClick={() => setActiveTab('system-logs')}
style={{
padding: '12px 24px',
border: 'none',
background: activeTab === 'system-logs' ? '#4299e1' : 'transparent',
color: activeTab === 'system-logs' ? 'white' : '#4a5568',
borderRadius: '8px 8px 0 0',
cursor: 'pointer',
fontWeight: '600',
fontSize: '14px',
borderBottom: activeTab === 'system-logs' ? '2px solid #4299e1' : '2px solid transparent'
}}
>
🖥 시스템 로그
</button>
</div>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={loadDashboardData}
disabled={isLoading}
style={{
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '8px 16px',
fontSize: '14px',
fontWeight: '600',
cursor: isLoading ? 'not-allowed' : 'pointer',
opacity: isLoading ? 0.6 : 1
}}
>
🔄 새로고침
</button>
{frontendErrors.length > 0 && (
<button
onClick={clearFrontendErrors}
style={{
background: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '8px 16px',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer'
}}
>
🗑 오류 로그 삭제
</button>
)}
</div>
</div>
<div style={{ padding: '32px', maxWidth: '1400px', margin: '0 auto' }}>
{/* 메시지 표시 */}
{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={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
gap: '20px',
marginBottom: '32px'
}}>
<div style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
border: '1px solid #e9ecef'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<div style={{ fontSize: '24px' }}>👥</div>
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#495057', margin: 0 }}>
전체 사용자
</h3>
</div>
<div style={{ fontSize: '32px', fontWeight: '700', color: '#2d3748' }}>
{stats.totalUsers}
</div>
<div style={{ fontSize: '14px', color: '#28a745' }}>
활성: {stats.activeUsers}
</div>
</div>
<div style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
border: '1px solid #e9ecef'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<div style={{ fontSize: '24px' }}></div>
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#495057', margin: 0 }}>
오늘 로그인
</h3>
</div>
<div style={{ fontSize: '32px', fontWeight: '700', color: '#28a745' }}>
{stats.todayLogins}
</div>
<div style={{ fontSize: '14px', color: '#6c757d' }}>
성공한 로그인
</div>
</div>
<div style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
border: '1px solid #e9ecef'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<div style={{ fontSize: '24px' }}></div>
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#495057', margin: 0 }}>
로그인 실패
</h3>
</div>
<div style={{ fontSize: '32px', fontWeight: '700', color: '#dc3545' }}>
{stats.failedLogins}
</div>
<div style={{ fontSize: '14px', color: '#6c757d' }}>
오늘 실패 횟수
</div>
</div>
<div style={{
background: 'white',
borderRadius: '12px',
padding: '24px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
border: '1px solid #e9ecef'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<div style={{ fontSize: '24px' }}></div>
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#495057', margin: 0 }}>
최근 오류
</h3>
</div>
<div style={{ fontSize: '32px', fontWeight: '700', color: '#ffc107' }}>
{stats.recentErrors}
</div>
<div style={{ fontSize: '14px', color: '#6c757d' }}>
24시간
</div>
</div>
</div>
{/* 콘텐츠 그리드 */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '24px'
}}>
{/* 최근 활동 */}
<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 }}>
🔐 최근 로그인 활동
</h2>
</div>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
{isLoading ? (
<div style={{ padding: '40px', textAlign: 'center' }}>
<div style={{ fontSize: '16px', color: '#6c757d' }}>로딩 ...</div>
</div>
) : recentActivity.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center' }}>
<div style={{ fontSize: '16px', color: '#6c757d' }}>최근 활동이 없습니다</div>
</div>
) : (
recentActivity.map((activity, index) => (
<div key={index} style={{
padding: '16px 24px',
borderBottom: '1px solid #f1f3f4',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<div style={{ fontSize: '20px' }}>
{getActivityIcon(activity.login_status)}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#2d3748' }}>
{activity.name}
</div>
<div style={{ fontSize: '12px', color: '#6c757d' }}>
@{activity.username} {activity.ip_address}
</div>
{activity.failure_reason && (
<div style={{ fontSize: '12px', color: '#dc3545', marginTop: '2px' }}>
{activity.failure_reason}
</div>
)}
</div>
<div style={{ fontSize: '12px', color: '#6c757d' }}>
{formatDateTime(activity.login_time)}
</div>
</div>
))
)}
</div>
</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 }}>
프론트엔드 오류
</h2>
</div>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
{frontendErrors.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center' }}>
<div style={{ fontSize: '16px', color: '#6c757d' }}>최근 오류가 없습니다</div>
</div>
) : (
frontendErrors.map((error, index) => (
<div key={index} style={{
padding: '16px 24px',
borderBottom: '1px solid #f1f3f4',
display: 'flex',
alignItems: 'flex-start',
gap: '12px'
}}>
<div style={{ fontSize: '16px', marginTop: '2px' }}>
{getErrorTypeIcon(error.type)}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#dc3545' }}>
{error.type?.replace('_', ' ').toUpperCase() || 'ERROR'}
</div>
<div style={{
fontSize: '13px',
color: '#495057',
marginTop: '4px',
wordBreak: 'break-word',
lineHeight: '1.4'
}}>
{error.message?.substring(0, 100)}
{error.message?.length > 100 && '...'}
</div>
<div style={{ fontSize: '12px', color: '#6c757d', marginTop: '4px' }}>
{error.url && (
<span>{new URL(error.url).pathname} </span>
)}
{formatDateTime(error.timestamp)}
</div>
</div>
</div>
))
)}
</div>
</div>
</div>
{/* 자동 새로고침 안내 */}
<div style={{
marginTop: '24px',
padding: '16px',
backgroundColor: '#e3f2fd',
border: '1px solid #bbdefb',
borderRadius: '8px',
textAlign: 'center'
}}>
<p style={{ fontSize: '14px', color: '#1565c0', margin: 0 }}>
📊 페이지는 30초마다 자동으로 새로고침됩니다
</p>
</div>
</div>
</div>
);
};
export default LogMonitoringPage;