fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선
- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산 - 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader) - 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql) - synology_deployment 버전에도 동일 수정 적용
This commit is contained in:
@@ -368,17 +368,28 @@ const updateWorkReport = async (req, res) => {
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제 (V2 - Service Layer 사용)
|
||||
* 권한: 그룹장(group_leader), 시스템(system), 관리자(admin)만 가능
|
||||
*/
|
||||
const removeDailyWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id: reportId } = req.params;
|
||||
const userInfo = {
|
||||
user_id: req.user?.user_id || req.user?.id,
|
||||
access_level: req.user?.access_level || req.user?.role,
|
||||
};
|
||||
|
||||
if (!userInfo.user_id) {
|
||||
return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' });
|
||||
}
|
||||
|
||||
// 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능
|
||||
const allowedRoles = ['admin', 'system', 'group_leader'];
|
||||
if (!allowedRoles.includes(userInfo.access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '작업보고서 삭제 권한이 없습니다.',
|
||||
details: '그룹장 이상의 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await dailyWorkReportService.removeDailyWorkReportService(reportId, userInfo);
|
||||
|
||||
@@ -405,6 +416,7 @@ const removeDailyWorkReport = async (req, res) => {
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
const access_level = req.user?.access_level || req.user?.role;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
@@ -412,6 +424,15 @@ const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능
|
||||
const allowedRoles = ['admin', 'system', 'group_leader'];
|
||||
if (!allowedRoles.includes(access_level)) {
|
||||
return res.status(403).json({
|
||||
error: '작업보고서 삭제 권한이 없습니다.',
|
||||
details: '그룹장 이상의 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
|
||||
247
api.hyungi.net/controllers/userController.js
Normal file
247
api.hyungi.net/controllers/userController.js
Normal file
@@ -0,0 +1,247 @@
|
||||
// controllers/userController.js - 사용자 관리 컨트롤러
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { ApiError, asyncHandler } = require('../utils/errorHandler');
|
||||
const db = require('../db');
|
||||
|
||||
/**
|
||||
* 모든 사용자 조회
|
||||
*/
|
||||
const getAllUsers = asyncHandler(async (req, res) => {
|
||||
console.log('👥 모든 사용자 조회 요청');
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
role,
|
||||
access_level,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_login
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
const [users] = await db.execute(query);
|
||||
|
||||
console.log(`✅ 사용자 ${users.length}명 조회 완료`);
|
||||
|
||||
res.success(users, '사용자 목록 조회 성공');
|
||||
});
|
||||
|
||||
/**
|
||||
* 특정 사용자 조회
|
||||
*/
|
||||
const getUserById = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
console.log(`👤 사용자 조회: ID ${id}`);
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
role,
|
||||
access_level,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_login
|
||||
FROM users
|
||||
WHERE user_id = ?
|
||||
`;
|
||||
|
||||
const [users] = await db.execute(query, [id]);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new ApiError('사용자를 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
console.log(`✅ 사용자 조회 완료: ${users[0].name}`);
|
||||
|
||||
res.success(users[0], '사용자 조회 성공');
|
||||
});
|
||||
|
||||
/**
|
||||
* 새 사용자 생성
|
||||
*/
|
||||
const createUser = asyncHandler(async (req, res) => {
|
||||
const { username, name, email, phone, role, password } = req.body;
|
||||
|
||||
console.log(`👤 새 사용자 생성: ${name} (${username})`);
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !name || !role || !password) {
|
||||
throw new ApiError('필수 필드가 누락되었습니다.', 400);
|
||||
}
|
||||
|
||||
// 사용자명 중복 확인
|
||||
const checkQuery = 'SELECT user_id FROM users WHERE username = ?';
|
||||
const [existing] = await db.execute(checkQuery, [username]);
|
||||
|
||||
if (existing.length > 0) {
|
||||
throw new ApiError('이미 존재하는 사용자명입니다.', 400);
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 사용자 생성
|
||||
const insertQuery = `
|
||||
INSERT INTO users (username, name, email, phone, role, access_level, password_hash, is_active, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW())
|
||||
`;
|
||||
|
||||
const [result] = await db.execute(insertQuery, [
|
||||
username,
|
||||
name,
|
||||
email || null,
|
||||
phone || null,
|
||||
role,
|
||||
role, // access_level을 role과 동일하게 설정
|
||||
hashedPassword
|
||||
]);
|
||||
|
||||
console.log(`✅ 사용자 생성 완료: ID ${result.insertId}`);
|
||||
|
||||
res.created({ user_id: result.insertId }, '사용자가 성공적으로 생성되었습니다.');
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
const updateUser = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { username, name, email, phone, role, password } = req.body;
|
||||
|
||||
console.log(`👤 사용자 수정: ID ${id}`);
|
||||
|
||||
// 사용자 존재 확인
|
||||
const checkQuery = 'SELECT user_id FROM users WHERE user_id = ?';
|
||||
const [existing] = await db.execute(checkQuery, [id]);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new ApiError('사용자를 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
// 업데이트할 필드들
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
if (username) {
|
||||
// 사용자명 중복 확인 (자신 제외)
|
||||
const dupQuery = 'SELECT user_id FROM users WHERE username = ? AND user_id != ?';
|
||||
const [duplicate] = await db.execute(dupQuery, [username, id]);
|
||||
|
||||
if (duplicate.length > 0) {
|
||||
throw new ApiError('이미 존재하는 사용자명입니다.', 400);
|
||||
}
|
||||
|
||||
updates.push('username = ?');
|
||||
values.push(username);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
updates.push('name = ?');
|
||||
values.push(name);
|
||||
}
|
||||
|
||||
if (email !== undefined) {
|
||||
updates.push('email = ?');
|
||||
values.push(email || null);
|
||||
}
|
||||
|
||||
if (phone !== undefined) {
|
||||
updates.push('phone = ?');
|
||||
values.push(phone || null);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
updates.push('role = ?, access_level = ?');
|
||||
values.push(role, role);
|
||||
}
|
||||
|
||||
if (password) {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
updates.push('password_hash = ?');
|
||||
values.push(hashedPassword);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
throw new ApiError('수정할 내용이 없습니다.', 400);
|
||||
}
|
||||
|
||||
updates.push('updated_at = NOW()');
|
||||
values.push(id);
|
||||
|
||||
const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`;
|
||||
|
||||
await db.execute(updateQuery, values);
|
||||
|
||||
console.log(`✅ 사용자 수정 완료: ID ${id}`);
|
||||
|
||||
res.success({ user_id: id }, '사용자 정보가 성공적으로 수정되었습니다.');
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 상태 변경 (활성화/비활성화)
|
||||
*/
|
||||
const updateUserStatus = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { is_active } = req.body;
|
||||
|
||||
console.log(`👤 사용자 상태 변경: ID ${id}, 활성화: ${is_active}`);
|
||||
|
||||
const query = 'UPDATE users SET is_active = ?, updated_at = NOW() WHERE user_id = ?';
|
||||
const [result] = await db.execute(query, [is_active ? 1 : 0, id]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('사용자를 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
console.log(`✅ 사용자 상태 변경 완료: ID ${id}`);
|
||||
|
||||
res.success({ user_id: id, is_active }, '사용자 상태가 성공적으로 변경되었습니다.');
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자 삭제
|
||||
*/
|
||||
const deleteUser = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
console.log(`👤 사용자 삭제: ID ${id}`);
|
||||
|
||||
// 자기 자신 삭제 방지
|
||||
if (req.user && req.user.user_id == id) {
|
||||
throw new ApiError('자기 자신은 삭제할 수 없습니다.', 400);
|
||||
}
|
||||
|
||||
const query = 'DELETE FROM users WHERE user_id = ?';
|
||||
const [result] = await db.execute(query, [id]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new ApiError('사용자를 찾을 수 없습니다.', 404);
|
||||
}
|
||||
|
||||
console.log(`✅ 사용자 삭제 완료: ID ${id}`);
|
||||
|
||||
res.success({ user_id: id }, '사용자가 성공적으로 삭제되었습니다.');
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserStatus,
|
||||
deleteUser
|
||||
};
|
||||
@@ -431,7 +431,7 @@ class WorkAnalysisController {
|
||||
) as error_rate_percent
|
||||
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.project_id, p.project_name, p.job_no, dwr.work_type_id, wt.name
|
||||
|
||||
@@ -30,6 +30,8 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy # DB가 준비된 후 시작
|
||||
redis:
|
||||
condition: service_healthy # Redis가 준비된 후 시작
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "20005:3005" # RULES.md 준수: API 포트 20005
|
||||
@@ -37,6 +39,8 @@ services:
|
||||
- ./.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
volumes:
|
||||
- ./public/img:/usr/src/app/public/img:ro
|
||||
- ./uploads:/usr/src/app/uploads
|
||||
@@ -67,16 +71,22 @@ services:
|
||||
- PMA_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- UPLOAD_LIMIT=50M
|
||||
|
||||
# Redis 캐시 서버 (선택사항 - 세션 관리 및 속도 제한용)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: redis_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-yourredispassword}
|
||||
# Redis 캐시 서버 (세션 관리 및 속도 제한용)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: redis_hyungi_net
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "20379:6379" # RULES.md 준수: Redis 포트 20379
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
networks:
|
||||
- default
|
||||
|
||||
# Nginx 리버스 프록시 (선택사항 - HTTPS 및 로드밸런싱용)
|
||||
# nginx:
|
||||
@@ -95,7 +105,8 @@ services:
|
||||
volumes:
|
||||
db_data:
|
||||
driver: local
|
||||
# redis_data: # Redis 사용 시 주석 해제
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -343,6 +343,293 @@ app.use('/api/performance', performanceRoutes);
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/tools', toolsRoute);
|
||||
|
||||
// 👤 사용자 관리 API (관리자 전용)
|
||||
app.get('/api/users', async (req, res) => {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (!req.user || !['admin', 'system'].includes(req.user.access_level)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
const [users] = await db.execute(`
|
||||
SELECT user_id, username, name, role, access_level, is_active, created_at, worker_id
|
||||
FROM users
|
||||
WHERE is_active = 1
|
||||
ORDER BY user_id
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사용자 목록 조회 성공',
|
||||
data: users
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사용자 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 목록을 불러올 수 없습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 👤 사용자 정보 수정 API (관리자 전용)
|
||||
app.put('/api/users/:id', async (req, res) => {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (!req.user || !['admin', 'system'].includes(req.user.access_level)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const userId = req.params.id;
|
||||
const { username, name, role, access_level, is_active } = req.body;
|
||||
|
||||
// undefined 값을 null로 변환
|
||||
const safeUsername = username !== undefined ? username : null;
|
||||
const safeName = name !== undefined ? name : null;
|
||||
const safeRole = role !== undefined ? role : null;
|
||||
const safeAccessLevel = access_level !== undefined ? access_level : null;
|
||||
const safeIsActive = is_active !== undefined ? is_active : null;
|
||||
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
const [result] = await db.execute(`
|
||||
UPDATE users
|
||||
SET username = ?, name = ?, role = ?, access_level = ?, is_active = ?, updated_at = NOW()
|
||||
WHERE user_id = ?
|
||||
`, [safeUsername, safeName, safeRole, safeAccessLevel, safeIsActive, userId]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사용자 정보가 성공적으로 수정되었습니다.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 정보 수정에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 👤 사용자 삭제 API (관리자 전용)
|
||||
app.delete('/api/users/:id', async (req, res) => {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (!req.user || !['admin', 'system'].includes(req.user.access_level)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const userId = req.params.id;
|
||||
|
||||
// 자기 자신은 삭제할 수 없도록 방지
|
||||
if (parseInt(userId) === req.user.user_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '자기 자신은 삭제할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 사용자 존재 여부 확인
|
||||
const [existingUser] = await db.execute(`
|
||||
SELECT user_id, username FROM users WHERE user_id = ?
|
||||
`, [userId]);
|
||||
|
||||
if (existingUser.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 삭제 (실제로는 비활성화)
|
||||
const [result] = await db.execute(`
|
||||
UPDATE users
|
||||
SET is_active = 0, updated_at = NOW()
|
||||
WHERE user_id = ?
|
||||
`, [userId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `사용자 '${existingUser[0].username}'가 성공적으로 비활성화되었습니다.`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사용자 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 삭제에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 👤 사용자 생성 API (관리자 전용)
|
||||
app.post('/api/users', async (req, res) => {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (!req.user || !['admin', 'system'].includes(req.user.access_level)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const { username, name, role, access_level, password } = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !name || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '사용자명, 이름, 비밀번호는 필수 입력 항목입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 중복 사용자명 확인
|
||||
const [existingUser] = await db.execute(`
|
||||
SELECT user_id FROM users WHERE username = ?
|
||||
`, [username]);
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '이미 존재하는 사용자명입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const bcrypt = require('bcryptjs');
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// undefined 값을 null로 변환 및 role에 따른 access_level 자동 설정
|
||||
const safeRole = role !== undefined ? role : null;
|
||||
|
||||
// role에 따라 access_level 자동 설정
|
||||
let safeAccessLevel;
|
||||
if (access_level !== undefined) {
|
||||
safeAccessLevel = access_level;
|
||||
} else if (safeRole === 'admin') {
|
||||
safeAccessLevel = 'admin';
|
||||
} else if (safeRole === 'leader' || safeRole === 'group_leader') {
|
||||
safeAccessLevel = 'group_leader';
|
||||
} else {
|
||||
safeAccessLevel = 'worker';
|
||||
}
|
||||
|
||||
// 사용자 생성
|
||||
const [result] = await db.execute(`
|
||||
INSERT INTO users (username, name, password, role, access_level, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, NOW(), NOW())
|
||||
`, [username, name, hashedPassword, safeRole, safeAccessLevel]);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `사용자 '${username}'가 성공적으로 생성되었습니다.`,
|
||||
data: {
|
||||
user_id: result.insertId,
|
||||
username: username,
|
||||
name: name,
|
||||
role: safeRole,
|
||||
access_level: safeAccessLevel,
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사용자 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 생성에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 👤 사용자 상태 변경 API (관리자 전용)
|
||||
app.put('/api/users/:id/status', async (req, res) => {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (!req.user || !['admin', 'system'].includes(req.user.access_level)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const userId = req.params.id;
|
||||
const { is_active } = req.body;
|
||||
|
||||
// 자기 자신의 상태는 변경할 수 없도록 방지
|
||||
if (parseInt(userId) === req.user.user_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '자기 자신의 상태는 변경할 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const { getDb } = require('./dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
// 사용자 존재 여부 확인
|
||||
const [existingUser] = await db.execute(`
|
||||
SELECT user_id, username, is_active FROM users WHERE user_id = ?
|
||||
`, [userId]);
|
||||
|
||||
if (existingUser.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 변경
|
||||
const newStatus = is_active !== undefined ? is_active : !existingUser[0].is_active;
|
||||
const [result] = await db.execute(`
|
||||
UPDATE users
|
||||
SET is_active = ?, updated_at = NOW()
|
||||
WHERE user_id = ?
|
||||
`, [newStatus, userId]);
|
||||
|
||||
const statusText = newStatus ? '활성화' : '비활성화';
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `사용자 '${existingUser[0].username}'가 성공적으로 ${statusText}되었습니다.`,
|
||||
data: {
|
||||
user_id: parseInt(userId),
|
||||
username: existingUser[0].username,
|
||||
is_active: newStatus
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사용자 상태 변경 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '사용자 상태 변경에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 📤 파일 업로드
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
-- 009_fix_duplicate_monthly_status.sql
|
||||
-- monthly_worker_status 테이블의 중복 데이터 정리
|
||||
|
||||
-- 1. 중복 데이터 확인 (디버깅용)
|
||||
-- SELECT worker_id, date, COUNT(*) as cnt
|
||||
-- FROM monthly_worker_status
|
||||
-- GROUP BY worker_id, date
|
||||
-- HAVING cnt > 1;
|
||||
|
||||
-- 2. 중복 데이터 정리: 같은 worker_id, date에 대해 최신 데이터만 남기고 나머지 삭제
|
||||
DELETE mws1 FROM monthly_worker_status mws1
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
worker_id,
|
||||
date,
|
||||
MAX(id) as keep_id
|
||||
FROM monthly_worker_status
|
||||
GROUP BY worker_id, date
|
||||
) mws2 ON mws1.worker_id = mws2.worker_id
|
||||
AND mws1.date = mws2.date
|
||||
AND mws1.id < mws2.keep_id;
|
||||
|
||||
-- 3. 중복 제거 후 데이터 재집계 (선택사항)
|
||||
-- 만약 합산이 필요하다면 다음 프로시저를 실행
|
||||
DELIMITER $$
|
||||
|
||||
CREATE OR REPLACE PROCEDURE ConsolidateDuplicateMonthlyStatus()
|
||||
BEGIN
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
DECLARE v_worker_id INT;
|
||||
DECLARE v_date DATE;
|
||||
|
||||
-- 중복이 있는 worker_id, date 조합 찾기
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT worker_id, date
|
||||
FROM monthly_worker_status
|
||||
GROUP BY worker_id, date
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN cur;
|
||||
|
||||
read_loop: LOOP
|
||||
FETCH cur INTO v_worker_id, v_date;
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
-- 해당 작업자의 해당 날짜 데이터를 재계산하여 업데이트
|
||||
CALL UpdateMonthlyWorkerStatus(v_date, v_worker_id);
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 4. 프로시저 실행하여 중복 데이터 통합
|
||||
-- CALL ConsolidateDuplicateMonthlyStatus();
|
||||
|
||||
-- 5. 확인: 중복이 남아있는지 체크
|
||||
SELECT
|
||||
'중복 체크 완료' as message,
|
||||
COUNT(*) as remaining_duplicates
|
||||
FROM (
|
||||
SELECT worker_id, date, COUNT(*) as cnt
|
||||
FROM monthly_worker_status
|
||||
GROUP BY worker_id, date
|
||||
HAVING cnt > 1
|
||||
) duplicates;
|
||||
|
||||
@@ -203,7 +203,7 @@ class WorkAnalysis {
|
||||
dwr.created_at
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
|
||||
@@ -37,7 +37,7 @@ const getAnalysis = async (startDate, endDate) => {
|
||||
const byProjectSql = `
|
||||
SELECT p.project_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.worker_id) as participants
|
||||
FROM DailyWorkReports dwr
|
||||
JOIN Projects p ON dwr.project_id = p.project_id
|
||||
JOIN projects p ON dwr.project_id = p.project_id
|
||||
${whereClause}
|
||||
GROUP BY p.project_name
|
||||
HAVING hours > 0
|
||||
@@ -73,7 +73,7 @@ const getAnalysis = async (startDate, endDate) => {
|
||||
t.category as task_category, dwr.work_details,
|
||||
(${workHoursCalc}) as work_hours, dwr.memo
|
||||
FROM DailyWorkReports dwr
|
||||
JOIN Projects p ON dwr.project_id = p.project_id
|
||||
JOIN projects p ON dwr.project_id = p.project_id
|
||||
JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
JOIN Tasks t ON dwr.task_id = t.task_id
|
||||
${whereClause}
|
||||
|
||||
@@ -47,7 +47,7 @@ const getAllByDate = async (date) => {
|
||||
t.category, t.subcategory, d.description
|
||||
FROM DailyIssueReports d
|
||||
LEFT JOIN workers w ON d.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON d.project_id = p.project_id
|
||||
LEFT JOIN projects p ON d.project_id = p.project_id
|
||||
LEFT JOIN IssueTypes t ON d.issue_type_id = t.issue_type_id
|
||||
WHERE d.date = ?
|
||||
ORDER BY d.start_time ASC`,
|
||||
|
||||
@@ -45,14 +45,30 @@ class MonthlyStatusModel {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 중복 방지: worker_id와 date로 그룹화하고 최신 데이터만 조회
|
||||
const [rows] = await db.execute(`
|
||||
SELECT
|
||||
mws.*,
|
||||
mws.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type
|
||||
w.job_type,
|
||||
MAX(mws.year) as year,
|
||||
MAX(mws.month) as month,
|
||||
mws.date,
|
||||
SUM(mws.total_work_hours) as total_work_hours,
|
||||
SUM(mws.actual_work_hours) as actual_work_hours,
|
||||
SUM(mws.vacation_hours) as vacation_hours,
|
||||
SUM(mws.total_work_count) as total_work_count,
|
||||
SUM(mws.regular_work_count) as regular_work_count,
|
||||
SUM(mws.error_work_count) as error_work_count,
|
||||
MAX(mws.work_status) as work_status,
|
||||
MAX(mws.has_vacation) as has_vacation,
|
||||
MAX(mws.has_error) as has_error,
|
||||
MAX(mws.has_issues) as has_issues,
|
||||
MAX(mws.last_updated) as last_updated
|
||||
FROM monthly_worker_status mws
|
||||
JOIN workers w ON mws.worker_id = w.worker_id
|
||||
WHERE mws.date = ?
|
||||
GROUP BY mws.worker_id, mws.date, w.worker_name, w.job_type
|
||||
ORDER BY w.worker_name ASC
|
||||
`, [date]);
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ const create = async (report, callback) => {
|
||||
wr.memo
|
||||
FROM WorkReports wr
|
||||
LEFT JOIN workers w ON wr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON wr.project_id = p.project_id
|
||||
LEFT JOIN projects p ON wr.project_id = p.project_id
|
||||
LEFT JOIN Tasks t ON wr.task_id = t.task_id
|
||||
WHERE wr.\`date\` = ?
|
||||
ORDER BY w.worker_name ASC
|
||||
|
||||
35
api.hyungi.net/package-lock.json
generated
35
api.hyungi.net/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@simplewebauthn/server": "^13.1.1",
|
||||
"async-retry": "^1.3.3",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
@@ -770,6 +771,29 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt/node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
@@ -3001,6 +3025,17 @@
|
||||
"node": ">= 10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"@simplewebauthn/server": "^13.1.1",
|
||||
"async-retry": "^1.3.3",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
44
api.hyungi.net/routes/userRoutes.js
Normal file
44
api.hyungi.net/routes/userRoutes.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// routes/userRoutes.js - 사용자 관리 라우터
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const userController = require('../controllers/userController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(verifyToken);
|
||||
|
||||
// 관리자 권한 확인 미들웨어
|
||||
const adminOnly = (req, res, next) => {
|
||||
if (req.user && (req.user.role === 'admin' || req.user.role === 'system')) {
|
||||
next();
|
||||
} else {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자 권한이 필요합니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 라우트에 관리자 권한 적용
|
||||
router.use(adminOnly);
|
||||
|
||||
// 📋 사용자 목록 조회
|
||||
router.get('/', userController.getAllUsers);
|
||||
|
||||
// 👤 특정 사용자 조회
|
||||
router.get('/:id', userController.getUserById);
|
||||
|
||||
// ➕ 새 사용자 생성
|
||||
router.post('/', userController.createUser);
|
||||
|
||||
// ✏️ 사용자 정보 수정
|
||||
router.put('/:id', userController.updateUser);
|
||||
|
||||
// 🔄 사용자 상태 변경
|
||||
router.put('/:id/status', userController.updateUserStatus);
|
||||
|
||||
// 🗑️ 사용자 삭제
|
||||
router.delete('/:id', userController.deleteUser);
|
||||
|
||||
module.exports = router;
|
||||
@@ -20,23 +20,19 @@ const initRedis = async () => {
|
||||
const redis = require('redis');
|
||||
|
||||
redisClient = redis.createClient({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
socket: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
reconnectStrategy: (retries) => {
|
||||
if (retries > 10) {
|
||||
console.warn('Redis 재시도 횟수 초과. 메모리 캐시를 사용합니다.');
|
||||
return false; // Redis 연결 포기
|
||||
}
|
||||
return Math.min(retries * 100, 3000);
|
||||
}
|
||||
},
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db: process.env.REDIS_DB || 0,
|
||||
retry_strategy: (options) => {
|
||||
if (options.error && options.error.code === 'ECONNREFUSED') {
|
||||
console.warn('Redis 서버에 연결할 수 없습니다. 메모리 캐시를 사용합니다.');
|
||||
return undefined; // Redis 연결 포기
|
||||
}
|
||||
if (options.total_retry_time > 1000 * 60 * 60) {
|
||||
return new Error('Redis 재시도 시간 초과');
|
||||
}
|
||||
if (options.attempt > 10) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.min(options.attempt * 100, 3000);
|
||||
}
|
||||
database: process.env.REDIS_DB || 0
|
||||
});
|
||||
|
||||
redisClient.on('error', (err) => {
|
||||
|
||||
Reference in New Issue
Block a user