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
|
||||
|
||||
Reference in New Issue
Block a user