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

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