Files
TK-FB-Project/api.hyungi.net/controllers/systemController.js
Hyungi Ahn 2a3feca45b 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 필드 추가
2025-08-18 11:16:18 +09:00

507 lines
14 KiB
JavaScript

// 시스템 관리 컨트롤러
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: '비밀번호 재설정 중 오류가 발생했습니다.'
});
}
};