feat: 시스템 관리자 대시보드 개선

- 시스템 관리자 전용 웹페이지 구현 (system.html)
- 깔끔한 흰색 배경의 올드스쿨 스타일 적용
- 반응형 그리드 레이아웃으로 카드 배치 개선
- ES6 모듈 방식으로 JavaScript 구조 개선
- 이벤트 리스너 방식으로 버튼 클릭 처리 변경
- 시스템 상태, 사용자 통계, 계정 관리 기능 구현
- 시스템 로그 조회 기능 추가
- 나머지 관리 기능들 스켈레톤 구현 (개발 중 상태)
- 인코딩 문제 해결을 위한 영어 로그 메시지 적용
- hyungi 계정을 system 권한으로 설정
- JWT 토큰에 role 필드 추가
- 시스템 전용 API 엔드포인트 구현

주요 변경사항:
- web-ui/pages/dashboard/system.html: 시스템 관리자 전용 페이지
- web-ui/css/system-dashboard.css: 시스템 대시보드 전용 스타일
- web-ui/js/system-dashboard.js: 시스템 대시보드 로직
- api.hyungi.net/controllers/systemController.js: 시스템 API 컨트롤러
- api.hyungi.net/routes/systemRoutes.js: 시스템 API 라우트
- api.hyungi.net/controllers/authController.js: 시스템 권한 로그인 처리
- api.hyungi.net/services/auth.service.js: JWT 토큰에 role 필드 추가
This commit is contained in:
Hyungi Ahn
2025-08-18 11:16:18 +09:00
parent 809b2af53e
commit 2a3feca45b
14 changed files with 2797 additions and 27 deletions

View File

@@ -0,0 +1,789 @@
/* 시스템 대시보드 전용 스타일 */
/* 시스템 대시보드 배경 - 깔끔한 흰색 */
.main-layout .content-wrapper {
background: #ffffff;
min-height: calc(100vh - 80px);
padding: 2rem;
border-left: 1px solid #e0e0e0;
}
.page-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
color: #333;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 1rem;
}
.page-header h1 {
margin: 0;
font-size: 1.8rem;
font-weight: 500;
color: #2c3e50;
}
/* 시스템 배지 */
.system-badge {
background: #e74c3c;
color: white;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid #c0392b;
}
.header-right {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 1rem;
}
.logout-btn {
background: #e74c3c;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.logout-btn:hover {
background: #c0392b;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
}
/* 메인 컨텐츠 */
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: 0;
display: flex;
flex-direction: column;
gap: 2rem;
width: 100%;
}
/* 시스템 상태 개요 */
.system-overview {
margin-bottom: 2rem;
}
.system-overview h2 {
color: #2c3e50;
margin-bottom: 1.5rem;
font-size: 1.4rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid #ecf0f1;
padding-bottom: 0.5rem;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.status-card {
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s ease;
}
.status-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.status-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
background: linear-gradient(45deg, #3498db, #2980b9);
color: white;
}
.status-info h3 {
margin: 0 0 0.5rem 0;
color: #2c3e50;
font-size: 1rem;
font-weight: 500;
}
.status-value {
font-size: 1.3rem;
font-weight: 600;
margin: 0.5rem 0;
}
.status-value.online {
color: #27ae60;
}
.status-value.warning {
color: #f39c12;
}
.status-value.error {
color: #e74c3c;
}
.status-info small {
color: #7f8c8d;
font-size: 0.85rem;
}
/* 관리 섹션 */
.management-section {
margin-bottom: 2rem;
}
.management-section h2 {
color: #2c3e50;
margin-bottom: 1.5rem;
font-size: 1.4rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid #ecf0f1;
padding-bottom: 0.5rem;
}
.management-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.management-card {
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s ease;
display: flex;
flex-direction: column;
height: 100%;
}
.management-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.management-card.primary {
border-left: 4px solid #3498db;
}
.card-header {
display: flex;
align-items: center;
gap: 0.8rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #f0f0f0;
}
.card-header i {
font-size: 1.2rem;
color: #3498db;
}
.card-header h3 {
margin: 0;
color: #2c3e50;
font-size: 1.1rem;
font-weight: 500;
}
.card-content {
display: flex;
flex-direction: column;
height: 100%;
}
.card-content p {
color: #7f8c8d;
margin-bottom: 1.5rem;
line-height: 1.5;
font-size: 0.9rem;
flex: 1;
}
.card-actions {
display: flex;
gap: 0.8rem;
margin-top: auto;
}
.btn {
padding: 0.6rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
background: #ffffff;
text-decoration: none;
}
.btn-primary {
background: #3498db;
color: white;
border-color: #2980b9;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-secondary {
background: #95a5a6;
color: white;
border-color: #7f8c8d;
}
.btn-secondary:hover {
background: #7f8c8d;
}
/* 최근 활동 */
.recent-activity {
margin-bottom: 2rem;
}
.recent-activity h2 {
color: white;
margin-bottom: 1.5rem;
font-size: 1.8rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.activity-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.activity-list {
max-height: 400px;
overflow-y: auto;
}
.activity-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
transition: background-color 0.3s ease;
}
.activity-item:hover {
background: rgba(52, 152, 219, 0.05);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(45deg, #3498db, #2980b9);
color: white;
font-size: 1rem;
}
.activity-info h4 {
margin: 0 0 0.3rem 0;
color: #2c3e50;
font-size: 1rem;
font-weight: 600;
}
.activity-info p {
margin: 0;
color: #7f8c8d;
font-size: 0.9rem;
}
.activity-time {
margin-left: auto;
color: #95a5a6;
font-size: 0.8rem;
}
/* 모달 */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background: white;
margin: 5% auto;
padding: 0;
border-radius: 16px;
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
background: linear-gradient(45deg, #3498db, #2980b9);
color: white;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.3s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.modal-body {
padding: 2rem;
max-height: 60vh;
overflow-y: auto;
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.status-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.management-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
}
@media (max-width: 768px) {
.main-layout .content-wrapper {
padding: 1rem;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.page-header h1 {
font-size: 1.5rem;
}
.status-grid,
.management-grid {
grid-template-columns: 1fr;
gap: 0.8rem;
}
.status-card,
.management-card {
padding: 1rem;
}
.modal-content {
width: 95%;
margin: 5% auto;
}
.system-overview h2,
.management-section h2,
.recent-activity h2 {
font-size: 1.2rem;
}
}
@media (max-width: 480px) {
.main-layout .content-wrapper {
padding: 0.5rem;
}
.status-card,
.management-card {
padding: 0.8rem;
}
.btn {
padding: 0.5rem 0.8rem;
font-size: 0.8rem;
}
}
/* 계정 관리 스타일 */
.account-management {
padding: 1rem;
}
.account-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #ecf0f1;
}
.account-header h4 {
margin: 0;
color: #2c3e50;
font-size: 1.3rem;
font-weight: 600;
}
.account-filters {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.account-filters input,
.account-filters select {
padding: 0.7rem;
border: 2px solid #ecf0f1;
border-radius: 8px;
font-size: 0.9rem;
transition: border-color 0.3s ease;
}
.account-filters input:focus,
.account-filters select:focus {
outline: none;
border-color: #3498db;
}
.users-table {
overflow-x: auto;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.users-table table {
width: 100%;
border-collapse: collapse;
background: white;
}
.users-table th,
.users-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
.users-table th {
background: linear-gradient(45deg, #3498db, #2980b9);
color: white;
font-weight: 600;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.users-table tr:hover {
background: rgba(52, 152, 219, 0.05);
}
.role-badge {
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.role-badge.role-system {
background: linear-gradient(45deg, #e74c3c, #c0392b);
color: white;
}
.role-badge.role-admin {
background: linear-gradient(45deg, #f39c12, #e67e22);
color: white;
}
.role-badge.role-leader {
background: linear-gradient(45deg, #9b59b6, #8e44ad);
color: white;
}
.role-badge.role-user {
background: linear-gradient(45deg, #95a5a6, #7f8c8d);
color: white;
}
.status-badge {
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.status-badge.active {
background: linear-gradient(45deg, #27ae60, #2ecc71);
color: white;
}
.status-badge.inactive {
background: linear-gradient(45deg, #e74c3c, #c0392b);
color: white;
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.btn-small {
padding: 0.4rem 0.6rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.3s ease;
}
.btn-edit {
background: linear-gradient(45deg, #3498db, #2980b9);
color: white;
}
.btn-edit:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.btn-delete {
background: linear-gradient(45deg, #e74c3c, #c0392b);
color: white;
}
.btn-delete:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3);
}
.loading-spinner {
text-align: center;
padding: 3rem;
color: #7f8c8d;
}
.loading-spinner i {
font-size: 2rem;
margin-bottom: 1rem;
}
.error-message {
text-align: center;
padding: 3rem;
color: #e74c3c;
}
.error-message i {
font-size: 3rem;
margin-bottom: 1rem;
}
.error-message p {
margin: 1rem 0;
font-size: 1.1rem;
}
/* 알림 스타일 */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 3000;
animation: slideIn 0.3s ease;
}
.notification-info {
background: linear-gradient(45deg, #3498db, #2980b9);
}
.notification-success {
background: linear-gradient(45deg, #27ae60, #2ecc71);
}
.notification-warning {
background: linear-gradient(45deg, #f39c12, #e67e22);
}
.notification-error {
background: linear-gradient(45deg, #e74c3c, #c0392b);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 사용자 폼 스타일 */
.user-edit-form,
.user-create-form {
padding: 1.5rem;
}
.user-edit-form h4,
.user-create-form h4 {
margin: 0 0 2rem 0;
color: #2c3e50;
font-size: 1.3rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #2c3e50;
font-weight: 500;
font-size: 0.9rem;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.8rem;
border: 2px solid #ecf0f1;
border-radius: 8px;
font-size: 0.9rem;
transition: border-color 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.form-group input:disabled {
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 2px solid #ecf0f1;
}
/* 스크롤바 스타일링 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(45deg, #3498db, #2980b9);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(45deg, #2980b9, #3498db);
}

View File

@@ -13,8 +13,8 @@ function getApiBaseUrl() {
hostname === 'localhost' || hostname === '127.0.0.1' ||
hostname.includes('.local') || hostname.includes('hyungi')) {
// 현재 웹서버의 도메인/IP를 그대로 사용하되 API 포트(3005)로 직접 연결
const baseUrl = `${protocol}//${hostname}:3005/api`;
// 현재 웹서버의 도메인/IP를 그대로 사용하되 API 포트(20005)로 직접 연결
const baseUrl = `${protocol}//${hostname}:20005/api`;
console.log('✅ nginx 프록시 사용:', baseUrl);
return baseUrl;
@@ -22,7 +22,7 @@ function getApiBaseUrl() {
// 🚨 백업: 직접 접근 (nginx 프록시 실패시에만)
console.warn('⚠️ 직접 API 접근 (백업 모드)');
return `${protocol}//${hostname}:3005/api`;
return `${protocol}//${hostname}:20005/api`;
}
// API 설정

View File

@@ -0,0 +1,907 @@
// System Dashboard JavaScript
console.log('🚀 system-dashboard.js loaded');
import { apiRequest } from './api-helper.js';
import { getCurrentUser } from './auth.js';
console.log('📦 modules imported successfully');
// 전역 변수
let systemData = {
users: [],
logs: [],
systemStatus: {}
};
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
console.log('📄 DOM loaded, starting initialization');
initializeSystemDashboard();
setupEventListeners();
});
// Setup event listeners
function setupEventListeners() {
// Add event listeners to all data-action buttons
const actionButtons = document.querySelectorAll('[data-action]');
actionButtons.forEach(button => {
const action = button.getAttribute('data-action');
switch(action) {
case 'account-management':
button.addEventListener('click', openAccountManagement);
console.log('✅ Account management button listener added');
break;
case 'system-logs':
button.addEventListener('click', openSystemLogs);
console.log('✅ System logs button listener added');
break;
case 'database-management':
button.addEventListener('click', openDatabaseManagement);
console.log('✅ Database management button listener added');
break;
case 'system-settings':
button.addEventListener('click', openSystemSettings);
console.log('✅ System settings button listener added');
break;
case 'backup-management':
button.addEventListener('click', openBackupManagement);
console.log('✅ Backup management button listener added');
break;
case 'monitoring':
button.addEventListener('click', openMonitoring);
console.log('✅ Monitoring button listener added');
break;
case 'close-modal':
button.addEventListener('click', () => closeModal('account-modal'));
console.log('✅ Modal close button listener added');
break;
}
});
console.log(`🎯 Total ${actionButtons.length} event listeners setup completed`);
}
// Initialize system dashboard
async function initializeSystemDashboard() {
try {
console.log('🚀 Starting system dashboard initialization...');
// Load user info
await loadUserInfo();
console.log('✅ User info loaded');
// Load system status
await loadSystemStatus();
console.log('✅ System status loaded');
// Load user statistics
await loadUserStats();
console.log('✅ User statistics loaded');
// Load recent activities
await loadRecentActivities();
console.log('✅ Recent activities loaded');
// Setup auto-refresh (every 30 seconds)
setInterval(refreshSystemStatus, 30000);
console.log('🎉 System dashboard initialization completed');
} catch (error) {
console.error('❌ System dashboard initialization error:', error);
showNotification('Error loading system dashboard', 'error');
}
}
// 사용자 정보 로드
async function loadUserInfo() {
try {
const user = getCurrentUser();
if (user && user.name) {
document.getElementById('user-name').textContent = user.name;
}
} catch (error) {
console.error('사용자 정보 로드 오류:', error);
}
}
// 시스템 상태 로드
async function loadSystemStatus() {
try {
// 서버 상태 확인
const serverStatus = await checkServerStatus();
updateServerStatus(serverStatus);
// 데이터베이스 상태 확인
const dbStatus = await checkDatabaseStatus();
updateDatabaseStatus(dbStatus);
// 시스템 알림 확인
const alerts = await getSystemAlerts();
updateSystemAlerts(alerts);
} catch (error) {
console.error('시스템 상태 로드 오류:', error);
}
}
// 서버 상태 확인
async function checkServerStatus() {
try {
const response = await apiRequest('/api/system/status', 'GET');
return response.success ? 'online' : 'offline';
} catch (error) {
return 'offline';
}
}
// 데이터베이스 상태 확인
async function checkDatabaseStatus() {
try {
const response = await apiRequest('/api/system/db-status', 'GET');
return response;
} catch (error) {
return { status: 'error', connections: 0 };
}
}
// 시스템 알림 가져오기
async function getSystemAlerts() {
try {
const response = await apiRequest('/api/system/alerts', 'GET');
return response.alerts || [];
} catch (error) {
return [];
}
}
// 서버 상태 업데이트
function updateServerStatus(status) {
const serverCheckTime = document.getElementById('server-check-time');
const statusElements = document.querySelectorAll('.status-value');
if (serverCheckTime) {
serverCheckTime.textContent = new Date().toLocaleTimeString('ko-KR');
}
// 서버 상태 표시 업데이트 로직 추가
}
// 데이터베이스 상태 업데이트
function updateDatabaseStatus(dbStatus) {
const dbConnections = document.getElementById('db-connections');
if (dbConnections && dbStatus.connections !== undefined) {
dbConnections.textContent = dbStatus.connections;
}
}
// 시스템 알림 업데이트
function updateSystemAlerts(alerts) {
const systemAlerts = document.getElementById('system-alerts');
if (systemAlerts) {
systemAlerts.textContent = alerts.length;
systemAlerts.className = `status-value ${alerts.length > 0 ? 'warning' : 'online'}`;
}
}
// 사용자 통계 로드
async function loadUserStats() {
try {
const response = await apiRequest('/api/system/users/stats', 'GET');
if (response.success) {
const activeUsers = document.getElementById('active-users');
const totalUsers = document.getElementById('total-users');
if (activeUsers) activeUsers.textContent = response.data.active || 0;
if (totalUsers) totalUsers.textContent = response.data.total || 0;
}
} catch (error) {
console.error('사용자 통계 로드 오류:', error);
}
}
// 최근 활동 로드
async function loadRecentActivities() {
try {
const response = await apiRequest('/api/system/recent-activities', 'GET');
if (response.success && response.data) {
displayRecentActivities(response.data);
}
} catch (error) {
console.error('최근 활동 로드 오류:', error);
displayDefaultActivities();
}
}
// 최근 활동 표시
function displayRecentActivities(activities) {
const container = document.getElementById('recent-activities');
if (!container) return;
if (!activities || activities.length === 0) {
container.innerHTML = '<p style="text-align: center; color: #7f8c8d; padding: 2rem;">최근 활동이 없습니다.</p>';
return;
}
const html = activities.map(activity => `
<div class="activity-item">
<div class="activity-icon">
<i class="fas ${getActivityIcon(activity.type)}"></i>
</div>
<div class="activity-info">
<h4>${activity.title}</h4>
<p>${activity.description}</p>
</div>
<div class="activity-time">
${formatTimeAgo(activity.created_at)}
</div>
</div>
`).join('');
container.innerHTML = html;
}
// 기본 활동 표시 (데이터 로드 실패 시)
function displayDefaultActivities() {
const container = document.getElementById('recent-activities');
if (!container) return;
const defaultActivities = [
{
type: 'system',
title: '시스템 시작',
description: '시스템이 정상적으로 시작되었습니다.',
created_at: new Date().toISOString()
}
];
displayRecentActivities(defaultActivities);
}
// 활동 타입에 따른 아이콘 반환
function getActivityIcon(type) {
const icons = {
'login': 'fa-sign-in-alt',
'user_create': 'fa-user-plus',
'user_update': 'fa-user-edit',
'user_delete': 'fa-user-minus',
'system': 'fa-cog',
'database': 'fa-database',
'backup': 'fa-download',
'error': 'fa-exclamation-triangle'
};
return icons[type] || 'fa-info-circle';
}
// 시간 포맷팅 (몇 분 전, 몇 시간 전 등)
function formatTimeAgo(dateString) {
const now = new Date();
const date = new Date(dateString);
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) {
return '방금 전';
} else if (diffInSeconds < 3600) {
return `${Math.floor(diffInSeconds / 60)}분 전`;
} else if (diffInSeconds < 86400) {
return `${Math.floor(diffInSeconds / 3600)}시간 전`;
} else {
return `${Math.floor(diffInSeconds / 86400)}일 전`;
}
}
// 시스템 상태 새로고침
async function refreshSystemStatus() {
try {
await loadSystemStatus();
await loadUserStats();
} catch (error) {
console.error('시스템 상태 새로고침 오류:', error);
}
}
// Open account management
function openAccountManagement() {
console.log('🎯 Account management button clicked');
const modal = document.getElementById('account-modal');
const content = document.getElementById('account-management-content');
console.log('Modal element:', modal);
console.log('Content element:', content);
if (modal && content) {
console.log('✅ Modal and content elements found, loading content...');
// Load account management content
loadAccountManagementContent(content);
modal.style.display = 'block';
console.log('✅ Modal displayed');
} else {
console.error('❌ Modal or content element not found');
}
}
// 계정 관리 컨텐츠 로드
async function loadAccountManagementContent(container) {
try {
container.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i> 로딩 중...
</div>
`;
// 사용자 목록 로드
const response = await apiRequest('/api/system/users', 'GET');
if (response.success) {
displayAccountManagement(container, response.data);
} else {
throw new Error(response.error || '사용자 목록을 불러올 수 없습니다.');
}
} catch (error) {
console.error('계정 관리 컨텐츠 로드 오류:', error);
container.innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-triangle"></i>
<p>계정 정보를 불러오는 중 오류가 발생했습니다.</p>
<button class="btn btn-primary" onclick="loadAccountManagementContent(document.getElementById('account-management-content'))">
다시 시도
</button>
</div>
`;
}
}
// 계정 관리 화면 표시
function displayAccountManagement(container, users) {
const html = `
<div class="account-management">
<div class="account-header">
<h4><i class="fas fa-users"></i> 사용자 계정 관리</h4>
<button class="btn btn-primary" onclick="openCreateUserForm()">
<i class="fas fa-plus"></i> 새 사용자
</button>
</div>
<div class="account-filters">
<input type="text" id="user-search" placeholder="사용자 검색..." onkeyup="filterUsers()">
<select id="role-filter" onchange="filterUsers()">
<option value="">모든 권한</option>
<option value="system">시스템</option>
<option value="admin">관리자</option>
<option value="leader">그룹장</option>
<option value="user">사용자</option>
</select>
</div>
<div class="users-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>사용자명</th>
<th>이름</th>
<th>권한</th>
<th>상태</th>
<th>마지막 로그인</th>
<th>작업</th>
</tr>
</thead>
<tbody id="users-tbody">
${generateUsersTableRows(users)}
</tbody>
</table>
</div>
</div>
`;
container.innerHTML = html;
systemData.users = users;
}
// 사용자 테이블 행 생성
function generateUsersTableRows(users) {
if (!users || users.length === 0) {
return '<tr><td colspan="7" style="text-align: center; padding: 2rem;">등록된 사용자가 없습니다.</td></tr>';
}
return users.map(user => `
<tr data-user-id="${user.user_id}">
<td>${user.user_id}</td>
<td>${user.username}</td>
<td>${user.name || '-'}</td>
<td>
<span class="role-badge role-${user.role}">
${getRoleDisplayName(user.role)}
</span>
</td>
<td>
<span class="status-badge ${user.is_active ? 'active' : 'inactive'}">
${user.is_active ? '활성' : '비활성'}
</span>
</td>
<td>${user.last_login_at ? formatDate(user.last_login_at) : '없음'}</td>
<td class="action-buttons">
<button class="btn-small btn-edit" onclick="editUser(${user.user_id})" title="수정">
<i class="fas fa-edit"></i>
</button>
<button class="btn-small btn-delete" onclick="deleteUser(${user.user_id})" title="삭제">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
}
// 권한 표시명 반환
function getRoleDisplayName(role) {
const roleNames = {
'system': '시스템',
'admin': '관리자',
'leader': '그룹장',
'user': '사용자'
};
return roleNames[role] || role;
}
// 날짜 포맷팅
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('ko-KR');
}
// 시스템 로그 열기
function openSystemLogs() {
console.log('시스템 로그 버튼 클릭됨');
const modal = document.getElementById('account-modal');
const content = document.getElementById('account-management-content');
if (modal && content) {
loadSystemLogsContent(content);
modal.style.display = 'block';
}
}
// 시스템 로그 컨텐츠 로드
async function loadSystemLogsContent(container) {
try {
container.innerHTML = `
<div class="system-logs">
<h4><i class="fas fa-file-alt"></i> 시스템 로그</h4>
<div class="log-filters">
<select id="log-type-filter">
<option value="">모든 로그</option>
<option value="login">로그인</option>
<option value="activity">활동</option>
<option value="error">오류</option>
</select>
<input type="date" id="log-date-filter">
<button class="btn btn-primary" onclick="filterLogs()">
<i class="fas fa-search"></i> 검색
</button>
</div>
<div class="logs-container">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i> 로그 로딩 중...
</div>
</div>
</div>
`;
// 로그 데이터 로드
await loadLogsData();
} catch (error) {
console.error('시스템 로그 로드 오류:', error);
container.innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-triangle"></i>
<p>시스템 로그를 불러오는 중 오류가 발생했습니다.</p>
</div>
`;
}
}
// 로그 데이터 로드
async function loadLogsData() {
try {
const response = await apiRequest('/api/system/logs/activity', 'GET');
const logsContainer = document.querySelector('.logs-container');
if (response.success && response.data) {
displayLogs(response.data, logsContainer);
} else {
logsContainer.innerHTML = '<p>로그 데이터가 없습니다.</p>';
}
} catch (error) {
console.error('로그 데이터 로드 오류:', error);
document.querySelector('.logs-container').innerHTML = '<p>로그 데이터를 불러올 수 없습니다.</p>';
}
}
// 로그 표시
function displayLogs(logs, container) {
if (!logs || logs.length === 0) {
container.innerHTML = '<p>표시할 로그가 없습니다.</p>';
return;
}
const html = `
<table class="logs-table">
<thead>
<tr>
<th>시간</th>
<th>유형</th>
<th>사용자</th>
<th>내용</th>
</tr>
</thead>
<tbody>
${logs.map(log => `
<tr>
<td>${formatDate(log.created_at)}</td>
<td><span class="log-type ${log.type}">${log.type}</span></td>
<td>${log.username || '-'}</td>
<td>${log.description}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
container.innerHTML = html;
}
// 로그 필터링
function filterLogs() {
console.log('로그 필터링 실행');
// 실제 구현은 추후 추가
showNotification('로그 필터링 기능은 개발 중입니다.', 'info');
}
// 데이터베이스 관리 열기
function openDatabaseManagement() {
console.log('데이터베이스 관리 버튼 클릭됨');
showNotification('데이터베이스 관리 기능은 개발 중입니다.', 'info');
}
// 시스템 설정 열기
function openSystemSettings() {
console.log('시스템 설정 버튼 클릭됨');
showNotification('시스템 설정 기능은 개발 중입니다.', 'info');
}
// 백업 관리 열기
function openBackupManagement() {
console.log('백업 관리 버튼 클릭됨');
showNotification('백업 관리 기능은 개발 중입니다.', 'info');
}
// 모니터링 열기
function openMonitoring() {
console.log('모니터링 버튼 클릭됨');
showNotification('모니터링 기능은 개발 중입니다.', 'info');
}
// 모달 닫기
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
}
// 로그아웃
function logout() {
if (confirm('로그아웃 하시겠습니까?')) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
}
// 알림 표시
function showNotification(message, type = 'info') {
// 간단한 알림 표시 (나중에 토스트 라이브러리로 교체 가능)
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 5000);
}
// 사용자 편집
async function editUser(userId) {
try {
// 사용자 정보 가져오기
const response = await apiRequest(`/api/system/users`, 'GET');
if (!response.success) {
throw new Error('사용자 정보를 가져올 수 없습니다.');
}
const user = response.data.find(u => u.user_id === userId);
if (!user) {
throw new Error('해당 사용자를 찾을 수 없습니다.');
}
// 편집 폼 표시
showUserEditForm(user);
} catch (error) {
console.error('사용자 편집 오류:', error);
showNotification('사용자 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
// 사용자 편집 폼 표시
function showUserEditForm(user) {
const formHtml = `
<div class="user-edit-form">
<h4><i class="fas fa-user-edit"></i> 사용자 정보 수정</h4>
<form id="edit-user-form">
<div class="form-group">
<label for="edit-username">사용자명</label>
<input type="text" id="edit-username" value="${user.username}" disabled>
</div>
<div class="form-group">
<label for="edit-name">이름</label>
<input type="text" id="edit-name" value="${user.name || ''}" required>
</div>
<div class="form-group">
<label for="edit-email">이메일</label>
<input type="email" id="edit-email" value="${user.email || ''}">
</div>
<div class="form-group">
<label for="edit-role">권한</label>
<select id="edit-role" required>
<option value="system" ${user.role === 'system' ? 'selected' : ''}>시스템</option>
<option value="admin" ${user.role === 'admin' ? 'selected' : ''}>관리자</option>
<option value="leader" ${user.role === 'leader' ? 'selected' : ''}>그룹장</option>
<option value="user" ${user.role === 'user' ? 'selected' : ''}>사용자</option>
</select>
</div>
<div class="form-group">
<label for="edit-is-active">상태</label>
<select id="edit-is-active" required>
<option value="1" ${user.is_active ? 'selected' : ''}>활성</option>
<option value="0" ${!user.is_active ? 'selected' : ''}>비활성</option>
</select>
</div>
<div class="form-group">
<label for="edit-worker-id">작업자 ID</label>
<input type="number" id="edit-worker-id" value="${user.worker_id || ''}">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 저장
</button>
<button type="button" class="btn btn-secondary" onclick="closeModal('account-modal')">
<i class="fas fa-times"></i> 취소
</button>
</div>
</form>
</div>
`;
const container = document.getElementById('account-management-content');
container.innerHTML = formHtml;
// 폼 제출 이벤트 리스너
document.getElementById('edit-user-form').addEventListener('submit', async (e) => {
e.preventDefault();
await updateUser(user.user_id);
});
}
// 사용자 정보 업데이트
async function updateUser(userId) {
try {
const formData = {
name: document.getElementById('edit-name').value,
email: document.getElementById('edit-email').value || null,
role: document.getElementById('edit-role').value,
access_level: document.getElementById('edit-role').value,
is_active: parseInt(document.getElementById('edit-is-active').value),
worker_id: document.getElementById('edit-worker-id').value || null
};
const response = await apiRequest(`/api/system/users/${userId}`, 'PUT', formData);
if (response.success) {
showNotification('사용자 정보가 성공적으로 업데이트되었습니다.', 'success');
closeModal('account-modal');
// 계정 관리 다시 로드
setTimeout(() => openAccountManagement(), 500);
} else {
throw new Error(response.error || '업데이트에 실패했습니다.');
}
} catch (error) {
console.error('사용자 업데이트 오류:', error);
showNotification('사용자 정보 업데이트 중 오류가 발생했습니다.', 'error');
}
}
// 사용자 삭제
async function deleteUser(userId) {
try {
// 사용자 정보 가져오기
const response = await apiRequest(`/api/system/users`, 'GET');
if (!response.success) {
throw new Error('사용자 정보를 가져올 수 없습니다.');
}
const user = response.data.find(u => u.user_id === userId);
if (!user) {
throw new Error('해당 사용자를 찾을 수 없습니다.');
}
// 삭제 확인
if (!confirm(`정말로 사용자 '${user.username}'를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`)) {
return;
}
// 사용자 삭제 요청
const deleteResponse = await apiRequest(`/api/system/users/${userId}`, 'DELETE');
if (deleteResponse.success) {
showNotification('사용자가 성공적으로 삭제되었습니다.', 'success');
// 계정 관리 다시 로드
setTimeout(() => {
const container = document.getElementById('account-management-content');
if (container) {
loadAccountManagementContent(container);
}
}, 500);
} else {
throw new Error(deleteResponse.error || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('사용자 삭제 오류:', error);
showNotification('사용자 삭제 중 오류가 발생했습니다.', 'error');
}
}
// 새 사용자 생성 폼 열기
function openCreateUserForm() {
const formHtml = `
<div class="user-create-form">
<h4><i class="fas fa-user-plus"></i> 새 사용자 생성</h4>
<form id="create-user-form">
<div class="form-group">
<label for="create-username">사용자명 *</label>
<input type="text" id="create-username" required>
</div>
<div class="form-group">
<label for="create-password">비밀번호 *</label>
<input type="password" id="create-password" required minlength="6">
</div>
<div class="form-group">
<label for="create-name">이름 *</label>
<input type="text" id="create-name" required>
</div>
<div class="form-group">
<label for="create-email">이메일</label>
<input type="email" id="create-email">
</div>
<div class="form-group">
<label for="create-role">권한 *</label>
<select id="create-role" required>
<option value="">권한 선택</option>
<option value="system">시스템</option>
<option value="admin">관리자</option>
<option value="leader">그룹장</option>
<option value="user">사용자</option>
</select>
</div>
<div class="form-group">
<label for="create-worker-id">작업자 ID</label>
<input type="number" id="create-worker-id">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-plus"></i> 생성
</button>
<button type="button" class="btn btn-secondary" onclick="loadAccountManagementContent(document.getElementById('account-management-content'))">
<i class="fas fa-arrow-left"></i> 돌아가기
</button>
</div>
</form>
</div>
`;
const container = document.getElementById('account-management-content');
container.innerHTML = formHtml;
// 폼 제출 이벤트 리스너
document.getElementById('create-user-form').addEventListener('submit', async (e) => {
e.preventDefault();
await createUser();
});
}
// 새 사용자 생성
async function createUser() {
try {
const formData = {
username: document.getElementById('create-username').value,
password: document.getElementById('create-password').value,
name: document.getElementById('create-name').value,
email: document.getElementById('create-email').value || null,
role: document.getElementById('create-role').value,
access_level: document.getElementById('create-role').value,
worker_id: document.getElementById('create-worker-id').value || null
};
const response = await apiRequest('/api/system/users', 'POST', formData);
if (response.success) {
showNotification('새 사용자가 성공적으로 생성되었습니다.', 'success');
// 계정 관리 목록으로 돌아가기
setTimeout(() => {
const container = document.getElementById('account-management-content');
loadAccountManagementContent(container);
}, 500);
} else {
throw new Error(response.error || '사용자 생성에 실패했습니다.');
}
} catch (error) {
console.error('사용자 생성 오류:', error);
showNotification('사용자 생성 중 오류가 발생했습니다.', 'error');
}
}
// 사용자 필터링
function filterUsers() {
const searchTerm = document.getElementById('user-search').value.toLowerCase();
const roleFilter = document.getElementById('role-filter').value;
const rows = document.querySelectorAll('#users-tbody tr');
rows.forEach(row => {
const username = row.cells[1].textContent.toLowerCase();
const name = row.cells[2].textContent.toLowerCase();
const role = row.querySelector('.role-badge').textContent.toLowerCase();
const matchesSearch = username.includes(searchTerm) || name.includes(searchTerm);
const matchesRole = !roleFilter || role.includes(roleFilter);
row.style.display = matchesSearch && matchesRole ? '' : 'none';
});
}
// 모달 관련 함수들만 전역으로 노출 (동적으로 생성되는 HTML에서 사용)
window.closeModal = closeModal;
window.editUser = editUser;
window.deleteUser = deleteUser;
window.openCreateUserForm = openCreateUserForm;
window.filterUsers = filterUsers;
window.filterLogs = filterLogs;
// 테스트용 전역 함수
window.testFunction = function() {
console.log('🧪 테스트 함수 호출됨!');
alert('테스트 함수가 정상적으로 작동합니다!');
};
console.log('🌐 전역 함수들 노출 완료');
// 모달 외부 클릭 시 닫기
window.onclick = function(event) {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
};

View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>시스템 관리자 대시보드 - TK Portal</title>
<link rel="stylesheet" href="/css/main-layout.css">
<link rel="stylesheet" href="/css/admin.css">
<link rel="stylesheet" href="/css/system-dashboard.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="icon" type="image/png" href="/img/favicon.png">
<script type="module" src="/js/auth-check.js"></script>
</head>
<body>
<div class="main-layout">
<!-- 기존 네비게이션 바 사용 -->
<div id="navbar-container"></div>
<div class="content-wrapper">
<!-- 시스템 관리자 페이지 헤더 -->
<div class="page-header">
<h1><i class="fas fa-cogs"></i> 시스템 관리자</h1>
<span class="system-badge">SYSTEM</span>
</div>
<!-- 메인 컨텐츠 -->
<main class="main-content">
<!-- 시스템 상태 개요 -->
<section class="system-overview">
<h2><i class="fas fa-tachometer-alt"></i> 시스템 상태</h2>
<div class="status-grid">
<div class="status-card">
<div class="status-info">
<h3>서버 상태</h3>
<p class="status-value online">온라인</p>
<small>마지막 확인: <span id="server-check-time">--</span></small>
</div>
</div>
<div class="status-card">
<div class="status-info">
<h3>데이터베이스</h3>
<p class="status-value online">정상</p>
<small>연결 수: <span id="db-connections">--</span></small>
</div>
</div>
<div class="status-card">
<div class="status-info">
<h3>활성 사용자</h3>
<p class="status-value" id="active-users">--</p>
<small>총 사용자: <span id="total-users">--</span></small>
</div>
</div>
<div class="status-card">
<div class="status-info">
<h3>시스템 알림</h3>
<p class="status-value warning" id="system-alerts">--</p>
<small>미처리 알림</small>
</div>
</div>
</div>
</section>
<!-- 주요 관리 기능 -->
<section class="management-section">
<h2><i class="fas fa-tools"></i> 시스템 관리</h2>
<div class="management-grid">
<!-- 계정 관리 -->
<div class="management-card primary">
<div class="card-header">
<i class="fas fa-user-cog"></i>
<h3>계정 관리</h3>
</div>
<div class="card-content">
<p>사용자 계정 생성, 수정, 삭제 및 권한 관리</p>
<div class="card-actions">
<button class="btn btn-primary" data-action="account-management">
<i class="fas fa-users"></i> 계정 관리
</button>
</div>
</div>
</div>
<!-- 시스템 로그 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-file-alt"></i>
<h3>시스템 로그</h3>
</div>
<div class="card-content">
<p>로그인 이력, 시스템 활동 및 오류 로그 조회</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="system-logs">
<i class="fas fa-search"></i> 로그 조회
</button>
</div>
</div>
</div>
<!-- 데이터베이스 관리 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-database"></i>
<h3>데이터베이스</h3>
</div>
<div class="card-content">
<p>데이터베이스 백업, 복원 및 최적화</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="database-management">
<i class="fas fa-cog"></i> DB 관리
</button>
</div>
</div>
</div>
<!-- 시스템 설정 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-sliders-h"></i>
<h3>시스템 설정</h3>
</div>
<div class="card-content">
<p>전역 설정, 보안 정책 및 시스템 매개변수</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="system-settings">
<i class="fas fa-wrench"></i> 설정
</button>
</div>
</div>
</div>
<!-- 백업 관리 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-shield-alt"></i>
<h3>백업 관리</h3>
</div>
<div class="card-content">
<p>자동 백업 설정 및 복원 관리</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="backup-management">
<i class="fas fa-download"></i> 백업
</button>
</div>
</div>
</div>
<!-- 모니터링 -->
<div class="management-card">
<div class="card-header">
<i class="fas fa-chart-line"></i>
<h3>시스템 모니터링</h3>
</div>
<div class="card-content">
<p>성능 지표, 리소스 사용량 및 트래픽 분석</p>
<div class="card-actions">
<button class="btn btn-secondary" data-action="monitoring">
<i class="fas fa-eye"></i> 모니터링
</button>
</div>
</div>
</div>
</div>
</section>
<!-- 최근 활동 -->
<section class="recent-activity">
<h2><i class="fas fa-history"></i> 최근 시스템 활동</h2>
<div class="activity-container">
<div class="activity-list" id="recent-activities">
<!-- 동적으로 로드됨 -->
</div>
</div>
</section>
</main>
<!-- 계정 관리 모달 -->
<div id="account-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-user-cog"></i> 계정 관리</h3>
<button class="close-btn" data-action="close-modal">&times;</button>
</div>
<div class="modal-body">
<div id="account-management-content">
<!-- 계정 관리 내용이 여기에 로드됩니다 -->
</div>
</div>
</main>
</div>
</div>
<!-- 테스트용 인라인 스크립트 -->
<script>
console.log('🧪 인라인 스크립트 실행됨');
// 간단한 테스트 함수
function testClick() {
console.log('🎯 버튼 클릭 테스트 성공!');
alert('버튼이 정상적으로 작동합니다!');
}
// DOM 로드 후 이벤트 리스너 설정
document.addEventListener('DOMContentLoaded', function() {
console.log('📄 DOM 로드 완료');
const accountBtn = document.querySelector('[data-action="account-management"]');
if (accountBtn) {
accountBtn.addEventListener('click', testClick);
console.log('✅ 계정 관리 버튼에 테스트 이벤트 리스너 추가됨');
} else {
console.log('❌ 계정 관리 버튼을 찾을 수 없음');
}
});
</script>
<script type="module" src="/js/load-navbar.js"></script>
<script type="module" src="/js/system-dashboard.js"></script>
</body>
</html>