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

@@ -1,5 +1,5 @@
# .env
PORT=3005
PORT=20005
# MariaDB 컨테이너 초기화용 루트 패스워드
DB_ROOT_PASSWORD=matxAc-jutty1-ruhsoc

View File

@@ -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`
);

View 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: '비밀번호 재설정 중 오류가 발생했습니다.'
});
}
};

View File

@@ -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:

View File

@@ -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);
// ⚙️ 시스템 데이터들 (모든 인증된 사용자)

View File

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

View File

@@ -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]
);

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

View File

@@ -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
}

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>