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:
@@ -1,5 +1,5 @@
|
||||
# .env
|
||||
PORT=3005
|
||||
PORT=20005
|
||||
|
||||
# MariaDB 컨테이너 초기화용 루트 패스워드
|
||||
DB_ROOT_PASSWORD=matxAc-jutty1-ruhsoc
|
||||
|
||||
@@ -24,8 +24,10 @@ const login = async (req, res) => {
|
||||
let redirectUrl;
|
||||
|
||||
switch (user.role) {
|
||||
case 'system': // 시스템 계정 전용 대시보드
|
||||
redirectUrl = '/pages/dashboard/system.html';
|
||||
break;
|
||||
case 'admin':
|
||||
case 'system': // 'system'도 관리자로 취급
|
||||
redirectUrl = '/pages/dashboard/admin.html';
|
||||
break;
|
||||
case 'leader':
|
||||
@@ -69,7 +71,7 @@ exports.register = async (req, res) => {
|
||||
|
||||
// 중복 아이디 확인
|
||||
const [existing] = await db.query(
|
||||
'SELECT user_id FROM users WHERE username = ?',
|
||||
'SELECT user_id FROM Users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -86,7 +88,7 @@ exports.register = async (req, res) => {
|
||||
// role 설정 (access_level에 따라)
|
||||
const roleMap = {
|
||||
'admin': 'admin',
|
||||
'system': 'admin',
|
||||
'system': 'system', // 시스템 계정은 system role로 설정
|
||||
'group_leader': 'leader',
|
||||
'support_team': 'support',
|
||||
'worker': 'user'
|
||||
@@ -95,7 +97,7 @@ exports.register = async (req, res) => {
|
||||
|
||||
// 사용자 등록
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO users (username, password, name, role, access_level, worker_id)
|
||||
`INSERT INTO Users (username, password, name, role, access_level, worker_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[username, hashedPassword, name, role, access_level, worker_id]
|
||||
);
|
||||
@@ -126,7 +128,7 @@ exports.deleteUser = async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query(
|
||||
'SELECT user_id FROM users WHERE user_id = ?',
|
||||
'SELECT user_id FROM Users WHERE user_id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
@@ -138,7 +140,7 @@ exports.deleteUser = async (req, res) => {
|
||||
}
|
||||
|
||||
// 사용자 삭제
|
||||
await db.query('DELETE FROM users WHERE user_id = ?', [id]);
|
||||
await db.query('DELETE FROM Users WHERE user_id = ?', [id]);
|
||||
|
||||
console.log('[사용자 삭제 성공] ID:', id);
|
||||
|
||||
@@ -165,7 +167,7 @@ exports.getAllUsers = async (req, res) => {
|
||||
// 비밀번호 제외하고 조회
|
||||
const [rows] = await db.query(
|
||||
`SELECT user_id, username, name, role, access_level, worker_id, created_at
|
||||
FROM users
|
||||
FROM Users
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
|
||||
506
api.hyungi.net/controllers/systemController.js
Normal file
506
api.hyungi.net/controllers/systemController.js
Normal file
@@ -0,0 +1,506 @@
|
||||
// 시스템 관리 컨트롤러
|
||||
const { getDb } = require('../dbPool');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
/**
|
||||
* 시스템 상태 확인
|
||||
*/
|
||||
exports.getSystemStatus = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 데이터베이스 연결 상태 확인
|
||||
const [dbStatus] = await db.query('SELECT 1 as status');
|
||||
|
||||
// 시스템 상태 정보
|
||||
const systemStatus = {
|
||||
server: 'online',
|
||||
database: dbStatus.length > 0 ? 'online' : 'offline',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
version: process.version
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: systemStatus
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('시스템 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '시스템 상태를 확인할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 데이터베이스 상태 확인
|
||||
*/
|
||||
exports.getDatabaseStatus = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 데이터베이스 연결 수 확인
|
||||
const [connections] = await db.query('SHOW STATUS LIKE "Threads_connected"');
|
||||
const [maxConnections] = await db.query('SHOW VARIABLES LIKE "max_connections"');
|
||||
|
||||
// 데이터베이스 크기 확인
|
||||
const [dbSize] = await db.query(`
|
||||
SELECT
|
||||
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: 'online',
|
||||
connections: parseInt(connections[0]?.Value || 0),
|
||||
max_connections: parseInt(maxConnections[0]?.Value || 0),
|
||||
size_mb: dbSize[0]?.size_mb || 0
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('데이터베이스 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '데이터베이스 상태를 확인할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 시스템 알림 조회
|
||||
*/
|
||||
exports.getSystemAlerts = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 최근 실패한 로그인 시도
|
||||
const [failedLogins] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM login_logs
|
||||
WHERE login_status = 'failed'
|
||||
AND login_time > DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||
`);
|
||||
|
||||
// 비활성 사용자 수
|
||||
const [inactiveusers] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM users
|
||||
WHERE is_active = 0
|
||||
`);
|
||||
|
||||
const alerts = [];
|
||||
|
||||
if (failedLogins[0]?.count > 5) {
|
||||
alerts.push({
|
||||
type: 'security',
|
||||
level: 'warning',
|
||||
message: `최근 1시간 동안 ${failedLogins[0].count}회의 로그인 실패가 발생했습니다.`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
if (inactiveusers[0]?.count > 0) {
|
||||
alerts.push({
|
||||
type: 'user',
|
||||
level: 'info',
|
||||
message: `${inactiveusers[0].count}명의 비활성 사용자가 있습니다.`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
alerts: alerts
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('시스템 알림 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '시스템 알림을 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 최근 시스템 활동 조회
|
||||
*/
|
||||
exports.getRecentActivities = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 최근 로그인 활동
|
||||
const [loginActivities] = await db.query(`
|
||||
SELECT
|
||||
ll.login_time as created_at,
|
||||
u.name as user_name,
|
||||
ll.login_status,
|
||||
ll.ip_address,
|
||||
'login' as activity_type
|
||||
FROM login_logs ll
|
||||
LEFT JOIN users u ON ll.user_id = u.user_id
|
||||
ORDER BY ll.login_time DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
// 비밀번호 변경 활동
|
||||
const [passwordActivities] = await db.query(`
|
||||
SELECT
|
||||
pcl.changed_at as created_at,
|
||||
u.name as user_name,
|
||||
pcl.change_type,
|
||||
'password_change' as activity_type
|
||||
FROM password_change_logs pcl
|
||||
LEFT JOIN users u ON pcl.user_id = u.user_id
|
||||
ORDER BY pcl.changed_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
// 활동 통합 및 정렬
|
||||
const activities = [
|
||||
...loginActivities.map(activity => ({
|
||||
type: activity.login_status === 'success' ? 'login' : 'login_failed',
|
||||
title: activity.login_status === 'success'
|
||||
? `${activity.user_name || '알 수 없는 사용자'} 로그인`
|
||||
: `로그인 실패 (${activity.ip_address})`,
|
||||
description: activity.login_status === 'success'
|
||||
? `IP: ${activity.ip_address}`
|
||||
: `사용자: ${activity.user_name || '알 수 없음'}`,
|
||||
created_at: activity.created_at
|
||||
})),
|
||||
...passwordActivities.map(activity => ({
|
||||
type: 'password_change',
|
||||
title: `${activity.user_name || '알 수 없는 사용자'} 비밀번호 변경`,
|
||||
description: `변경 유형: ${activity.change_type}`,
|
||||
created_at: activity.created_at
|
||||
}))
|
||||
];
|
||||
|
||||
// 시간순 정렬
|
||||
activities.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: activities.slice(0, 15)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('최근 활동 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '최근 활동을 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 통계 조회
|
||||
*/
|
||||
exports.getUserStats = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 전체 사용자 수
|
||||
const [totalusers] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
|
||||
// 활성 사용자 수
|
||||
const [activeusers] = await db.query('SELECT COUNT(*) as count FROM users WHERE is_active = 1');
|
||||
|
||||
// 최근 24시간 로그인 사용자 수
|
||||
const [recentLogins] = await db.query(`
|
||||
SELECT COUNT(DISTINCT user_id) as count
|
||||
FROM login_logs
|
||||
WHERE login_status = 'success'
|
||||
AND login_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
`);
|
||||
|
||||
// 권한별 사용자 수
|
||||
const [roleStats] = await db.query(`
|
||||
SELECT role, COUNT(*) as count
|
||||
FROM users
|
||||
WHERE is_active = 1
|
||||
GROUP BY role
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: totalusers[0]?.count || 0,
|
||||
active: activeusers[0]?.count || 0,
|
||||
recent_logins: recentLogins[0]?.count || 0,
|
||||
by_role: roleStats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 통계 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 통계를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 사용자 목록 조회 (시스템 관리자용)
|
||||
*/
|
||||
exports.getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const [users] = await db.query(`
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
role,
|
||||
access_level,
|
||||
worker_id,
|
||||
is_active,
|
||||
last_login_at,
|
||||
failed_login_attempts,
|
||||
locked_until,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: users
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 목록을 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 생성
|
||||
*/
|
||||
exports.createUser = async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, email, role, access_level, worker_id } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !password || !name || !role) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '필수 정보가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자명 중복 확인
|
||||
const [existing] = await db.query('SELECT user_id FROM users WHERE username = ?', [username]);
|
||||
if (existing.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 존재하는 사용자명입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 이메일 중복 확인 (이메일이 제공된 경우)
|
||||
if (email) {
|
||||
const [existingEmail] = await db.query('SELECT user_id FROM users WHERE email = ?', [email]);
|
||||
if (existingEmail.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 사용 중인 이메일입니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 사용자 생성
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO users (username, password, name, email, role, access_level, worker_id, is_active, created_at, password_changed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW(), NOW())
|
||||
`, [username, hashedPassword, name, email || null, role, access_level || role, worker_id || null]);
|
||||
|
||||
// 비밀번호 변경 로그 기록
|
||||
await db.query(`
|
||||
INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type)
|
||||
VALUES (?, ?, NOW(), 'initial')
|
||||
`, [result.insertId, req.user.user_id]);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '사용자가 성공적으로 생성되었습니다.',
|
||||
user_id: result.insertId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 생성 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 수정
|
||||
*/
|
||||
exports.updateUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, email, role, access_level, is_active, worker_id } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query('SELECT user_id FROM users WHERE user_id = ?', [id]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 이메일 중복 확인 (다른 사용자가 사용 중인지)
|
||||
if (email) {
|
||||
const [existingEmail] = await db.query(
|
||||
'SELECT user_id FROM users WHERE email = ? AND user_id != ?',
|
||||
[email, id]
|
||||
);
|
||||
if (existingEmail.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 사용 중인 이메일입니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
await db.query(`
|
||||
UPDATE users
|
||||
SET name = ?, email = ?, role = ?, access_level = ?, is_active = ?, worker_id = ?, updated_at = NOW()
|
||||
WHERE user_id = ?
|
||||
`, [name, email || null, role, access_level || role, is_active ? 1 : 0, worker_id || null, id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사용자 정보가 성공적으로 업데이트되었습니다.'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 수정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 삭제
|
||||
*/
|
||||
exports.deleteUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const db = await getDb();
|
||||
|
||||
// 자기 자신 삭제 방지
|
||||
if (parseInt(id) === req.user.user_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '자기 자신은 삭제할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query('SELECT user_id, username FROM users WHERE user_id = ?', [id]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 삭제 (관련 로그는 유지)
|
||||
await db.query('DELETE FROM users WHERE user_id = ?', [id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `사용자 '${user[0].username}'가 성공적으로 삭제되었습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('사용자 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자 비밀번호 재설정
|
||||
*/
|
||||
exports.resetUserPassword = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { new_password } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
if (!new_password || new_password.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '비밀번호는 최소 6자 이상이어야 합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query('SELECT user_id, username FROM users WHERE user_id = ?', [id]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(new_password, 10);
|
||||
|
||||
// 비밀번호 업데이트
|
||||
await db.query(`
|
||||
UPDATE users
|
||||
SET password = ?, password_changed_at = NOW(), failed_login_attempts = 0, locked_until = NULL
|
||||
WHERE user_id = ?
|
||||
`, [hashedPassword, id]);
|
||||
|
||||
// 비밀번호 변경 로그 기록
|
||||
await db.query(`
|
||||
INSERT INTO password_change_logs (user_id, changed_by_user_id, changed_at, change_type)
|
||||
VALUES (?, ?, NOW(), 'admin')
|
||||
`, [id, req.user.user_id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `사용자 '${user[0].username}'의 비밀번호가 재설정되었습니다.`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('비밀번호 재설정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '비밀번호 재설정 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ services:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./migrations:/docker-entrypoint-initdb.d # SQL 마이그레이션 자동 실행
|
||||
ports:
|
||||
- "3306:3306" # 개발 시 외부 접속용 (운영 시 제거)
|
||||
- "20306:3306" # RULES.md 준수: DB 포트 20306
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
condition: service_healthy # DB가 준비된 후 시작
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3005}:3005"
|
||||
- "20005:3005" # RULES.md 준수: API 포트 20005
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:80"
|
||||
- "20080:80" # RULES.md 준수: phpMyAdmin 포트 20080
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
|
||||
@@ -192,7 +192,8 @@ const healthRoutes = require('./routes/healthRoutes');
|
||||
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
||||
const dailyWorkReportRoutes = require('./routes/dailyWorkReportRoutes');
|
||||
const workAnalysisRoutes = require('./routes/workAnalysisRoutes');
|
||||
const analysisRoutes = require('./routes/analysisRoutes'); // 새로운 분석 라우트
|
||||
const analysisRoutes = require('./routes/analysisRoutes');
|
||||
const systemRoutes = require('./routes/systemRoutes'); // 새로운 분석 라우트
|
||||
|
||||
// 🔒 인증 미들웨어 가져오기
|
||||
const { verifyToken } = require('./middlewares/authMiddleware');
|
||||
@@ -307,6 +308,9 @@ app.use('/api/analysis', analysisRoutes); // 새로운 분석 라우트 등록
|
||||
|
||||
// 📊 리포트 및 분석
|
||||
app.use('/api/workreports', workReportRoutes);
|
||||
|
||||
// 🔧 시스템 관리 (시스템 권한만)
|
||||
app.use('/api/system', systemRoutes);
|
||||
app.use('/api/uploads', uploadRoutes);
|
||||
|
||||
// ⚙️ 시스템 데이터들 (모든 인증된 사용자)
|
||||
|
||||
0
api.hyungi.net/migrations/01_schema.sql
Normal file
0
api.hyungi.net/migrations/01_schema.sql
Normal file
45
api.hyungi.net/migrations/update_hyungi_system_role.sql
Normal file
45
api.hyungi.net/migrations/update_hyungi_system_role.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- hyungi 계정을 시스템 권한으로 업데이트
|
||||
-- 실행 날짜: 2025-01-XX
|
||||
|
||||
-- hyungi 계정의 role을 system으로 변경
|
||||
UPDATE Users
|
||||
SET
|
||||
role = 'system',
|
||||
access_level = 'system',
|
||||
name = '시스템 관리자',
|
||||
updated_at = NOW()
|
||||
WHERE username = 'hyungi';
|
||||
|
||||
-- 변경 결과 확인
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
role,
|
||||
access_level,
|
||||
is_active,
|
||||
updated_at
|
||||
FROM Users
|
||||
WHERE username = 'hyungi';
|
||||
|
||||
-- 시스템 권한 확인을 위한 쿼리
|
||||
SELECT
|
||||
username,
|
||||
name,
|
||||
role,
|
||||
access_level,
|
||||
CASE
|
||||
WHEN role = 'system' THEN '✅ 시스템 관리자'
|
||||
WHEN role = 'admin' THEN '🔧 관리자'
|
||||
WHEN role = 'leader' THEN '👨🏫 그룹장'
|
||||
ELSE '👤 일반 사용자'
|
||||
END as permission_level
|
||||
FROM Users
|
||||
ORDER BY
|
||||
CASE role
|
||||
WHEN 'system' THEN 1
|
||||
WHEN 'admin' THEN 2
|
||||
WHEN 'leader' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
username;
|
||||
@@ -97,7 +97,7 @@ router.post('/refresh-token', async (req, res) => {
|
||||
|
||||
// 사용자 정보 조회
|
||||
const [users] = await connection.execute(
|
||||
'SELECT * FROM users WHERE user_id = ? AND is_active = TRUE',
|
||||
'SELECT * FROM Users WHERE user_id = ? AND is_active = TRUE',
|
||||
[decoded.user_id]
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ router.post('/change-password', verifyToken, async (req, res) => {
|
||||
|
||||
// 현재 사용자의 비밀번호 조회
|
||||
const [users] = await connection.execute(
|
||||
'SELECT password FROM users WHERE user_id = ?',
|
||||
'SELECT password FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -283,7 +283,7 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
|
||||
|
||||
// 대상 사용자 확인
|
||||
const [users] = await connection.execute(
|
||||
'SELECT username, name FROM users WHERE user_id = ?',
|
||||
'SELECT username, name FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -400,7 +400,7 @@ router.get('/me', verifyToken, async (req, res) => {
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM users WHERE user_id = ?',
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -466,7 +466,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자명 중복 체크
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT user_id FROM users WHERE username = ?',
|
||||
'SELECT user_id FROM Users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -480,7 +480,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
// 이메일 중복 체크 (이메일이 제공된 경우)
|
||||
if (email) {
|
||||
const [existingEmail] = await connection.execute(
|
||||
'SELECT user_id FROM users WHERE email = ?',
|
||||
'SELECT user_id FROM Users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
@@ -561,7 +561,7 @@ router.get('/users', verifyToken, async (req, res) => {
|
||||
is_active,
|
||||
last_login_at,
|
||||
created_at
|
||||
FROM users
|
||||
FROM Users
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
@@ -638,7 +638,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT user_id, username FROM users WHERE user_id = ?',
|
||||
'SELECT user_id, username FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -662,7 +662,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
// 이메일 중복 체크
|
||||
if (email) {
|
||||
const [emailCheck] = await connection.execute(
|
||||
'SELECT user_id FROM users WHERE email = ? AND user_id != ?',
|
||||
'SELECT user_id FROM Users WHERE email = ? AND user_id != ?',
|
||||
[email, userId]
|
||||
);
|
||||
|
||||
@@ -732,7 +732,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 업데이트된 사용자 정보 조회
|
||||
const [updated] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM users WHERE user_id = ?',
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -786,7 +786,7 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT username FROM users WHERE user_id = ?',
|
||||
'SELECT username FROM Users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
|
||||
297
api.hyungi.net/routes/systemRoutes.js
Normal file
297
api.hyungi.net/routes/systemRoutes.js
Normal file
@@ -0,0 +1,297 @@
|
||||
// 시스템 관리 라우트
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const systemController = require('../controllers/systemController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
|
||||
// 시스템 권한 확인 미들웨어
|
||||
const requireSystemAccess = (req, res, next) => {
|
||||
if (!req.user || req.user.role !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '시스템 관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// 모든 라우트에 인증 및 시스템 권한 확인 적용
|
||||
router.use(verifyToken);
|
||||
router.use(requireSystemAccess);
|
||||
|
||||
// ===== 시스템 상태 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/status
|
||||
* 시스템 전체 상태 확인
|
||||
*/
|
||||
router.get('/status', systemController.getSystemStatus);
|
||||
|
||||
/**
|
||||
* GET /api/system/db-status
|
||||
* 데이터베이스 상태 확인
|
||||
*/
|
||||
router.get('/db-status', systemController.getDatabaseStatus);
|
||||
|
||||
/**
|
||||
* GET /api/system/alerts
|
||||
* 시스템 알림 조회
|
||||
*/
|
||||
router.get('/alerts', systemController.getSystemAlerts);
|
||||
|
||||
/**
|
||||
* GET /api/system/recent-activities
|
||||
* 최근 시스템 활동 조회
|
||||
*/
|
||||
router.get('/recent-activities', systemController.getRecentActivities);
|
||||
|
||||
// ===== 사용자 관리 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/users/stats
|
||||
* 사용자 통계 조회
|
||||
*/
|
||||
router.get('/users/stats', systemController.getUserStats);
|
||||
|
||||
/**
|
||||
* GET /api/system/users
|
||||
* 모든 사용자 목록 조회
|
||||
*/
|
||||
router.get('/users', systemController.getAllUsers);
|
||||
|
||||
/**
|
||||
* POST /api/system/users
|
||||
* 새 사용자 생성
|
||||
*/
|
||||
router.post('/users', systemController.createUser);
|
||||
|
||||
/**
|
||||
* PUT /api/system/users/:id
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
router.put('/users/:id', systemController.updateUser);
|
||||
|
||||
/**
|
||||
* DELETE /api/system/users/:id
|
||||
* 사용자 삭제
|
||||
*/
|
||||
router.delete('/users/:id', systemController.deleteUser);
|
||||
|
||||
/**
|
||||
* POST /api/system/users/:id/reset-password
|
||||
* 사용자 비밀번호 재설정
|
||||
*/
|
||||
router.post('/users/:id/reset-password', systemController.resetUserPassword);
|
||||
|
||||
// ===== 시스템 로그 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/login
|
||||
* 로그인 로그 조회
|
||||
*/
|
||||
router.get('/logs/login', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const { page = 1, limit = 50, status, user_id, start_date, end_date } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND ll.login_status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
whereClause += ' AND ll.user_id = ?';
|
||||
params.push(user_id);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND ll.login_time >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND ll.login_time <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
const [logs] = await db.query(`
|
||||
SELECT
|
||||
ll.log_id,
|
||||
ll.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
ll.login_time,
|
||||
ll.ip_address,
|
||||
ll.user_agent,
|
||||
ll.login_status,
|
||||
ll.failure_reason
|
||||
FROM login_logs ll
|
||||
LEFT JOIN Users u ON ll.user_id = u.user_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ll.login_time DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, [...params, parseInt(limit), parseInt(offset)]);
|
||||
|
||||
const [totalCount] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM login_logs ll
|
||||
WHERE ${whereClause}
|
||||
`, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: totalCount[0].count,
|
||||
pages: Math.ceil(totalCount[0].count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('로그인 로그 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '로그인 로그를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/password-changes
|
||||
* 비밀번호 변경 로그 조회
|
||||
*/
|
||||
router.get('/logs/password-changes', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const { page = 1, limit = 50 } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const [logs] = await db.query(`
|
||||
SELECT
|
||||
pcl.log_id,
|
||||
pcl.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
pcl.changed_by_user_id,
|
||||
admin.username as changed_by_username,
|
||||
admin.name as changed_by_name,
|
||||
pcl.changed_at,
|
||||
pcl.change_type,
|
||||
pcl.ip_address
|
||||
FROM password_change_logs pcl
|
||||
LEFT JOIN Users u ON pcl.user_id = u.user_id
|
||||
LEFT JOIN Users admin ON pcl.changed_by_user_id = admin.user_id
|
||||
ORDER BY pcl.changed_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, [parseInt(limit), parseInt(offset)]);
|
||||
|
||||
const [totalCount] = await db.query('SELECT COUNT(*) as count FROM password_change_logs');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: totalCount[0].count,
|
||||
pages: Math.ceil(totalCount[0].count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('비밀번호 변경 로그 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '비밀번호 변경 로그를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/activity
|
||||
* 활동 로그 조회 (activity_logs 테이블이 있는 경우)
|
||||
*/
|
||||
router.get('/logs/activity', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const { page = 1, limit = 50, activity_type, user_id } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (activity_type) {
|
||||
whereClause += ' AND al.activity_type = ?';
|
||||
params.push(activity_type);
|
||||
}
|
||||
|
||||
if (user_id) {
|
||||
whereClause += ' AND al.user_id = ?';
|
||||
params.push(user_id);
|
||||
}
|
||||
|
||||
const [logs] = await db.query(`
|
||||
SELECT
|
||||
al.log_id,
|
||||
al.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
al.activity_type,
|
||||
al.table_name,
|
||||
al.record_id,
|
||||
al.action,
|
||||
al.ip_address,
|
||||
al.user_agent,
|
||||
al.created_at
|
||||
FROM activity_logs al
|
||||
LEFT JOIN Users u ON al.user_id = u.user_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, [...params, parseInt(limit), parseInt(offset)]);
|
||||
|
||||
const [totalCount] = await db.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM activity_logs al
|
||||
WHERE ${whereClause}
|
||||
`, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: totalCount[0].count,
|
||||
pages: Math.ceil(totalCount[0].count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('활동 로그 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '활동 로그를 조회할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -57,7 +57,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
|
||||
|
||||
|
||||
const token = jwt.sign(
|
||||
{ user_id: user.user_id, username: user.username, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
|
||||
{ user_id: user.user_id, username: user.username, role: user.role, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
@@ -80,6 +80,7 @@ const loginService = async (username, password, ipAddress, userAgent) => {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name || user.username,
|
||||
role: user.role,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id
|
||||
}
|
||||
|
||||
789
web-ui/css/system-dashboard.css
Normal file
789
web-ui/css/system-dashboard.css
Normal 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);
|
||||
}
|
||||
@@ -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 설정
|
||||
|
||||
907
web-ui/js/system-dashboard.js
Normal file
907
web-ui/js/system-dashboard.js
Normal 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';
|
||||
}
|
||||
});
|
||||
};
|
||||
219
web-ui/pages/dashboard/system.html
Normal file
219
web-ui/pages/dashboard/system.html
Normal 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">×</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>
|
||||
Reference in New Issue
Block a user