diff --git a/api.hyungi.net/.env b/api.hyungi.net/.env
index 81ae4f5..f530d5c 100644
--- a/api.hyungi.net/.env
+++ b/api.hyungi.net/.env
@@ -1,5 +1,5 @@
# .env
-PORT=3005
+PORT=20005
# MariaDB 컨테이너 초기화용 루트 패스워드
DB_ROOT_PASSWORD=matxAc-jutty1-ruhsoc
diff --git a/api.hyungi.net/controllers/authController.js b/api.hyungi.net/controllers/authController.js
index 589f8b4..4c363fd 100644
--- a/api.hyungi.net/controllers/authController.js
+++ b/api.hyungi.net/controllers/authController.js
@@ -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`
);
diff --git a/api.hyungi.net/controllers/systemController.js b/api.hyungi.net/controllers/systemController.js
new file mode 100644
index 0000000..b44c738
--- /dev/null
+++ b/api.hyungi.net/controllers/systemController.js
@@ -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: '비밀번호 재설정 중 오류가 발생했습니다.'
+ });
+ }
+};
diff --git a/api.hyungi.net/docker-compose.yml b/api.hyungi.net/docker-compose.yml
index 34c0415..53c737f 100644
--- a/api.hyungi.net/docker-compose.yml
+++ b/api.hyungi.net/docker-compose.yml
@@ -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:
diff --git a/api.hyungi.net/index.js b/api.hyungi.net/index.js
index 35e8b74..66bc8f4 100644
--- a/api.hyungi.net/index.js
+++ b/api.hyungi.net/index.js
@@ -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);
// ⚙️ 시스템 데이터들 (모든 인증된 사용자)
diff --git a/api.hyungi.net/migrations/01_schema.sql b/api.hyungi.net/migrations/01_schema.sql
new file mode 100644
index 0000000..e69de29
diff --git a/api.hyungi.net/migrations/update_hyungi_system_role.sql b/api.hyungi.net/migrations/update_hyungi_system_role.sql
new file mode 100644
index 0000000..a864eb9
--- /dev/null
+++ b/api.hyungi.net/migrations/update_hyungi_system_role.sql
@@ -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;
diff --git a/api.hyungi.net/routes/authRoutes.js b/api.hyungi.net/routes/authRoutes.js
index fd8b057..14928ba 100644
--- a/api.hyungi.net/routes/authRoutes.js
+++ b/api.hyungi.net/routes/authRoutes.js
@@ -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]
);
diff --git a/api.hyungi.net/routes/systemRoutes.js b/api.hyungi.net/routes/systemRoutes.js
new file mode 100644
index 0000000..8ba265f
--- /dev/null
+++ b/api.hyungi.net/routes/systemRoutes.js
@@ -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;
diff --git a/api.hyungi.net/services/auth.service.js b/api.hyungi.net/services/auth.service.js
index 743f79f..621b2a6 100644
--- a/api.hyungi.net/services/auth.service.js
+++ b/api.hyungi.net/services/auth.service.js
@@ -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
}
diff --git a/web-ui/css/system-dashboard.css b/web-ui/css/system-dashboard.css
new file mode 100644
index 0000000..29ce9f0
--- /dev/null
+++ b/web-ui/css/system-dashboard.css
@@ -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);
+}
diff --git a/web-ui/js/api-config.js b/web-ui/js/api-config.js
index 6fbc3d1..946e515 100644
--- a/web-ui/js/api-config.js
+++ b/web-ui/js/api-config.js
@@ -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 설정
diff --git a/web-ui/js/system-dashboard.js b/web-ui/js/system-dashboard.js
new file mode 100644
index 0000000..0d08f7c
--- /dev/null
+++ b/web-ui/js/system-dashboard.js
@@ -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 = '
최근 활동이 없습니다.
';
+ return;
+ }
+
+ const html = activities.map(activity => `
+
+
+
+
+
+
${activity.title}
+
${activity.description}
+
+
+ ${formatTimeAgo(activity.created_at)}
+
+
+ `).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 = `
+
+ 로딩 중...
+
+ `;
+
+ // 사용자 목록 로드
+ 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 = `
+
+
+
계정 정보를 불러오는 중 오류가 발생했습니다.
+
+
+ `;
+ }
+}
+
+// 계정 관리 화면 표시
+function displayAccountManagement(container, users) {
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ 사용자명 |
+ 이름 |
+ 권한 |
+ 상태 |
+ 마지막 로그인 |
+ 작업 |
+
+
+
+ ${generateUsersTableRows(users)}
+
+
+
+
+ `;
+
+ container.innerHTML = html;
+ systemData.users = users;
+}
+
+// 사용자 테이블 행 생성
+function generateUsersTableRows(users) {
+ if (!users || users.length === 0) {
+ return '| 등록된 사용자가 없습니다. |
';
+ }
+
+ return users.map(user => `
+
+ | ${user.user_id} |
+ ${user.username} |
+ ${user.name || '-'} |
+
+
+ ${getRoleDisplayName(user.role)}
+
+ |
+
+
+ ${user.is_active ? '활성' : '비활성'}
+
+ |
+ ${user.last_login_at ? formatDate(user.last_login_at) : '없음'} |
+
+
+
+ |
+
+ `).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 = `
+
+
시스템 로그
+
+
+
+
+
+
+
+ `;
+
+ // 로그 데이터 로드
+ await loadLogsData();
+
+ } catch (error) {
+ console.error('시스템 로그 로드 오류:', error);
+ container.innerHTML = `
+
+
+
시스템 로그를 불러오는 중 오류가 발생했습니다.
+
+ `;
+ }
+}
+
+// 로그 데이터 로드
+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 = '로그 데이터가 없습니다.
';
+ }
+ } catch (error) {
+ console.error('로그 데이터 로드 오류:', error);
+ document.querySelector('.logs-container').innerHTML = '로그 데이터를 불러올 수 없습니다.
';
+ }
+}
+
+// 로그 표시
+function displayLogs(logs, container) {
+ if (!logs || logs.length === 0) {
+ container.innerHTML = '표시할 로그가 없습니다.
';
+ return;
+ }
+
+ const html = `
+
+
+
+ | 시간 |
+ 유형 |
+ 사용자 |
+ 내용 |
+
+
+
+ ${logs.map(log => `
+
+ | ${formatDate(log.created_at)} |
+ ${log.type} |
+ ${log.username || '-'} |
+ ${log.description} |
+
+ `).join('')}
+
+
+ `;
+
+ 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 = `
+
+ `;
+
+ 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 = `
+
+ `;
+
+ 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';
+ }
+ });
+};
diff --git a/web-ui/pages/dashboard/system.html b/web-ui/pages/dashboard/system.html
new file mode 100644
index 0000000..40090a9
--- /dev/null
+++ b/web-ui/pages/dashboard/system.html
@@ -0,0 +1,219 @@
+
+
+
+
+
+ 시스템 관리자 대시보드 - TK Portal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 시스템 상태
+
+
+
+
서버 상태
+
온라인
+
마지막 확인: --
+
+
+
+
+
데이터베이스
+
정상
+
연결 수: --
+
+
+
+
+
활성 사용자
+
--
+
총 사용자: --
+
+
+
+
+
+
+
+
+ 시스템 관리
+
+
+
+
+
+
사용자 계정 생성, 수정, 삭제 및 권한 관리
+
+
+
+
+
+
+
+
+
+
+
로그인 이력, 시스템 활동 및 오류 로그 조회
+
+
+
+
+
+
+
+
+
+
+
데이터베이스 백업, 복원 및 최적화
+
+
+
+
+
+
+
+
+
+
+
전역 설정, 보안 정책 및 시스템 매개변수
+
+
+
+
+
+
+
+
+
+
+
자동 백업 설정 및 복원 관리
+
+
+
+
+
+
+
+
+
+
+
성능 지표, 리소스 사용량 및 트래픽 분석
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+